调整jfinal weixin架构的记录

JFinal weixin 是一个微信公众平台极速SDK,基于 JFinal weixin 开发不仅可以立即拥有简单易用的API,而且将立即拥有JFinal所有的极速化支持,让开发更加轻松自如。针对jfinal weixin未考虑到将点击菜单输出的回复(例如 1、top10 2、top20)的作为二级菜单进行处理,基于二级菜单的模式进行一次较粗的更改。为了降低使用该框架的开发的门槛、去除以xml形式的菜单配置带来的频繁IO读取导致的性能瓶颈,进行框架升级工作输出itc-weixin 1.0版本。

1、版本说明

  • jfinal-weixin 修改0.1版 基于的版本情况: jfinal 1.9,jfinal-weixin 1.2;
  • itc-weixin 1.0 基于的版本情况: jfianl 2.2, jfinal-weixint 1.6。

2、架构说明

  • 本文对 jfinal-weixin 中的继承Controller的处理方法、AOP、配置conf(包含路由、数据库、Cache等配置)等暂不作讨论;
  • 本文主要讨论 jfinal-weixin 中的关于微信端交互的动作(菜单时间,文本消息等)的架构。在代码层面为继承MsgController的处理方法的讨论与重构。

3、jfinal-weixin 架构


jfinal weixin 由底层 jfinal 提供 Controller、model、JFinalConfig、view(Jsp、FreeMark、Velocity)、AOP 的支持,融合了对微信SDK(微信消息加解密、消息格式、API等)的支持。其中继承Controller的MsgController提供了微信消息的唯一入口index()方法(即在开发者中心输入的 URL 必须要指向此 action)。

(1)jfinal-weixin 1.2 架构

jfinal-weixin 1.2 版本的升级将WeixinController 更名为 MsgController,WeixinInterceptor 更名为 MsgInterceptor,添加 ApiConfigKit ,后续版本的升级都是在1.2版本的基础上对微信SDK部分的升级。本文将以1.2版本作为基础版本进行讨论。如下图所示为 jfinal-weixin 1.2 的架构图。
jfinal-weixin 1.2架构
index()方法主要是接收消息,对消息进行解密,根据消息类型分发到相应的函数进行处理。

  • 1、消息解析getInMsg();
  • 2、根据消息类型进行分发处理;
  • 3、对接收到微信服务器的 InMsg 消息后后响应 OutMsg 消息的render()。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public abstract class MsgController extends Controller {
/* weixin 调用唯一入口 */
public void index() {
/* 解析消息 */
InMsg msg = getInMsg();

/* 并根据消息类型分发到相应的处理方法 */
if (msg instanceof InTextMsg)
processInTextMsg((InTextMsg) msg);
else if (msg instanceof InMenuEvent)
processInMenuEvent((InMenuEvent) msg);
else
log.error();
}

public void render(OutMsg outMsg){
/* 在接收到微信服务器的 InMsg 消息后后响应 OutMsg 消息 */
}
}
index()函数具体消息处理过程的解析:
  • getInMsg() 方法中通过 getRequest() 方法获取消息InMsg。
  • 使用jfinal提供的 HttpKit.readData() 进行消息的读取,然后使用 MsgEncryptKit.decrypt() 进行消息的解密。
  • 将解密后的消息通过 instanceof 来判断消息的类型,根据消息类型进行消息分发处理,比如 processInTextMsg、processInMenuEvent 等。
  • 分发消息处理函数中通过调用 render() 方法来输出OutMsg;在 render() 方法中通过 OutMsgXmlBuilder.build() 来将OutMsg中参数与输出消息类型的 TEMPLATE(模板) 的相应字段进行匹配组装成String类型的消息outMsgXml。
  • 使用 MsgEncryptKit.encrypt() 对outMsgXml进行加密,得到加密后的outMsgXml。
  • 通过 renderText(outMsgXml, “text/xml”) 方法调用 TextRender 实现的 render() 来进行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
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
public InMsg getInMsg() {
String inMsgXml = HttpKit.readData(getRequest()); //读取消息

inMsgXml = MsgEncryptKit.decrypt(inMsgXml, getPara("timestamp"), getPara("nonce"),
getPara("msg_signature")); //对消息进行解密

String inMsg = InMsgParser.parse(inMsgXml); //从 xml 中解析出各类消息与事件

/* 相应的出来函数 */
processInTextMsg((InTextMsg) msg){
//处理文本的

............ // 具体的处理方法

render(OutMsg);
}
}

/* 输出的方法 */
public void render(OutMsg outMsg) {
/* 将OutMsg中参数与输出消息类型的 TEMPLATE(模板) 的相应字段进行匹配。 */
/* 组装成String类型的消息outMsgXml */
String outMsgXml = OutMsgXmlBuilder.build(outMsg);

outMsgXml = MsgEncryptKit.encrypt(outMsgXml, getPara("timestamp"), getPara("nonce")); //进行加密

/* 调用Controller中的 render = renderFactory.getTextRender(text, contentType) 方法;*/
/* 生成Render类型的TextRender(继承Render)的render的对象;*/
/* 生成的render对象调用了TextRender覆写Render的 render() 方法进行Response消息。*/
renderText(outMsgXml, "text/xml");
}

public static String readData(HttpServletRequest request) {
/* 这里只给出了核心代码,去除了异常处理*/
BufferedReader br = null;
StringBuilder result = new StringBuilder();
br = request.getReader(); // 读取post过来的参数
for (String line=null; (line=br.readLine())!=null;) {
result.append(line).append("\n"); //组装消息为字符串
}
return result.toString();
}


public abstract class Render {
/* Render to client */
public abstract void render();
}

public class TextRender extends Render {
public void render() { // TextRender 覆盖Render中的 render() 方法
PrintWriter writer = null;
try {
/* HTTP/1.0 caches might not implement Cache-Control */
/* and might only implement Pragma: no-cache */
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
if (contentType == null) {
response.setContentType(DEFAULT_CONTENT_TYPE);
}
else {
response.setContentType(contentType);
response.setCharacterEncoding(getEncoding());
}
writer = response.getWriter();
writer.write(text);
writer.flush();
} catch (IOException e) {
throw new RenderException(e);
}
finally {
if (writer != null)
writer.close();
}
}
}

经过上面的消息的处理流程,就可以完成点击菜单、回复关键字获取信息。

如何创建一个消息处理?

创建一个微信消息处理就需要创建一个继承 MsgController 消息处理类。创建的微信消息处理类通过覆写 MsgController中的processInTextMsg、processInMenuEvent 等方法来实现自定义的消息处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class TestMsgController extends MsgController {
protected void processInTextMsg(InTextMsg inTextMsg) {
String msgContent = "欢迎使用智慧运维平台,请选择菜单来获取运维信息。欢迎您向我们反馈平台使用中存在的问题;感谢您对“互联网+运维”工作的大力支持!";
OutTextMsg outMsg = new OutTextMsg(inTextMsg);
outMsg.setContent(msgContent);
render(outMsg);
}

protected void processInMenuEvent(InMenuEvent inMenuEvent) {
String msgContent = "欢迎使用智慧运维平台,请选择菜单来获取运维信息。欢迎您向我们反馈平台使用中存在的问题;感谢您对“互联网+运维”工作的大力支持!";
OutTextMsg outMsg = new OutTextMsg(inMenuEvent);
outMsg.setContent(msgContent);
render(outMsg);
}
}

Tips:这里需要注意回复文本的关键字、菜单EventKey必须唯一绑定在一个 TestMsgController 的方法上。

微信SDK的介绍

微信SDK中包括API接口、In(接收消息)、Out(输出消息)、InMsgParaser、OutMsgXmlBuilder主要的方法。详细的结构如下图所示(注:API接口属于微信提供调用功能,不属于实现的消息收发范畴,将在后面单独介绍。):

jfinal-weixin SDK
  • In(接收消息):对接收消息类型进行抽象,将 toUserName、fromUserName、createTime、msgType、agentId(存在于企业号中,为企业号的应用的id) 封装为 abstract InMsg 抽象类。继承 InMsg 的有Event事件、speech_recognition语音识别(在企业号中无此消息类型)、InTextMsg接收文本消息、InLocationMsg接收地理消息、InVoiceMsg接收语音消息、InVideoMsg接收视频消息、InImageMsg接收图片消息、InLinkMsg接收链接消息。点击Menu菜单事件为Event事件最主要处理的消息类型;在Menu菜单事件中“点击菜单跳转链接时的事件推送”(event = “view”)一般都进行忽略;在 MsgController 不进行任何处理。 speech_recognition语音识别(在企业号中无此消息类型)主要与与 InVoiceMsg 唯一的不同是多了一个 Recognition 标记。

  • Out(输出消息):对回复消息类型进行抽象,将 toUserName、fromUserName、createTime、msgType 封装为 abstract OutMsg 的抽象类,继承 OutMsg 的有OutNewsMsg回复图文消息、OutTextMsg回复文本消息、OutImageMsg回复图片消息、OutMusicMsg回复音乐消息(在企业号中无此消息类型)、OutVideoMsg回复视频消息、OutVoiceMsg回复语音消息 。

  • InMsgParaser:从 xml 中解析出各类消息与事件,其中最主要的方法为 doParse() 。在 doParse() 方法中判断 msgType 的类型来组装成相应的InMsg类型的消息对象;当 msgType 为 event 时,还需要根据event类型来组装成Event事件类型的消息对象。

  • OutMsgXmlBuilder:利用 FreeMarker 动态生成 OutMsg xml 内容,包括 build()方法、initFreeMarkerConfiguration()方法、initStringTemplateLoader()方法。在该类中,首先调用 initFreeMarkerConfiguration() 对FreeMarker进行初始化配置。在进行初始化配置的过程中调用 initStringTemplateLoader() 方法来动态加载OutMsg消息类型的TEMPLATE模板。build()方法将outMsg与TEMPLATE模板对应生成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
