微信对国人而言,想必大名鼎鼎,活跃用户数已经突破6.5亿,足以说明这款应用的生命力。可是使用人数众多,不表明微信的API设计优异,有过微信公众号开发经验的人,想必复杂的报文,众多的服务API以及各类公众号资源与权限设置搞得头痛。其实Tiny框架设计理念之一就是简化开发人员的工做,设计Tiny微信框架能够必定程度上减小通常开发人员的难度。 java
前段时间本人写过一篇博文《微信框架的几个层次》,提到了十个层级,介绍以前先说一下微信的消息通信机制,主要分为被动推送和主动请求两种模式: json
1、被动推送模式。此时微信服务器是通信发起方,用户服务器是通信接收方。 缓存
这种模式下推送报文分两类:消息和事件。如用户在微信客户端发送的文本消息、图片消息在通信层面上就是消息报文;而事件报文通常用于处理异步响应,好比用户点击微信菜单触发菜单事件等。 安全
2、主动请求模式。此时用户服务器是通信发起方,而微信服务器则是通信接收方。 服务器
主动请求场景不少,微信开发平台提供的大部分API都是这种模式,如自定义菜单、素材管理、支付等。而微信服务器与微信客户端之间的数据更新有如下两种方式: 微信
Tiny微信框架的核心接口如图所示: session
以上接口涵盖了微信通信、报文转换、消息接收和发送、上下文会话、业务处理等诸多方面,接口说明以下: 微信开发
接口
|
接口说明
|
---|---|
WeiXinConnector | 微信链接管理,管理接收消息和请求消息,同时保持微信的通信信息(验证令牌和JS访问票据等) |
WeiXinContext | 微信上下文环境,支持保存微信的用户会话,也能够记录各个业务处理器的操做结果。 |
WeiXinConvert | 微信消息/结果转换统一接口,支持优先级排序 |
WeiXinHandler | 微信业务处理器,支持按优先级排序。按类型能够分为发送和接收处理器。开发人员须要扩展该接口实现业务逻辑。 |
WeiXinManager | 微信配置管理器,负责加载微信API接口相关参数,和渲染微信URL。 |
WeiXinReceiver | 微信接收消息器,负责接收微信服务器推送过来的消息和事件,WeiXinConnector委托其接收消息。 |
WeiXinSender | 微信发送消息器,负责发送消息和上传文件到微信服务器,并处理响应,WeiXinConnector委托其发送消息。 |
WeiXinSession | 微信用户会话,目前以微信的openId作主键。 |
WeiXinSessionManager | 微信会话管理器,负责新增、修改和清理微信用户会话。 |
微信的服务主要是基于HTTP协议,安全经过访问令牌(access_token)保证;少数业务场景使用HTTPS加密协议,甚至涉及安全证书,例如微信商户的支付接口。 架构
Tiny微信框架的通信处理由WeiXinConnector总调度,接口定义以下: app
public interface WeiXinConnector { /** * 默认的bean配置名称 */ public static final String DEFAULT_BEAN_NAME="weiXinConnector"; public static final String ACCESS_TOKEN="ACCESS_TOKEN"; /** * 获取当前的管理号客户端信息 * @return */ Client getClient(); /** * 得到微信消息发送者,负责往微信服务器发送消息 * @return */ WeiXinSender getWeiXinSender(); /** * 得到微信消息接收者,负责解析微信服务器推送过来的消息 * @return */ WeiXinReceiver getWeiXinReceiver(); /** * 获取微信的会话管理者 * @return */ WeiXinSessionManager getWeiXinSessionManager(); /** * 获取微信验证令牌 * @return */ AccessToken getAccessToken(); /** * 得到微信的JS访问票据 * @return */ JsApiTicket getJsApiTicket(); /** * 发送微信消息 * @param message */ void send(ToServerMessage message); /** * 上传微信文件 * @param upload */ void upload(WeiXinHttpUpload upload); /** * 接收微信消息 * @param request * @param response */ void receive(HttpServletRequest request,HttpServletResponse response); }
public interface WeiXinHttpConnector { /** * 默认的bean配置名称 */ public static final String DEFAULT_BEAN_NAME="weiXinHttpConnector"; /** * 用get方式访问微信URL * * @param url 要访问的微信URL * @return 请求结果 */ String getUrl(String url); /** * 用post方式访问微信URL * * @param url 要访问的微信URL * @param content * @param cert * @return 请求结果 */ String postUrl(String url, String content,WeiXinCert cert); /** * 上传文件 * @param url * @param upload * @return */ String upload(String url,WeiXinHttpUpload upload); }目前微信框架实现了HttpClient3.1和HttpClient4.5.1两个版本的底层通信方案,对开发人员而言只须要配置不一样的依赖,而无需关心具体通信代码。
本人一直对微信的报文设计很有微词,从总体上看微信报文缺少统一规范,XML、JSON格式混用,字段命名也不规范。Tiny微信提供WeiXinConvert接口负责报文与对象之间的转换,目前XML报文经过Xsteam转换,JSON报文经过fastjson转换。接口定义以下:
public interface WeiXinConvert extends Comparable<WeiXinConvert> { /** * 得到优先级 * @return */ int getPriority(); /** * 设置优先级 * @param priority */ void setPriority(int priority); /** * 得到报文的状态 * @return */ WeiXinConvertMode getWeiXinConvertMode(); /** * 得到结果类型 * @return */ Class<?> getCalssType(); /** * 判断转换接口可否处理输入信息(微信报文会出现不一样类型报文字段一致的状况,须要根据上下文判断) * @param <INPUT> * @param input * @param context * @return */ <INPUT> boolean isMatch(INPUT input,WeiXinContext context); /** * 转换消息(微信报文会出现不一样类型报文字段一致的状况,须要根据上下文判断) * @param input * @return */ <OUTPUT,INPUT> OUTPUT convert(INPUT input,WeiXinContext context); }
微信发送报文调试最麻烦的地方就是访问令牌(access_token),这个是根据用户应用动态生成的,并且只保持两个小时有效。Tiny微信框架提供了模拟测试页面,只须要bean配置页面设置相关appId和APP秘钥等参数,开发人员在页面就无需手动输入访问令牌。测试页面以下:
接收报文一般是用来模拟手机端的发送消息,特别是一些复杂交互场景:如命令行菜单,若是每次都经过手机端调试。效率很是低。而经过本测试页面,直接输入模拟的手机报文直接就能够获得报文结果,准确而且快速。模拟页面如图:
前面在介绍微信核心接口时提到过WeiXinReceiver和WeiXinSender,分别处理微信推送消息与主动发送消息。可是用户的业务是复杂多变的,Tiny是如何保证微信框架的可扩展性呢?其实WeiXinReceiver和WeiXinSender是由一组有序WeiXinHandler组成,而每个WeiXinHandler均可以处理一类消息,接口定义以下:
public interface WeiXinHandler extends Comparable<WeiXinHandler> { int getPriority(); void setPriority(int priority); WeiXinHandlerMode getWeiXinHandlerMode(); /** * 是否匹配对象和上下文 * @param <T> * @param message * @return */ <T> boolean isMatch(T message,WeiXinContext context); /** * 处理对象 * @param <T> * @param message * @param context */ <T> void process(T message,WeiXinContext context); }
简单举个例子,好比开发一个图片消息处理器ImageMessageHandler,用来处理微信客户端的图片类消息,代码以下:
public class ImageMessageHandler extends AbstractWeiXinHandler{ public WeiXinHandlerMode getWeiXinHandlerMode() { return WeiXinHandlerMode.RECEIVE; } public <T> boolean isMatch(T message, WeiXinContext context) { return message instanceof ImageMessage; } //具体业务处理 public <T> void process(T message, WeiXinContext context) { ImageMessage mess = (ImageMessage) message; //逻辑处理 TextReplyMessage replyMessage= new TextReplyMessage(); replyMessage.setContent("回复图片消息["+mess.getPicUrl()+"]"); replyMessage.setToUserName(mess.getFromUserName()); replyMessage.setFromUserName(mess.getToUserName()); replyMessage.setCreateTime((int)(System.currentTimeMillis()/1000)); context.setOutput(replyMessage); } }
用户主要是编写isMatch和process这两个函数,前者决定这个业务类能处理哪些微信消息和事件,后者是真正的业务处理类。微信消息的包装和转换由微信框架提供,用户应该关心业务处理逻辑,原则上一个Handler只建议处理一类消息。编写完毕后,须要将Handler配置成bean文件,微信框架就能调用了。
ImageMessageHandler的做用是接收微信客户端发送的图片类消息,并返回图片地址给用户,效果以下:
WeiXinSession接口定义以下:
public interface WeiXinSession extends Serializable{ /** * 会话Id * @return */ String getSessionId(); /** * 是否包含某元素 * @param name * @return */ boolean contains(String name); /** * 返回指定name的序列化对象 * @param <T> * @param name * @return */ <T extends Serializable> T getParameter(String name); /** * 设置序列化的参数对象 * @param <T> * @param name * @param value */ <T extends Serializable> void setParameter(String name,T value); /** * 取得session的建立时间。 * * @return 建立时间戮 */ long getCreationTime(); /** * 取得最近访问时间。 * * @return 最近访问时间戮 */ long getLastAccessedTime(); /** * 取得session的最大不活动期限,超过此时间,session就会失效。 * * @return 不活动期限的秒数,0表示永不过时 */ int getMaxInactiveInterval(); /** * 设置session的最大不活动期限,单位秒 * @param maxInactiveInterval */ void setMaxInactiveInterval(int maxInactiveInterval); /** * 判断session有没有过时。 * * @return 若是过时了,则返回<code>true</code> */ boolean isExpired(); /** * 更新session */ void update(); }
public interface WeiXinSessionManager { /** * 默认的bean配置名称 */ public static final String DEFAULT_BEAN_NAME="weiXinSessionManager"; /** * 建立会话 * @param sessionId * @return */ WeiXinSession createWeiXinSession(String sessionId); /** * 查询会话 * @param sessionId * @return */ WeiXinSession getWeiXinSession(String sessionId); /** * 添加会话 * @param session */ void addWeiXinSession(WeiXinSession session); /** * 手动删除会话 * @param sessionId * @return */ void removeWeiXinSession(String sessionId); /** * 遍历会话 * @return */ WeiXinSession[] getWeiXinSessions(); /** * 清理会话过时的Session */ void expireWeiXinSessions(); /** * 清理所有Session */ void clear(); /** * Session最大过时时间设置,单位s,默认0 * @return */ int getMaxInactiveInterval(); /** * Session清理线程首次延迟时间,单位s,默认值60 * @return */ int getExpireTimerDelay(); /** * Session清理线程运行周期,单位s,默认值300 * @return */ int getExpireTimePeriod(); }
配置文件是以menuconfig.xml为结尾,以演示工程的command.menuconfig.xml为例:
<!-- 菜单命令节点支持多个菜单配置节点和系统命令节点 --> <menu-configs> <!-- 菜单配置节点能够嵌套,支持定义子菜单和菜单命令节点 --> <menu-config id="m001" name="menu" title="功能目录" > <regex><![CDATA[m|menu|菜单]]></regex> <description><![CDATA[微信服务列表]]></description> <menu-config id="g001" name="guess" title="数字竞猜" path="/game/guessNumber.page"> <regex><![CDATA[guess|猜数字]]></regex> <description><![CDATA[猜数字小游戏,输入guess或者猜数字]]></description> <menu-command name="new" title="新建游戏" event-type="enter" class-name="org.tinygroup.weixinservice.commandhandler.NewGuessGameHandler"> <regex><![CDATA[new|新游戏]]></regex> <description><![CDATA[输入“新游戏”或者“new”,从新开始猜数字]]></description> </menu-command> <menu-command name="input" title="输入数值" class-name="org.tinygroup.weixinservice.commandhandler.GuessNumberHandler" path="/game/guessNumberResult.page"> <regex><![CDATA[^[1-9]\d*$]]></regex> <description><![CDATA[请输入1-50之间的整数]]></description> </menu-command> <menu-command name="del" title="清理用户数据" event-type="exit" class-name="org.tinygroup.weixinservice.commandhandler.DelGuessNumberSessionHandler" path="/menucommand/showMenuConfig.page"> <regex><![CDATA[del|delete]]></regex> <description><![CDATA[输入del或者delete]]></description> </menu-command> </menu-config> <menu-config id="g002" name="robot" title="机器人" > <regex><![CDATA[robot]]></regex> <description><![CDATA[输入robot]]></description> <menu-command name="input" title="问答环节" event-type="enter" system-enable="false" bean-name="askRobotHandler" path="/game/answer.page"> <regex><![CDATA[[\u4e00-\u9fa5_a-zA-Z0-9]+$]]></regex> <description><![CDATA[向智能机器人进行提问]]></description> </menu-command> </menu-config> <menu-config id="g003" name="time" title="时间转换"> <menu-command name="1" title="显示中式时间" class-name="org.tinygroup.weixinservice.commandhandler.TimeHandler" path="/game/chineseTime.page"> <regex><![CDATA[1]]></regex> <description><![CDATA[输入1展现中式时间]]></description> </menu-command> <menu-command name="2" title="显示英式时间" class-name="org.tinygroup.weixinservice.commandhandler.TimeHandler" path="/game/englishTime.page"> <regex><![CDATA[2]]></regex> <description><![CDATA[输入2展现英式时间]]></description> </menu-command> <regex><![CDATA[time]]></regex> <description><![CDATA[展现中式和英式的系统时间]]></description> </menu-config> </menu-config> <!-- 系统命令节点 --> <system-command name="root" title="返回根菜单" bean-name="homeCommandHandler" path="/menucommand/showMenuConfig.page"> <regex><![CDATA[root]]></regex> <description>输入root返回菜单的最上级</description> </system-command> <system-command name="up" title="回到上一级" bean-name="backCommandHandler" path="/menucommand/showMenuConfig.page"> <regex><![CDATA[up]]></regex> <description>输入up,回到当前菜单的上一级</description> </system-command> <system-command name="list" title="列出子列表" bean-name="queryCommandHandler" path="/menucommand/query.page"> <regex><![CDATA[list|list\s+[\u4e00-\u9fa5_a-zA-Z0-9]+$]]></regex> <description>列出系统命令和当前菜单的列表,支持“list 关键字”的方式</description> </system-command> <system-command name="help" title="显示详情" bean-name="helpCommandHandler" path="/menucommand/help.page"> <regex><![CDATA[help|help\s+[\u4e00-\u9fa5_a-zA-Z0-9]+$]]></regex> <description>列出命令详情</description> </system-command> <system-command name="exit" title="退出菜单" bean-name="exitCommandHandler" path="/menucommand/exit.page"> <regex><![CDATA[exit]]></regex> <description>输入exit退出菜单</description> </system-command> </menu-configs>
menu-configs是总结点,它包含两类子节点:菜单节点menu-config和系统命令节点system-command。菜单节点支持树结构,也就是能够自包含,菜单节点能够包含菜单命令节点menu-command,仅在当前菜单有效。系统命令不支持嵌套,并且只在menu-configs下面,它是全局有效的。最终效果以下:
好了,关于Tiny微信框架大体介绍如此,若是有开发人员对本框架感兴趣,想作一些扩展开发,能够联系本人。
若是您对个人博客感兴趣,请点击左上角的关注,以便及时收到个人相关通知。