源码:https://git.oschina.net/wr_qx/tiny
目录[-]
微信对国人而言,想必大名鼎鼎,活跃用户数已经突破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[输
|