公司老系统的重构计划早就有了,为了对Java硬编码的各种校验规则进行重构,特地参考了相关技术,最终选择了groovy
进行了系统的学习,并编写了一个即插即用的轻量级规则引擎。
文章目录
- 项目背景
- 技术选型
- groovy的性能
- groovy脚本执行线程安全问题
- 统一Java运行环境
- dsl风格的规则声明
- 弱类型的便利
- 校验规则的维护
- 基于事实推断的规则引擎实现
项目背景
笔者上班负责的是一个很老的某业务平台的申报系统。并发量随不高,但是申报分了很多的阶段,且每个阶段的申报项比较多,表单的字段也很多,校验规则也比较杂。项目前期有多个团队先后负责,代码风格不同,且业务规则的校验实现都是堆砌代码的方式,导致后期的代码维护比较麻烦。一个类文件通常是5千行代码以上,一个校验方法也至少几百行。
因为业务操作主要是数据的保存和申报,而校验的代码占到很大的比例,为此笔者的重构,很自然就想到把各种杂七杂八的校验代码给抽取出来,以规则脚本的形式进行更好的维护。
技术选型
抽取校验规则的技术选型,考虑了基于rete
算法的drools
、基于mvel
的easyRule
以及jvm
体系的groovy
。因为只是把系统中的校验规则抽取出来,做成脚本的形式维护,传统的规则引擎也基本用不上,因为它们更多的使用场景是基于多实体的事实对象的分析、推断,且比较重量级,且有一定的学习成本。而groovy
本身就是一门动态的脚本语言,很轻量级,可以在java
环境无缝衔接的调用,语法非常的简洁易学,它的dsl
特性还能编写出可读性更高的脚本声明形式。
groovy的性能
groovy
非常的轻量级,编译速度很快,新版本对编译的内容做了缓存,而且gc
方面也做了优化。笔者在这方面做了一些实验:通过spring boot
的定时任务每隔一秒钟用30
个线程同时跑groovy
规则脚本,并对应用的jvm
参数-XX:MetaspaceSize
和-XX:MaxMetaspaceSize
都做了限制。用groovy
老版本和新版本做了下对比,发现老版本执行很快就oom
了:
而新版本稳定发挥,执行的性能用visualVM
监控了下:
笔者还测试了下规则执行的耗时,第一次访问时需要耗时几百毫秒,后续就很快了,说明重复执行一个规则脚本,会缓存一些编译的类,提高性能:
groovy脚本执行线程安全问题
groovy
脚本在java环境中的执行有多种方式,具体可参考这篇技术博客Integrating Groovy into Java Applications。我们采用GroovyShell
的方式来执行规则脚本,因为规则是设计为dsl
声明方式,而不是类和方法的执行方式,用bingding
作为执行的上下文。binding
本身是线程不安全的,在多个请求线程用同一个GroovyShell
实例执行时,binding
上绑定的变量是共享的,为此在实现上需要进行线程同步处理。而我们的做法是每次都创建一个新的GroovyShell
来避免这个问题,这种方式没有性能问题,因为groovy
本身就有对编译脚本相应的类缓存机制,且gc
方面做了优化。
统一Java运行环境
在植入groovy
脚本代码到当前的java
系统时,可以设置加载groovy
的类加载器为当前java
系统的类加载器,这样就可以直接使用当前环境中的类和类库了。比如可以在groovy
脚本中直接用获取spring bean
的工具类:
而外部需要传进来参与规则执行的对象可以通过设置为binding
的变量。
在groovy
脚本中抛出异常,和在java
系统中抛出的异常类型信息一样,不会被包装,方便系统原有的异常处理机制统一处理:
dsl风格的规则声明
借助于groovy
闭包和binding
可以很轻松的实现dsl
风格的声明:
这里的condition
以及逻辑运算闭包还可以进一步优化成,不满足逻辑条件就不再执行,比如上面截图中的第一个condition
满足条件,则后续的闭包不再解析执行,都可以自行控制实现。
弱类型的便利
原先在java
代码中编写的各种校验规则,需要严格的静态类型检查还需要避免在运行时的空指针问题,而groovy
天生就有类似于javascript
的弱类型特性,且变量属性的访问,也不会有空指针问题,比如Integer
类型的变量为null
,使用<
操作符;或者值为null
的字符串变量调用equals
。
在弱语言中有一个特别需要注意的问题,变量可能存在全局污染,我们只要遵循这样的原则:在一个groovy
脚本的头部声明变量时一定加上def
,因为不用类型声明的变量会作为全局变量,可以在shell
工具运行的多个脚本之间传递使用,因此要避免这一点。以下是笔者的练习:
校验规则的维护
对于简单的参数校验:非空、长度、正则等可以编写统一的groovy
函数或者闭包,这样字段校验的脚本会变得非常简单,而对于复杂的关联性的校验只需要用java
或者groovy
脚本,在我们定义的condition
中自由发挥即可。
运行结果:
另外通过这样的方式可以把规则成块的组织起来,包含了规则头部的声明块(变量的声明和初始化)、各个规则rule
闭包声明的规则执行块。有了这样的结构后,可以把规则的定义从文件搬到数据库中,然后再通过web端的编辑权限进行修改维护(用支持groovy
语法高亮的web
编辑器),这样就可以做到不重启服务器的情况下,来动态更新业务规则,非常的省心。
基于事实推断的规则引擎实现
基于groovy
强大的闭包语法特性,我们可以很轻松的实现笛卡尔积匹配方式的小型规则引擎,比如: