Groovy脚本实现轻量级规则引擎
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Groovy脚本实现轻量级规则引擎相关的知识,希望对你有一定的参考价值。
参考技术A1) 当业务规则变更时,对应的代码也得跟着更改,每次即使是小的变更都需要经历开发、测试验证上线等过程,变更成本比较大。
2) 长时间系统变得越来越难以维护。
3) 开发团队一般是由一个熟悉业务的BA(业务分析人员)和若干个熟悉技术的开发人员组成,开发人员对业务规则的把握能力远不及BA,但实际上却承担了将业务规则准确无误实现的重任。
4) 系统僵化,新需求插入困难。
5) 新需求上线周期较长。
规则引擎由推理引擎发展而来,是一种嵌入在应用程序中的组件,实现了 将业务决策从应用程序代码中分离出来 ,并使用预定义的语义模块编写业务决策。接受数据输入,解释业务规则,并根据业务规则做出业务决策。
把规则和核心业务拆开, 规则单独配置 。这样当我们的规则变化的时候,就可以通过修改规则文件而 不用修改核心的代码 了。
在 规则较为复杂的行业 ,都是适用的,如金融、制造、医疗、物流等行业,面临的规则变化较为复杂,目前使用较多。而且对规则引擎需求的行业也将越来越多,如团购平台、传统企业管理渠道等。
JBoss Drools Rete算法
Mandarax
JLisa
OpenRules
JEOPS
InfoSapient
JRuleEngine
Roolie
Rete 模式匹配算法是在模式匹配中利用推理机的时间冗余性和规则结构的相似性, 通过保存中间去处来提高推理效率的一种模式匹配算法。
在模式匹配过程中, 规则的前提中可能会有很多相同的模块, 因此在匹配规则前提时, 将进行大量的重复运算, 这样就带来时间冗余性问题。例如:
RULE1:if (A>B) and D or C then E=100
RULE2:if (A>B) and (BB) or (BB 要进行三次计算, 对B B, M2=B
springboot中如何整合groovy实现一个轻量级规则引擎
详细实现以及使用教程以及压测结果分析见:https://gitee.com/mr_wenpan/basis-enhance/blob/master/enhance-boot-groovy-engine/README.md
一、项目功能说明
该工程(enhance-boot-groovy-engine
)主要是利用【springboot + groovy
】对groovy动态加载脚本功能进行了封装和集成,使得在springboot项目中能够更加简单方便的使用groovy在不重启的情况下来动态的加载外部脚本,可以看做是一个基于groovy封装的轻量级【规则引擎】。
1、为什么选择groovy
- 在实际的平台化项目中,为了提升平台抽象能力使用更多场景,引入了规则引擎。如一个用户购买了500元商品可以获得10元红包,异或是购买了指定会场商品获取50元优惠券等。在不通互动场景中活动的规则是不同的,如果通过JAVA去实现,每次有新的规则要求,都要发布一次,这样成本就太高了。
- 如果项目里只需要使用到轻量级的规则脚本,那么此时去引入像Drools这样的重量级规则引擎,那么有些杀鸡用牛刀的感觉。
- groovy对Java的支持性非常好,完全可以使用Java语法去编写groovy脚本,对于Java开发者来说几乎没有学习成本。
2、项目结构
- 项目大体分为两部分,core部分代表了功能的核心实现,loader部分代表了对脚本来源的扩展支持从不同来源加载脚本
demo-enhance-groovy-engine
中是对该项目的一些使用demo
3、项目特点
- 基于springboot来整合的groovy,使用者仅需要引入【maven坐标】,配置好【
application.yml
】参数即可方便使用 - 提供了【从classpath下加载脚本】、【从Redis加载脚本】、【从MySQL数据库加载脚本】这三种脚本来源选项,脚本加载源可以方便的替换(仅需要更改application.yml配置即可)
- 使用【
GroovyClassLoader + InvokerHelper
+ 缓存parse好的Class对象】来解决频繁load groovy脚本为Class从而导致方法区OOM问题。同时通过缓存脚本信息来避免每次执行脚本都需要重新编译而带来的性能消耗,保证脚本执行的高效。 - 提供
EngineExecutor
方便的来执行脚本,同时提供多种执行脚本的方式,仅需要传入能定位脚本的唯一key和脚本里需要的参数即可方便的调用指定的脚本进行执行了 - 项目中提供了非常多的【可配置项】以及【可替换组件】,使用方可以根据项目需要调整配置项参数值,也可以自己实现某一个组件,然后注册到容器中,进而替换原有组件。
- 项目中使用【
caffeine
】来缓存脚本项,并设置过期时间,并且项目里提供了定时刷新(刷新间隔周期可配置)本地缓存里的脚本项的异步线程,可以及时的将【本地缓存的脚本项】和【数据源中的脚本项】进行对比,一旦发现数据源中的脚本发生了变更则及时刷新本地缓存中的脚本项,并且替换原脚本对应的Class。 - 项目中提供了很多xxxxxHelper,这些helper将本项目的核心功能进行透出,使用方可以通过这些helper来进行定制化操作
- 【
ApplicationContextHelper
】提供了操作spring容器的一些功能,可以借助该helper方便的对IOC容器进行操作 - 【
RefreshScriptHelper
】提供了动态刷新本地内存中的脚本的能力,可以通过该helper提供的方法来手动的刷新本地内存的脚本(支持单个刷新和批量刷新),比如:新增或修改了某个脚本后想立即让该脚本生效。 - 【
RegisterScriptHelper
】提供了动态向数据源和本地缓存里注册脚本的能力,可以通过该helper来动态的向数据源和本地缓存中修改脚本或注册新的groovy脚本
- 【
- 项目中使用caffeine缓存来缓存脚本,指定了最大缓存的脚本条数(可配置),当脚本条数达到最大缓存条数后通过淘汰算法淘汰不常用的脚本,以保证不会因为加载过多脚本导致内存占用过高。并且可以配置缓存条目的有效期,可以淘汰失效的或者长时间不用的脚本项,当再次使用到时再从数据源加载脚本并缓存。
- 项目提供的能力需要在application.yml中通过配置开启,如果引入了本项目的maven依赖,但是没有在application.yml中显示开启功能,那么本项目的功能不会生效。
- 脚本动态刷新部分参考zuul网关的filter源码进行编写
二、开启配置
1、下载源码并打包到自己的maven仓库
源码地址:https://gitee.com/mr_wenpan/basis-enhance/blob/master/enhance-boot-data-redis/README.md
执行命令:mvn clean install
(需要切换到该项目目录下执行)
2、pom中引入依赖坐标
特别说明:
- 【
enhance-groovy-engine-core
】是核心依赖,必须要引入,其他的按需求选配即可。 - 如果是从classpath下加载脚本,那么只需要引入【
enhance-groovy-classpath-loader
】即可 - 如果是从redis中加载脚本,那么需要引入【
enhance-groovy-redis-loader
】,由于是从Redis中读取脚本,所以Redis的核心依赖不能少【spring-boot-starter-data-redis
】和 【commons-pool2
】(如果项目中已经有Redis了,那可以不引入这两个) - 如果是从MySQL中加载脚本,那么需要引入【
enhance-groovy-mysql-loader
】以及连接MySQL所需要的的依赖
<!--核心依赖-->
<dependency>
<artifactId>enhance-groovy-engine-core</artifactId>
<groupId>org.basis.enhance</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--++++++++++++++++++++++++++++++++以下三种loader,按需选配即可++++++++++++++++++++++++++++++++-->
<!--++++++++++++++++++++++++++++++++++第一种:基于Redis的loader++++++++++++++++++++++++++++++++++-->
<!--加载Redis下的groovy脚本loader依赖-->
<dependency>
<artifactId>enhance-groovy-redis-loader</artifactId>
<groupId>org.basis.enhance</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--redis核心依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--Apache的 common-pool2(至少是2.2)提供连接池,供redis客户端使用-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!--++++++++++++++++++++++++++++++++++第二种:基于Classpath的loader++++++++++++++++++++++++++++++++++-->
<!--加载classpath下的groovy脚本loader依赖-->
<dependency>
<artifactId>enhance-groovy-classpath-loader</artifactId>
<groupId>org.basis.enhance</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--++++++++++++++++++++++++++++++++++第三种:基于MySQL的loader++++++++++++++++++++++++++++++++++-->
<!--加载MySQL下的groovy脚本loader依赖-->
<dependency>
<artifactId>enhance-groovy-mysql-loader</artifactId>
<groupId>org.basis.enhance</groupId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--以下是mysql相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
</dependency>
<!--mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
3、配置application.yml
以下以从Redis加载脚本为例来演示配置
server:
port: 1234
spring:
application:
name: customer-console
# redis基础配置
redis:
host: $SPRING_REDIS_HOST:xxx-host
port: $SPRING_REDIS_PORT:6379
password: $SPRING_REDIS_PASSWORD:xxxx@123
database: $SPRING_REDIS_DATABASE:2
enhance:
groovy:
engine:
# 脚本检查更新周期(单位:秒),(默认300L)
pollingCycle: 10
# 开启功能
enable: true
# 缓存过期时间(默认600L分钟)
cacheExpireAfterWrite: 10
#缓存初始容量(默认100)
cacheInitialCapacity: 10
# 缓存最大容量(默认500)
cacheMaximumSize: 20
# 开启基于Redis加载groovy脚本
redis-loader:
# 命名空间,可以和应用名称保持一致即可,主要是为了区分不同的应用
namespace: customer-console
# 开启基于Redis的loader
enable: true
三、使用介绍
1、Redis中导入groovy脚本
- 当然你也可以项目启动好后通过
RegisterScriptHelper
来注册脚本,这里采用预先加载好脚本到Redis来演示 - 对应的脚本都在
org.enhance.groovy.infra.groovy
目录下,自己按需设定脚本key即可
①、脚本内容
- key: customer-console
- hashKey: change-order
- 脚本:
package org.enhance.groovy.api.dto
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class ChangeOrderInfo extends Script
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
Object run()
// 调用方法
changeOrderInfo();
// 修改订单信息
OrderInfoDTO changeOrderInfo()
String newOrderAmount = "20000";
// 获取参数
OrderInfoDTO orderInfoDTO = orderInfo;
logger.info("即将修改订单金额,原金额为:, 修改后的金额为:", orderInfoDTO.getOrderAmount(), newOrderAmount);
orderInfoDTO.setOrderAmount("2000");
// 返回修改后的结果
return orderInfoDTO;
- key: customer-console
- hashKey: change-product
- 脚本:
package org.enhance.groovy.api.dto
import org.basis.enhance.groovy.entity.ExecuteParams
import org.slf4j.Logger
import org.slf4j.LoggerFactory
class ChangeProductInfo extends Script
private final Logger logger = LoggerFactory.getLogger(getClass());
// 修改商品信息
ProductInfoDTO changeProduct(ExecuteParams executeParams)
// 获取product对象
ProductInfoDTO productInfo = (ProductInfoDTO) executeParams.get("productInfo");
Double newOrderAmount = 20000D;
logger.info("即将修改商品金额,原金额为:, 修改后的金额为:", productInfo.getPrice(), newOrderAmount);
// 商品价格修改为newOrderAmount
productInfo.setPrice(newOrderAmount);
// 返回修改后的对象
return productInfo;
@Override
Object run()
return null
- key: customer-console
- hashKey: get-context
- 脚本:
package org.enhance.groovy.infra.groovy
import org.enhance.groovy.api.dto.ProductInfoDTO
import org.enhance.groovy.app.service.ProductService
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationContext
/**测试从spring ioc容器中获取bean,并调用bean的方法*/
class GetApplicationContext extends Script
private final Logger logger = LoggerFactory.getLogger(getClass());
@Override
Object run()
// 调用方法
ApplicationContext context = getContext();
// 获取容器中的bean
ProductService productService = context.getBean(ProductService.class);
// 调用bean的方法
Random random = new Random();
ProductInfoDTO productInfoDTO = productService.getProductById(random.nextInt(1000));
logger.info("productInfoDTO is : ", productInfoDTO);
// 调用bean 的修改方法
productService.updateProduct(productInfoDTO);
logger.info("updated productInfoDTO is : ", productInfoDTO);
return productInfoDTO;
// 获取spring容器
ApplicationContext getContext()
// 获取spring IOC容器
ApplicationContext context = applicationContext;
return context;
2、启动项目并调用脚本
- 项目启动好后直接访问:http://localhost:1234/v1/load-from-redis/change-order?scriptName=change-order即可
- 观察控制台,可以看到orderAmount已经被脚本动态修改了
/**
* scriptName只要能唯一定位到脚本即可
* 测试@link EngineExecutor#execute(ScriptQuery, ExecuteParams)
* 请求URL:http://localhost:1234/v1/load-from-redis/change-order?scriptName=change-order
*/
@GetMapping("/change-order")
public String changeOrderInfo(String scriptName)
// 构建参数
OrderInfoDTO orderInfoDTO = new OrderInfoDTO();
orderInfoDTO.setOrderAmount("1000");
orderInfoDTO.setOrderName("测试订单");
orderInfoDTO.setOrderNumber("BG-123987");
ExecuteParams executeParams = new ExecuteParams();
executeParams.put("orderInfo", orderInfoDTO);
// 执行脚本
EngineExecutorResult executorResult = engineExecutor.execute(new ScriptQuery(scriptName), executeParams);
String statusCode = executorResult.getExecutionStatus().getCode();
if("200".equals(statusCode))
log.info("脚本执行成功......");
else
log.info("脚本执行失败......");
log.info("changeOrderInfo=========>>>>>>>>>>>执行结果:", executorResult);
return "success";
以上是关于Groovy脚本实现轻量级规则引擎的主要内容,如果未能解决你的问题,请参考以下文章
springboot中如何整合groovy实现一个轻量级规则引擎