一、背景
最近发现为了发送企业微信助手消息,会在代码里面设置很多文本,并且需要拼凑消息内容,代码混乱且对发送方不友好。考虑把企业微信助手集成到消息中心,在这个过程中梳理了消息中心的整体架构,对部分代码做了优化,并在消息中心原有的简单文本替换基础上支持了脚本引擎。
二、整体架构
从触发到发送,经过的步骤是比较多的。主要步骤是:
- 【消息触发】触发消息发送到MQ,消息体主要组成有:{接收者,场景号,消息参数}
- 【消息过滤分发】消费消息并广播到不同的消息处理者,处理者对消息体拦截、校验,查询可用场景和模板。这里用路由的角度看,消息类型和场景其实可以理解为路由选择规则。
- 【消息渲染】消息模板+变量填充,这里的模板引擎只有占位符替换
${xxx},数据来自消息参数。 - 【消息发送】发送之前记录日志,发送调用下游,比如短信运营商,部分逻辑做了重试和批量支持,发送之后更新日志状态。
看了之前的代码,在感慨同事搭建这套架构做出的努力时候,部分代码我是不忍直视,闻到了坏代码的味道,所以先用设计模式做了部分整改,主要是图中红色部分,优化完之后,拓展就很简单了。有以下几个思考点:
- 相同的逻辑保持在模板方法里面,之前的同事已经用了模板类了,但是部分逻辑还是太长了,存在超过一屏的方法!所以再加一层模板,原来的模板提供基础方法,新的模板子类做流程控制,把部分依赖消息类型的操作,放在下游消息监听者实现。
- 方法过长是做了太多的事情,校验、接收者过滤、黑名单过滤等准备工作,更适合用责任链模式。而且这里其实有点像路由转发,还存在一些校验的逻辑,用类似Filter的方式来更好。
- 部分逻辑严格依赖于消息类型的,之前写了很多
if-else,增加类型枚举需要增加分支或者修改条件,决定改成策略模式。
三、重构原则
以前一直对设计模式没多少感觉,这次看了大量代码,结合之前的经验,比较顺利地优化了部分代码。重构过程中把《代码整洁之道》看了一遍,有了几点重构的思考:
- 代码过长,就要考虑职责问题了,尽量保持单一责任原则,可以使用责任链将准备工作分段。
- 模板化遵循开放闭合原则,可以暴露部分修改点,通过抽象保留稳定的代码。修改点可以是方法级别,提供抽象方法要求下游强制实现,可以是策略--使用类型枚举调用对应逻辑。
- 修改代码过程中,小改、小测试、小提交,最后合起来提交,重试单元测试!改好之后及时测试,做方法级别的测试和集成测试。
- Model-check 最近在看南京大学jyy(蒋炎岩)老师的操作系统,里面说到的一种验证模型的方法。
四、模板引擎强化
这次强化起源于部分消息是接收一个数组的,看了之前的文本参数替换过程,只是简单地把占位符${xxx}替换成参数里的变量。我就想要怎么支持变化的参数,让发送者拼接文本是不合适的。假设要生成的文本(markdown)如下:
这里是网络的世界
[百度](https://www.baidu.com)
[腾讯](https://www.qq.com)
[阿里](https://www.alibaba.com)
时间会消逝
上面是有百度、腾讯、阿里三个网站,假如这个网站列表是消息发送者指定的一个不定长的数据。输入的消息体:
{接收者,场景号,消息参数}
{
"接收者": "LiXiaoXiao",
"场景号": "1",
"消息参数":{
"name": "李小小",
"items": [
{
"name": "百度",
"url": "https://www.baidu.com"
},
{
"name": "腾讯",
"url": "https://www.qq.com"
},{
"name": "阿里",
"url": "https://www.alibaba.com"
}
]
}
}
方案一:参考mybatis<foreach>
- 保持文本特性,使用正则处理,替换时候通过反射获取对象,这个在后来改成了转成map获取对象参数。脚本格式参考mybatis的标记设计。
这里是编程的世界
<foreach>
[${items.name}}](${items.url})
</foreach>
时间会消逝
方案二:支持js
- 考虑到支持循环计算,是一种逻辑运算,联想到脚本引擎。问了老大说可以使用js脚本,赋予脚本执行计算逻辑能力。没想到java原生集成了js的引擎!但是语法支持是有限的,ESLint6不全支持。比较好玩的地方是java变量传给了js,js执行的变量可以被java读取。
<script>
var result = '这里是编程的世界\n';
for (var i = 0; i < items.length; i++) {
var item = items[i];
var each = '[' + item.name + '](' + item.url + ')\n';
result += each;
}
result += '时间会消逝'
</script>
public String doExec(String js, Map<String, Object> paramObj) {
ScriptEngine jsEngine = JS_ENGINE_MANAGER.getEngineByName(JS_ENGINE_NAME);
ScriptContext scriptContext = new SimpleScriptContext();
if (!CollectionUtils.isEmpty(paramObj.keySet())) {
paramObj.forEach((key, value) -> {
scriptContext.setAttribute(key, value, ScriptContext.ENGINE_SCOPE);
});
}
try {
jsEngine.setContext(scriptContext);
return jsEngine.eval(js).toString();
} catch (Exception e) {
log.info("执行js失败,js:", e);
return js;
}
}
方案三:支持groovy
- 集成js脚本之后,想到可以用groovy。一样的思路,传入java消息变量,生成文本返回。
<groovy>
result = '这里是编程的世界\n';
end = '时间会消逝';
for (item in items) {
result += String.format("[%s](%s)\n", item.name, item.url);
}
return result + end
</groovy>
public String doExec(String groovy, Map<String, Object> paramObj) {
GroovyShell groovyShell = new GroovyShell();
paramObj.entrySet().forEach(e -> groovyShell.setVariable(e.getKey(), e.getValue()));
try {
Object result = groovyShell.evaluate(groovy);
return result.toString();
} catch (Exception e) {
log.info("执行groovy失败,groovy:", e);
return groovy;
}
}
五、代码参考
基于上面的消息中心架构图,加上测试不同脚本引擎能力,用model-check的方式构建了一个简化版的消息中心。在复现过程中,发现了一些有争议的点,后续继续优化。
六、总结
- 多思考和多看书:之前看设计模式只是略懂,现在看代码会想着改成某个模式会如何,最重要是执行。
- 拥抱架构思维: 我们常常只看到一个角,没看到完整的冰山,当重塑事物的本质,可以重新定义的统一业务模型。
- 多画图:画图可以帮助自己和他人理解现状和想法,展示变化点。
- 测试很重要:做好单元测试、集成测试,用model-check的思路验证模型。