public class OutMsgXmlBuilder {
private static String encoding = "utf-8";
private static Configuration config = initFreeMarkerConfiguration(); //初始化配置

private static Configuration initFreeMarkerConfiguration() {
Configuration config = new Configuration();
StringTemplateLoader stringTemplateLoader = new StringTemplateLoader();
initStringTemplateLoader(stringTemplateLoader); // 加载TEMPLATE模板
config.setTemplateLoader(stringTemplateLoader); // 将TEMPLATE模板配置给config

........省略其他一些配置信息........

return config;
}

private static void initStringTemplateLoader(StringTemplateLoader loader) {
// text 文本消息
loader.putTemplate(OutTextMsg.class.getSimpleName(), OutTextMsg.TEMPLATE);
// news 图文消息
loader.putTemplate(OutNewsMsg.class.getSimpleName(), OutNewsMsg.TEMPLATE);
// image 图片消息
loader.putTemplate(OutImageMsg.class.getSimpleName(), OutImageMsg.TEMPLATE);
//voice 语音消息
loader.putTemplate(OutVoiceMsg.class.getSimpleName(), OutVoiceMsg.TEMPLATE);
// video 视频消息
loader.putTemplate(OutVideoMsg.class.getSimpleName(), OutVideoMsg.TEMPLATE);
// music 音乐消息
loader.putTemplate(OutMusicMsg.class.getSimpleName(), OutMusicMsg.TEMPLATE);
}

public static String build(OutMsg outMsg) {
if (outMsg == null)
throw new IllegalArgumentException("参数 OutMsg 不能为 null");

Map root = new HashMap();
// 供 OutMsg 里的 TEMPLATE 使用
root.put("__msg", outMsg);
try {
Template template = config.getTemplate(outMsg.getClass().getSimpleName(), encoding);
StringWriter sw = new StringWriter();
template.process(root, sw);
return sw.toString();
} catch (freemarker.core.InvalidReferenceException e) {
throw new RuntimeException("可能是 " + outMsg.getClass().getSimpleName()+ " 对象中的某些属性未赋值,请仔细检查", e);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
微信API

微信公众平台、企业号平台向开发者开放了功能调用的API接口。公众开发平台先于企业号开发平台推出,企业号平台目前提供的API接口不如公众平台丰富,但随着企业号平台的不断完善,相应的API接口也会在后续不断进行开放。

jfinal-weixin 提供了API调用的类方法,通过 HttpKit.get、HttpKit.post 来进行接口的调用。主要的API接口有:获取access_token、查询创建菜单、用户管理等。下面代码示例为企业号的获取access_token。

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
public class AccessTokenApi {
private static String url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken";
private static AccessToken accessToken;

public static AccessToken getAccessToken() {
if (accessToken != null && accessToken.isAvailable())
return accessToken;
refreshAccessToken();
return accessToken;
}

public static void refreshAccessToken() { //刷新重新获取access_token
accessToken = requestAccessToken();
}

/* synchronized关键字保证每次只有一个线程可以调用获取access_token */
private static synchronized AccessToken requestAccessToken() {
AccessToken result = null;
ApiConfig ac = ApiConfigKit.getApiConfig();
for (int i=0; i<3; i++) {
String corpId = ac.getCorpId();
String corpSecret = ac.getCorpSecret();
Map<String, String> queryParas = ParaMap.create("corpid", corpId).put("corpsecret", corpSecret).getData();
String json = HttpKit.get(url, queryParas);
result = new AccessToken(json);

if (result.isAvailable())
break;
}
return result;
}
}

关于微信SDK的参考资料:
微信公众号开发者文档:http://mp.weixin.qq.com/wiki/home/
微信企业号开发者文档:http://qydev.weixin.qq.com/wiki/index.php?title=%E9%A6%96%E9%A1%B5

(2)jfinal-weixin 1.6 架构

jfinal-weixin 1.6 版本基于 jfinal 2.2 版本,对于final框架的升级升级,本文不做讨论,有兴趣可以参考 jfinal 2.2 的手册来学习查看。
jfinal-weixin 1.6 架构如下图所示。
jfinal-weixin 1.6架构
jfinal-weixin 1.6 版本相对于 1.2版本 主要有3个方面的不同:

  • 1、增加了 MsgControllerAdapter 抽象类;
    MsgControllerAdapter 对 MsgController 部分方法提供了默认实现,以便开发者不去关注 MsgController 中不需要处理的抽象方法,节省代码量。微信消息处理函数(指架构图中的子Controller,准确的说是子MsgController)需要继承MsgControllerAdapter来获得消息处理的入口。
1
2
3
4
5
6
7
8
9
public abstract class MsgControllerAdapter extends MsgController {
protected abstract void processInTextMsg(InTextMsg inTextMsg);
protected abstract void processInMenuEvent(InMenuEvent inMenuEvent);
protected void processInLocationMsg(InLocationMsg inLocationMsg) {
//这里提供默认的实现方法
}

..............省略其他消息类型的处理的方法................
}
注:MsgControllerAdapter 在 jfinal-weixin 1.2 中已经引此类。由于后续版本的都是在1.2版本在核心类的命名保持一致,在基于1.2版本的开发过程中微信消息处理直接继承于MsgController,为了说明 MsgControllerAdapter 的作用,将此类作为1.6版本的特性来进行讲解。
  • 2、随着微信API接口的开发,丰富了微信SDK部分的内容,增加了消息类型和API接口;
    对接收消息In中的event事件消息进行了再次抽象,将event类型字段单独抽出来一个继承InMsg的EventInMsg抽象类。其他的所有的event事件消息实体类都继承该抽象类。同时也新增了一些新event事件的实体类,这里不再一一列出。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class EventInMsg extends InMsg
{

protected String event;

public EventInMsg(String toUserName, String fromUserName, Integer createTime, String msgType, String event)
{

super(toUserName, fromUserName, createTime, msgType);
this.event = event;
}

public String getEvent()
{

return event;
}

public void setEvent(String event)
{

this.event = event;
}
}
  • 3、新增了utils工具包,提供了Base64、Http请求、异常重试等工具。
    utils为jfinal-weixin 1.6 引入的工具包帮助类。在utils中主要有异常重试RetryUtils类、IOUtils类、Json转换JsonUtils、类工具ClassUtils、Http请求HttpUtils、HttpKitExt添加功能。对于这些工具的详细内容,可以查看源码进行学习。下面为HttpUtils工具类部分代码,使用 delegate 代理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    // http请求工具代理对象
private static final HttpDelegate delegate;

static {
HttpDelegate delegateToUse = null;
// okhttp3.OkHttpClient?
if (ClassUtils.isPresent("okhttp3.OkHttpClient", HttpUtils.class.getClassLoader())) {
delegateToUse = new OkHttp3Delegate();
}
// com.squareup.okhttp.OkHttpClient?
else if (ClassUtils.isPresent("com.squareup.okhttp.OkHttpClient", HttpUtils.class.getClassLoader())) {
delegateToUse = new OkHttpDelegate();
}
// com.jfinal.kit.HttpKit
else if (ClassUtils.isPresent("com.jfinal.kit.HttpKit", HttpUtils.class.getClassLoader())) {
delegateToUse = new HttpKitDelegate();
}
delegate = delegateToUse;
}
`

4、jfinal-weixin 1.2 修改0.1版


初期基于 jfinal-weixin 1.2 版本做开发时,微信消息处理函数直接继承MsgController。将微信的消息分为点击菜单和未点击菜单(用户第一次进入应用后发送文本消息和触发菜单的有效时间过期后的发送文本消息)的触发的微信消息处理。将未点击菜单的消息处理作为根节点,点击菜单触发的消息处理作为二级节点。消息处理每次都先进入根节点进行识别,若为根节点消息则由根节点处理方法继续处理,若为二级节点的消息则分发给相应的二级节点处理类进行处理。架构如下图所示:
jfinal-weixin 1.2 修改0.1版架构

微信菜单

MsgBaseController 为根节点,子Controller为二级节点。基于两级菜单的设置,使用一个 Menu.xml 来存储微信菜单的eventkey与处理方法MsgController的对应关系。Menu.xml 中的 ChildMenu 标签标识一个菜单,菜单的属性包括eventKey(微信菜单配置key值,唯一值)、class(对应的处理函数)、flag(预留字段,标示是否菜单可用)、content(为输出消息的内容)。
Menu.xml 文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<xml>
<MenuList>
<Menu>
<ChildMenu>
<eventKey>alarm_query</eventKey>
<class>com.nfjd.bocp.controller.AlarmMsgController</class>
<flag>0</flag>
<content>【1】请输入描述时间、系统、IP的关键字,关键字间用空格隔开;如:“今天 BOSS 2.105”。\r\n--时间选项支持:今天、昨天、本周、本月\r\n--系统选项支持:ALL、BOSS、CRM、BOMC、BASS\r\n--IP地址选项支持模糊匹配\r\n\r\n【2】输入"top"查询告警TOP排行</content>
</ChildMenu>
</Menu>
<Menu>
<ChildMenu>
<eventKey>knowledge_query</eventKey>
<class>com.nfjd.bocp.controller.ConsultController</class>
<flag>0</flag>
<content>请输入需要查询问题,支持模糊匹配和完整语句。支持语音和文本哦~</content>
</ChildMenu>
</Menu>
</MenuList>
</xml>

添加了 XmlMenuHelper 帮助类来操作 Menu.xml 文件。调用 XmlMenuHelper.getMenuContent(String eventKey) 根据菜单的键值eventKey,来获取点击菜单项事件的返回值,其中使用 IteratorMenu 来获取节点;IteratorMenu(Iterator<?> iterator, Element element, String eventKey) 递归获取制定菜单键值eventKey的节点;(注意:该方法用于获取一个一级菜单中Menu中的节点);GetXmlPath() 获取 Menu.xml 文件的路径;getDocument(String path)读取 Menu.xml 文件的内容,返回Document类型的文件;getRootElement(Document doc) 获取XML文件的根节点;getMenuContent(Document doc, String Name, String Value) 根据菜单的节点名和内容,来获取点击菜单项事件的返回值,调用 getELementbyKey ;getELementbyKey(Document doc, String Name, String Value) 根据节点名和内容进行查找节点。具体代码如下:

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
/**
*
* 该方法来获得菜单eventKey对应的返回内容;
*
* @author yingshneg create_date 2015年4月23日
*/

public class XmlMenuHelper
{

public XmlMenuHelper()
{

}

/**
* 根据菜单的键值eventKey,来获取点击菜单项事件的返回值 使用IteratorMenu来获取节点
*
* @author Eson modify_date 2015年4月24日
* @param eventKey 节点名
* @return String
*/

public String getMenuContent(String eventKey)
{

String Content = null;
/* 获取xml文件、根节点 */
Document doc = getDocument(GetXmlPath());
Element root = getRootElement(doc);
Element menu = root.element("MenuList");
Element recode = null;

/* for循环所有的一级节点,通过IteratorMenu递归遍历每个一级节点下的子节点。 */
for (Iterator<?> it = menu.elementIterator(); it.hasNext();)
{
Element item = (Element) it.next();
/* 通过节点名来递归遍历查找子节点 */
Element el = IteratorMenu(it, item, eventKey);
recode = el;
if (el != null)
{
Content = el.elementText("content");
break;
}
}
/* 节点为空 */
if (recode == null)
{
Content = "NotExsit";
}

return Content;
}

...............这里省略了其他方法,具体详见该类.................
}

采用XML文件方便数据的维护,频繁的读取文件IO操作必然造成性能瓶颈。在 itc-weixin 1.0 版本中对这部分进行优化,在项目启动时就加载菜单eventKey与MsgController的映射关系,算是以空间换时间的解决方案。

MsgController 之间request中参数传递

在两级菜单的设计中,根节点分发接收到的消息到二级节点处理输出回复的消息,回复消息进行加密后返回给微信服务器。消息加密算法中需要用到 timestamp、nonce 参数;timestamp、nonce 参数需要从 MsgBaseController 接收的post数据中获取。
MsgBaseController对象传递
如何进行MsgBaseController对象的传递?
首先我们应该都清楚 MsgBaseController、ChildMsgController 都继承 MsgController ;MsgBaseController 作为消息的入口对消息进行处理或分发分发处理。

  • (1) 在 MsgController 中增加一个成员变量 MsgBaseController ,初始值为null;
1
public MsgController baseController = null;
  • (2) 在 ChildMsgController 的构造函数中对成员变量 MsgBaseController 的对象baseController 赋值操作。
1
2
3
4
public ChildMsgController(MsgController baseController)
{

this.baseController = baseController;
}
  • (3) 在 MsgBaseController 中做分发处理的时,从 Menu.xml 的映射关系获得到处理函数的ClassName;通过ClassName利用反射创建构造函数,生成实例对象。”classObject = (MsgController) constructor.newInstance(this)” 这段代码将 this 为 MsgBaseController 的对象,newInstance(this)即调用(2)中的构造函数并创建一个实例对象。注:此处的代码在 MsgController 中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
MsgController classObject = null;
/* 通过controllerClass来判断是否为根节点的Controller; */
/* 若为MsgBaseController,则不进行反射来获取Controller对象。 */
if (controllerClass.equals("com.nfjd.bocp.controller.MsgBaseController"))
{
classObject = this;
} else
{
/* 通过反射来获取Menu.xml中ChildMenu标签下class标签下类名的实例 */
/* controllerClass为String类型的类名,例如com.nfjd.bocp.controller.AlarmMsgController */
Class<?> ctrlClass = Class.forName(controllerClass);
Constructor<?> constructor = ctrlClass.getConstructor(MsgController.class);
classObject = (MsgController) constructor.newInstance(this);
}
render的问题

如何解决ChildMsgController中执行了render()操作后未将消息推送给服务器的问题?
在分析该问题之前,先看一个 ChildMsgController 中的处理函数的代码。

1
2
3
4
5
6
7
8
public class AssessmentMsgController extends MsgController{
protected void processInMenuEvent(InMenuEvent inMenuEvent) {
String msgContent = "欢迎使用智慧运维平台,请选择菜单来获取运维信息。";
OutTextMsg outMsg = new OutTextMsg(inMenuEvent);
outMsg.setContent(msgContent);
render(outMsg);
}
}

在processInMenuEvent事件的中会进行一个render(outMsg)的操作将消息返回给微信服务器。在这个地方忽略了响应请求Response必须知道响应消息要发给谁,简单的说要先知道谁发过来的请求。render()就是一个Response消息的操作,在ChildMsgController中并不知道是谁发过来的请求,我要回复消息给谁。执行render操作也并没有像希望的那样发送消息给微信服务器。
MsgBaseController 作为消息接收的入口,通过getRequest接收到请求消息,知道了谁发过来的消息。在MsgBaseController中响应请求Response就可以知道我要消息发给谁。
解决这个问题方法就是在MsgBaseController中进行render()。

1
this.render(classObject.getRender()); //this 指代 MsgBaseController的对象baseController。

小结:以上的修改有不完美的地方。频繁的读取xml文件带来IO性能瓶颈;使用全局静态变量保存接收到的消息inMsg;记录用户点击菜单的记录存储在数据库中;需要进行二次的render;index()方法有200多行代码;这些都是可以优化的地方。

5、itc-weixin 1.0 升级说明


对jfinal-weixin 1.2 修改0.1版基础上进行应用开发经验总结,以及对jfinal平台有了更深入清晰的了解,结合存在不足,进行了第二次的架构的优化工作。
主要思路:唯一入口处理;不再对MsgController进行继承,把MsgController作为入口,将消息MsgProcess的分发处理进行剥离操作,返回OutMsg给MsgController。这样开发者只需要关注处理返回OutMsg。

itc-weixin 1.0 修改的基本思路介绍:
itc-weixin 1.0版本:修改基本思路

萌生这样的修改思路,来源于最初的0.1版本痛苦的修改经历以及从代码的逻辑清晰角度出发总结出来的经验。

  • 首先,在0.1版本中MsgBaseController为微信消息交互的入口,处理子Controller的业务逻辑需要从MsgController中跳转或分发处理到子Controller中,子Controller处理完的消息又需要回到MsgBaseController中进行render(呈现)操作。
  • 在进行0.1版本修改过程中,遇到在子Controller中进行render后,并没有得到要呈现的内容或回复的消息;经过调试发现,在子Controller里进行render后,程序回重新回到MsgBaseController中再进行一次render,因此需要将子Controller的render传递到MsgBaseController中。

    1
    2
    // classObject代表子Controller的对象
    this.render(classObject.getRender());
  • 其次,多次的代码间的跳转不利于代码的维护、增加了理解程序的难度。秉承简单清晰化的代码逻辑,减少业务开发者的难度。在1.0版本中将render操作唯一化。

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
81
82
83
84
85
86
87
88
/**
* weixin 公众号服务器调用唯一入口,即在开发者中心输入的 URL 必须要指向此 action
*
* @author esonzys modify_date 2016年4月13日
*/

@Before(MsgInterceptor.class)
public void index()
{

InMsg msg = null;
String inMsgXml = null;

MsgProcess processClass = null;
inMsgXml = ProcessMsg.getInMsgXml(inMsgXml, this);
if (ApiConfigKit.isDevMode())
{
System.out.println("接收消息:");
System.out.println(inMsgXml);
}

msg = ProcessMsg.getInMsg(msg, inMsgXml);// 对消息进行解析

if (msg == null)
{
log.info("msg为空!");
renderNull();
} else
{
// 判断是否为view类型的event事件
String msgType = msg.getMsgType();
String eventKey = null;

/**
* Event | text 分别处理
*/

if (msgType.equalsIgnoreCase("event"))
{
if (!isViewEvnet(msg))
{
eventKey = getEventKey(msg);
/**
* 处理不属于view类型(点击菜单跳转)类型的event
*/

try{
processClass = EventKeyMapping.M_EVENT_KEY_MAPPING
.getEventKeyClass(eventKey).newInstance();
}catch(Exception e){
//这里需要输出与newInstance相关的错误
e.printStackTrace();
}

}
} else if (msgType.equalsIgnoreCase("text"))
{
try
{
processClass = MsgBaseProcess.class.newInstance();
} catch (InstantiationException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e)
{
// TODO Auto-generated catch block
e.printStackTrace();
}
/* } */
}
log.info("处理函数(Process Class)的方法为:"
+ processClass.getClass().getName());

/**
* 获取输出的消息
*/

if (!processClass.equals("NotExsit"))
{
OutMsg outMsg = getOutMsg(processClass.getClass().getName(), msg, inMsgXml);
log.info("进行render!");
render(outMsg);
} else if (isViewEvnet(msg))
{
log.info("跳转链接,无须处理。");
renderNull();
} else
{
renderOutTextMsg("请检查输入的菜单键是否正确!!!", msg);
}
}
}

由上面的代码可以清楚的看到:(1)EventKeyMapping 来获取eventKey对应的processClass;(2)getOutMsg(String, InMsg, String)函数来获取processClass处理返回来的outMsg。

1
2
3
4
5
6
// 根据eventKey获取相应的业务逻辑的一个实例
processClass = EventKeyMapping.M_EVENT_KEY_MAPPING.getEventKeyClass(eventKey).newInstance();

// getOutMsg(String, InMsg, String)函数来获取processClass处理返回来的outMsg。
OutMsg outMsg = getOutMsg(processClass.getClass().getName(), msg, inMsgXml);
render(outMsg);

总结:按照设定的修改思路,对程序进行一步简单的重构,将冗长的代码进行剥离。在正式的发布版本中使用将子Controller业务处理部门抽象为一个Service层来进行处理。

itc-weixin 1.0 实现方法:

上面已经讲了,在1.0版本中引入Service层来进行一个业务逻辑处理。在Service层实现过程中引入 缺省适配器模式 以及jfinal 2.0版本中的新增的 业务层拦截器
itc-weixin 1.0版本:架构
总体架构如上图所示,下面将单独把Service层的单独拿出来进行说明。
Service层

  • 1、缺省适配器

github的下载地址:itc-weixin 项目 github 的 pull 地址: https://github.com/airman33/itc-weixin.git