微信对国人而言,想必大名鼎鼎,活跃用户数已经突破6.5亿,足以说明这款应用的生命力。但是使用人数众多,不代表微信的API设计优异,有过微信公众号开发经验的人,想必复杂的报文,众多的服务API以及各种公众号资源与权限设置搞得头痛。其实Tiny框架设计理念之一就是简化开发人员的工作,设计Tiny微信框架可以一定程度上减少一般开发人员的难度。
前段时间本人写过一篇博文《微信框架的几个层次》,提到了十个层级,介绍之前先说一下微信的消息通讯机制,主要分为被动推送和主动请求两种模式:
一、被动推送模式。此时微信服务器是通讯发起方,用户服务器是通讯接收方。
这种模式下推送报文分两类:消息和事件。如用户在微信客户端发送的文本消息、图片消息在通讯层面上就是消息报文;而事件报文一般用于处理异步响应,比如用户点击微信菜单触发菜单事件等。
二、主动请求模式。此时用户服务器是通讯发起方,而微信服务器则是通讯接收方。
主动请求场景很多,微信开发平台提供的大部分API都是这种模式,如自定义菜单、素材管理、支付等。而微信服务器与微信客户端之间的数据更新有以下两种方式:
Tiny微信框架的核心接口如图所示:
以上接口涵盖了微信通讯、报文转换、消息接收和发送、上下文会话、业务处理等诸多方面,接口说明如下:
接口 | 接口说明 |
---|---|
WeiXinConnector | 微信连接管理,管理接收消息和请求消息,同时保持微信的通讯信息(验证令牌和JS访问票据等) |
WeiXinContext | 微信上下文环境,支持保存微信的用户会话,也可以记录各个业务处理器的操作结果。 |
WeiXinConvert | 微信消息/结果转换统一接口,支持优先级排序 |
WeiXinHandler | 微信业务处理器,支持按优先级排序。按类型可以分为发送和接收处理器。开发人员需要扩展该接口实现业务逻辑。 |
WeiXinManager | 微信配置管理器,负责加载微信API接口相关参数,和渲染微信URL。 |
WeiXinReceiver | 微信接收消息器,负责接收微信服务器推送过来的消息和事件,WeiXinConnector委托其接收消息。 |
WeiXinSender | 微信发送消息器,负责发送消息和上传文件到微信服务器,并处理响应,WeiXinConnector委托其发送消息。 |
WeiXinSession | 微信用户会话,目前以微信的openId做主键。 |
WeiXinSessionManager | 微信会话管理器,负责新增、修改和清理微信用户会话。 |
微信的服务主要是基于HTTP协议,安全通过访问令牌(access_token)保证;少数业务场景使用HTTPS加密协议,甚至涉及安全证书,例如微信商户的支付接口。
Tiny微信框架的通讯处理由WeiXinConnector总调度,接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | 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); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | 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); } |
本人一直对微信的报文设计颇有微词,从整体上看微信报文缺乏统一规范,XML、JSON格式混用,字段命名也不规范。Tiny微信提供WeiXinConvert接口负责报文与对象之间的转换,目前XML报文通过Xsteam转换,JSON报文通过fastjson转换。接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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都可以处理一类消息,接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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,用来处理微信客户端的图片类消息,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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接口定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 | 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为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | <!-- 菜单命令节点支持多个菜单配置节点和系统命令节点 --> < 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微信框架大致介绍如此,如果有开发人员对本框架感兴趣,想做一些扩展开发,可以联系本人。
如果您对我的博客感兴趣,请点击左上角的关注,以便及时收到我的相关通知。
联系客服