前言
在实际项目开发中经常会遇到一些根据业务规则来进行决策的场景。例如常见的订单满减活动,其相关规则如下:
通常情况大家都会通过if条件判断来或者采用策略模式来实现,具体实现如下:
public Double orderCount(Double totalMoney)
{
if (totalMoney >= 100 && totalMoney <= 200)
{
return totalMoney - 10;
}
else if (totalMoney > 200 && totalMoney <= 500)
{
return totalMoney - 30;
}
else if (totalMoney > 500 && totalMoney <= 1000)
{
return totalMoney - 50;
}
else if (totalMoney > 1000 && totalMoney <= 5000)
{
return totalMoney - 100;
}
else if (totalMoney > 5000 && totalMoney <= 10000)
{
return totalMoney - 300;
}
else if (totalMoney > 10000)
{
return totalMoney - 500;
}
return totalMoney;
}
如果是采用那种方式实现,活动运行一段时间后,运营人员针对上述的满减活动规则需要进行相关调整,例如新增满3000-5000减300规则,那么针对上述的需求变更,我们只能修改相关代码来扩展实现,那么有没有办法将活动的规则和业务代码进行解耦,不管规则如何变化,相关执行代码不需要进行改变呢?
针对上述的需求我们可以通过规则引擎来实现。规则引擎主要完成的就是将业务规则从代码中分离出来,并使用预定义的语义模块编写业务决策。Java开源的规则引擎有:Drools、Easy Rules、Mandarax、IBM ILOG。使用最为广泛并且开源的是Drools。本文将详细讲解Drools规则引擎。
Drools简介
Drools 是一个基于Charles Forgy’s的RETE算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。
应用场景
优缺点
优点
-
声明式编程
使用规则的核心优势在于可以简化对于复杂问题的逻辑表述,并对这些逻辑进行验证(规则比编码具有更好的可阅读性)。规则机制可以解决很复杂的问题,提供一个如何解决问题的说明,并说明每个决策的是如何得出的。
-
逻辑和数据分离
将业务逻辑都放在规则里的好处是业务逻辑发生变化时,可以更加方便的进行维护。尤其是这个业务逻辑是一个跨域关联多个域的逻辑时。不像原先那样将业务逻辑分散在多个对象或控制器中,业务逻辑可以被组织在一个或多个清晰定义的规则文件中。
-
速度和可扩展性 由由 网络算法(Rete algorithm),跳跃算法(Leaps algorithm)提供了非常高效的方式根据业务对象的数据匹配规则。Drools的Rete算法已经是一个成熟的算法。在Drools的帮助下,应用程序变得非常可扩展。如果频繁更改请求,可以添加新规则,而无需修改现有规则。
-
知识集中化
通过使用规则,您创建一个可执行的知识库(知识库)。这是商业政策的一个真理点。理想情况下,规则是可读的,它们也可以用作文档。
缺点
-
复杂性提高
-
需要学习新的规则语法
-
引入新组件的风险
Spring Boot集成
相关jara包依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--drools规则引擎-->
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>7.6.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>7.6.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-templates</artifactId>
<version>7.6.0.Final</version>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-api</artifactId>
<version>7.6.0.Final</version>
</dependency>
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-spring</artifactId>
<version>7.6.0.Final</version>
</dependency>
说明:本文drools的引用采用的是7.0+版本
Drools核心配置
@Configuration
public class DroolsConfig
{
private static final String RULES_PATH = "rules/";
@Bean
@ConditionalOnMissingBean(KieFileSystem.class)
public KieFileSystem kieFileSystem() throws IOException
{
KieFileSystem kieFileSystem = getKieServices().newKieFileSystem();
for (Resource file : getRuleFiles())
{
kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH
+ file.getFilename(), "UTF-8"));
}
return kieFileSystem;
}
private Resource[] getRuleFiles() throws IOException
{
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
return resourcePatternResolver.getResources("classpath*:" + RULES_PATH
+ "**/*.*");
}
@Bean
@ConditionalOnMissingBean(KieContainer.class)
public KieContainer kieContainer() throws IOException
{
final KieRepository kieRepository = getKieServices().getRepository();
//设置时间格式
System.setProperty("drools.dateformat","yyyy-MM-dd HH:mm");
kieRepository.addKieModule(kieRepository::getDefaultReleaseId);
KieBuilder kieBuilder = getKieServices().newKieBuilder(kieFileSystem());
kieBuilder.buildAll();
return getKieServices().newKieContainer(
kieRepository.getDefaultReleaseId());
}
private KieServices getKieServices()
{
return KieServices.Factory.get();
}
@Bean
@ConditionalOnMissingBean(KieBase.class)
public KieBase kieBase() throws IOException
{
return kieContainer().getKieBase();
}
@Bean
@ConditionalOnMissingBean(KieSession.class)
public KieSession kieSession() throws IOException
{
return kieContainer().newKieSession();
}
@Bean
@ConditionalOnMissingBean(KModuleBeanFactoryPostProcessor.class)
public KModuleBeanFactoryPostProcessor kiePostProcessor()
{
return new KModuleBeanFactoryPostProcessor();
}
}
规则配置
package com.skywares.fw.drools;
dialect "java"
import com.skywares.fw.drools.pojo.*;
rule "订单金额小于等于100无优惠"
salience 10 // 规则优先级,值越大越先执行
no-loop true // 事件是否重复执行该规则 true 至执行一次
activation-group "discount_group"
when
$order:Order(totalMoney <= 100)
then
$order.setPayMoney($order.getTotalMoney());
System.out.println("订单金额小于等于100无优惠");
end
rule "订单金额大于100-200优惠10元"
salience 10
no-loop true
activation-group "discount_group"
when
$order:Order(totalMoney >100, totalMoney <= 200)
then
$order.setPayMoney($order.getTotalMoney() - 10);
System.out.println("订单金额100-200优惠10元");
end
rule "订单金额大于200-500优惠30元"
salience 10
no-loop true
activation-group "discount_group"
when
$order:Order(totalMoney >200 && totalMoney <= 500)
then
$order.setPayMoney($order.getTotalMoney() - 30);
System.out.println("订单金额200-500优惠30元");
end
rule "订单金额大于500-1000优惠50元"
salience 10
no-loop true
activation-group "discount_group"
date-effective "2022-11-11 00:00"
date-expires "2024-11-20 00:00"
when
$order:Order(totalMoney > 500 && totalMoney <= 1000)
then
$order.setPayMoney($order.getTotalMoney() - 50);
System.out.println("订单金额500-1000优惠50元");
end
rule "订单金额大于1000-3000优惠100元"
salience 10
no-loop true
activation-group "discount_group"
when
$order:Order(totalMoney > 1000 && totalMoney <=3000)
then
$order.setPayMoney($order.getTotalMoney() - 100);
System.out.println("订单金额1000-3000优惠100元");
end
rule "订单金额大于3000-5000优惠100元"
salience 10
no-loop true
activation-group "discount_group"
when
$order:Order(totalMoney > 3000 && totalMoney <=5000)
then
$order.setPayMoney($order.getTotalMoney() - 300);
System.out.println("订单金额3000-5000优惠300元");
end
说明:
-
package 与Java语言类似,drl的头部需要有package和import的声明,package不必和物理路径一致。
-
import 导出java Bean的完整路径,也可以将Java静态方法导入调用。
-
rule 规则名称,需要保持唯一 件,可以无限次执行。
-
no-loop 定义当前的规则是否不允许多次循环执行,默认是 false,也就是当前的规则只要满足条件,可以无限次执行。
-
salience 用来设置规则执行的优先级,salience 属性的值是一个数字,数字越大执行优先级越高, 同时它的值可以是一个负数。默认情况下,规则的 salience 默认值为 0。如果不设置规则的 salience 属性,那么执行顺序是随机的。
-
when 条件语句,就是当到达什么条件的时候
-
then 根据条件的结果,来执行什么动作
-
end 规则结束
如果大家对于drools规则语法不熟悉的可以详细查看官网。
相关测试
@Autowired
private KieBase kieBase;
@RequestMapping("orderDiscount")
public Order orderDiscount(@RequestParam Double totalMoney)
{
Order order = new Order();
order.setTotalMoney(500d);
KieSession kieSession = kieBase.newKieSession();
kieSession.insert(order);
kieSession.fireAllRules();
kieSession.dispose();
return order;
}
通过输入不同的订单金额,可以享受不同的优惠。例如输入1000可以减100
相关思考
上述虽然通过Drools实现满减优惠活动,但是随着业务的不断发展,需要动态修改相关规则,如果 每次修改规则都需要重启相关应用对于客户的体验会非常差,那么将如何实现动态加载业务规则?
动态加载规则文件
private static final String RULES_PATH = "rules/";
public KieSession reloadRule(String drlName)
{
KieServices kieServices = KieServices.Factory.get();
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH
+ drlName+".drl", "UTF-8"));
final KieRepository kieRepository = kieServices.getRepository();
//设置时间格式
System.setProperty("drools.dateformat","yyyy-MM-dd HH:mm");
kieRepository.addKieModule(kieRepository::getDefaultReleaseId);
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem).buildAll();
Results results = kieBuilder.getResults();
if (results.hasMessages(Message.Level.ERROR))
{
logger.error("load rule result:"+results.getMessages());
throw new IllegalStateException("规则加载错误");
}
KieContainer kiecontainer=kieServices.newKieContainer(
kieRepository.getDefaultReleaseId());
logger.info("reload rule success");
return kiecontainer.newKieSession();
}
说明:规则文件的路径在resouces/rule目录下,传入规则文件名称即可。
测试
@RequestMapping("dynamicRule")
public Order dynamicRule(@RequestParam String ruleName,@RequestParam Double totalMoney)
{
Order order = new Order();
order.setTotalMoney(totalMoney);
KieSession kieSession = ruleService.reloadRule(ruleName);
kieSession.insert(order);
kieSession.fireAllRules();
kieSession.dispose();
return order;
}
说明:业务规则编号需要添规则满3000到5000需要减300,我们只需要修改相关的规则文件,执行http://localhost:9090/drools/dynamicRule?totalMoney=5000&ruleName=orderdiscount请求,无需重启即可生效。
总结
本文详细Spring Boot集成Drools,但是存在如下问题
-
drools规则文件需要和业务工程进行分离,目前是耦合在同一项目中。
-
业务人员如何动态编写业务规则?
关于这些问题将在后续的文章中进行详细讲解。