一、布景

最近发现为了发送企业微信帮手音讯,会在代码里边设置许多文本,并且需要拼凑音讯内容,代码混乱且对发送方不友好。考虑把企业微信帮手集成到音讯中心,在这个过程中梳理了音讯中心的全体架构,对部分代码做了优化,并在音讯中心原有的简略文本替换根底上支撑了脚本引擎。

二、全体架构

消息中心设计和模板支持

从触发到发送,经过的过程是比较多的。首要过程是:

  • 【音讯触发】触发音讯发送到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的思路验证模型。