Tiny微信框架是怎样设计的?

Easter79
• 阅读 656

     微信对国人而言,想必大名鼎鼎,活跃用户数已经突破6.5亿,足以说明这款应用的生命力。但是使用人数众多,不代表微信的API设计优异,有过微信公众号开发经验的人,想必复杂的报文,众多的服务API以及各种公众号资源与权限设置搞得头痛。其实Tiny框架设计理念之一就是简化开发人员的工作,设计Tiny微信框架可以一定程度上减少一般开发人员的难度。

     前段时间本人写过一篇博文《微信框架的几个层次》,提到了十个层级,介绍之前先说一下微信的消息通讯机制,主要分为被动推送和主动请求两种模式:

 一、被动推送模式。此时微信服务器是通讯发起方,用户服务器是通讯接收方。

Tiny微信框架是怎样设计的?

这种模式下推送报文分两类:消息和事件。如用户在微信客户端发送的文本消息、图片消息在通讯层面上就是消息报文;而事件报文一般用于处理异步响应,比如用户点击微信菜单触发菜单事件等。

     二、主动请求模式。此时用户服务器是通讯发起方,而微信服务器则是通讯接收方。

Tiny微信框架是怎样设计的?

主动请求场景很多,微信开发平台提供的大部分API都是这种模式,如自定义菜单、素材管理、支付等。而微信服务器与微信客户端之间的数据更新有以下两种方式:

  1. 服务器主动推送信息。如微信的群发消息接口,在用户服务器触发群发消息后,由微信服务器往目标客户端主动推送消息。
  2. 客户端拉取信息。如自定义菜单的管理接口,在用户服务器修改了自定义菜单的内容后,微信服务器并不会主动推送内容,而是由微信客户端发现缓存过期后,自行向微信服务器请求更新。

     Tiny微信框架的核心接口如图所示:

Tiny微信框架是怎样设计的?

 以上接口涵盖了微信通讯、报文转换、消息接收和发送、上下文会话、业务处理等诸多方面,接口说明如下:

接口

接口说明

WeiXinConnector

微信连接管理,管理接收消息和请求消息,同时保持微信的通讯信息(验证令牌和JS访问票据等)

WeiXinContext

微信上下文环境,支持保存微信的用户会话,也可以记录各个业务处理器的操作结果。

WeiXinConvert

微信消息/结果转换统一接口,支持优先级排序

WeiXinHandler

微信业务处理器,支持按优先级排序。按类型可以分为发送和接收处理器。开发人员需要扩展该接口实现业务逻辑。

WeiXinManager

微信配置管理器,负责加载微信API接口相关参数,和渲染微信URL。

WeiXinReceiver

微信接收消息器,负责接收微信服务器推送过来的消息和事件,WeiXinConnector委托其接收消息。

WeiXinSender

微信发送消息器,负责发送消息和上传文件到微信服务器,并处理响应,WeiXinConnector委托其发送消息。

WeiXinSession

微信用户会话,目前以微信的openId做主键。

WeiXinSessionManager

微信会话管理器,负责新增、修改和清理微信用户会话。

第一层次:通信处理

微信的服务主要是基于HTTP协议,安全通过访问令牌(access_token)保证;少数业务场景使用HTTPS加密协议,甚至涉及安全证书,例如微信商户的支付接口。

Tiny微信框架的通讯处理由WeiXinConnector总调度,接口定义如下:

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);
    
}

具体到底层的HTTP和HTTPS协议通讯,org.tinygroup.weixinhttp工程提供了业务接口WeiXinHttpConnector实现具体的协议通讯。

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);
    
}

对开发人员而言,无需思考如何构建复杂的报文,只需依赖对应微信业务模块的子工程,构建不同的消息对象,而底层的报文解析与转换甚至通讯都由tiny框架处理。

第三层次:报文模拟发送

微信发送报文调试最麻烦的地方就是访问令牌(access_token),这个是根据用户应用动态生成的,而且只保持两个小时有效。Tiny微信框架提供了模拟测试页面,只需要bean配置页面设置相关appId和APP秘钥等参数,开发人员在页面就无需手动输入访问令牌。测试页面如下:

Tiny微信框架是怎样设计的?

第四层次:报文模拟接收

接收报文通常是用来模拟手机端的发送消息,特别是一些复杂交互场景:如命令行菜单,如果每次都通过手机端调试。效率非常低。而通过本测试页面,直接输入模拟的手机报文直接就可以得到报文结果,准确并且快速。模拟页面如图:

Tiny微信框架是怎样设计的?

第五层次:报文处理框架

前面在介绍微信核心接口时提到过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的作用是接收微信客户端发送的图片类消息,并返回图片地址给用户,效果如下:

Tiny微信框架是怎样设计的?

第六层次:上下文保持

微信是有上下文概念的,比如微信应用的小游戏:猜数字。用户输入一个数字,而服务器告诉用户比目标值偏大还是偏小,直到用户猜中为止。游戏很简单,但是这就涉及到上下文会话,Tiny微信框架提供WeiXinSession接口作为上下文统一接口,而WeiXinSessionManager作为上下文的管理接口存在。

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();
}

WeiXinSessionManager管理接口主要提供创建、删除、查询上下文会话的操作API,接口定义如下:

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();
}

当然Tiny微信框架提供了上下文相关接口,不代表每一类消息强制进行会话管理,比如简单文本消息,微信客户端的位置消息,完全可以请求-响应这种模式进行处理。

第七层次:处理的水平扩展能力

前文讲过Tiny微信框架的具体业务是由WeiXinHandler接口完成的,而WeiXinHandler接口是不依赖WeiXinConnector等委托对象,因此通过扩展WeiXinHandler接口完全可以实现处理能力的水平扩展,比如Tiny框架本身有服务中心,支持分布式服务,那么我们可以在WeiXinHandler接口包装服务中心,从而实现分布式服务。

第八层次:命令处理框架

实际上一个微信公众号,许多的时候都是通过使用者用文字(语音识别也归到用文字)的方式与平台进行交互,这个时候,其实就是一个命令行的处理。Tiny微信框架通过org.tinygroup.menucommand实现相关需求,开发人员只需要配置XML就可完成复杂的命令行处理。目前支持两种模式:

  1. 简单模式。用户不需要动态数据,仅需要配置即可。类似电话黄页,支持逐级递归,针对这种模式,Tiny支持配置方式,用户无需编码。
  2. 动态模式。用户除了静态数据,还涉及动态交互,无法通过配置解决。Tiny框架提供了MenuCommandHandler接口,然后在配置中指定具体类或bean,框架就能解决。

配置文件是以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微信框架是怎样设计的?

第九层次:模板语言的引入

我们都知道,在做业务开发时,肯定都不希望把文本信息都放在程序代码中,这个时候就可以引入模板语言来把数据和展示来进行一个分割,做业务的只管做业务,做显示的只管做显示,井水河水两不犯。比如命令菜单,无论是显示信息还是处理结果都是配置在XML或者模板文件里,而非硬编码在java类。

第十层次:模块化

这也是Tiny微信框架的特色之一。其实在本人设计微信架构之前,也阅读过不少微信框架的源代码,它们绝大多数都是定义一个大接口,跟微信服务器API基本上是一一对应的关系,这样开发人员在使用时要么引入所有资源,要么不用。而Tiny微信框架是根据微信公众号API分类,每一类API接口实现一类工程,这样用户在开发时用多少资源就引入多少工程,比如我的公众号应用是视频类,可能用到素材管理接口和消息类接口,但是和微信商户无关,那开发人员在创建工程时只需要引入前两者即可。

好了,关于Tiny微信框架大致介绍如此,如果有开发人员对本框架感兴趣,想做一些扩展开发,可以联系本人。

如果您对我的博客感兴趣,请点击左上角的关注,以便及时收到我的相关通知。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
梦
3年前
微信小程序new Date()转换时间异常问题
微信小程序苹果手机页面上显示时间异常,安卓机正常问题image(https://imghelloworld.osscnbeijing.aliyuncs.com/imgs/b691e1230e2f15efbd81fe11ef734d4f.png)错误代码vardate'2021030617:00:00'vardateT
Easter79 Easter79
3年前
Taro小程序自定义顶部导航栏
微信自带的顶部导航栏是无法支持自定义icon和增加元素的,在开发小程序的时候自带的根本满足不了需求,分享一个封装好的组件,支持自定义icon、扩展dom,适配安卓、ios、h5,全面屏。我用的是京东的Taro多端编译框架写的小程序,原生的也可以适用,用到的微信/taro的api做调整就行,实现效果如下。!在这里插入图片描述(https://i
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这
Easter79
Easter79
Lv1
今生可爱与温柔,每一样都不能少。
文章
2.8k
粉丝
5
获赞
1.2k