微信對國人而言,想必大名鼎鼎,活躍用戶數已經突破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微信框架大體介紹如此,若是有開發人員對本框架感興趣,想作一些擴展開發,能夠聯繫本人。
若是您對個人博客感興趣,請點擊左上角的關注,以便及時收到個人相關通知。