在实际项目开发中经常会遇到一些根据业务规则来进行决策的场景。例如常见的订单满减活动,其相关规则如下:
通常情况大家都会通过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 是一个基于Charles Forgy’s的RETE算法的,易于访问企业策略、易于调整以及易于管理的开源业务规则引擎,符合业内标准,速度快、效率高。业务分析师人员或审核人员可以利用它轻松查看业务规则,从而检验是否已编码的规则执行了所需的业务规则。
声明式编程
使用规则的核心优势在于可以简化对于复杂问题的逻辑表述,并对这些逻辑进行验证(规则比编码具有更好的可阅读性)。规则机制可以解决很复杂的问题,提供一个如何解决问题的说明,并说明每个决策的是如何得出的。
逻辑和数据分离
将业务逻辑都放在规则里的好处是业务逻辑发生变化时,可以更加方便的进行维护。尤其是这个业务逻辑是一个跨域关联多个域的逻辑时。不像原先那样将业务逻辑分散在多个对象或控制器中,业务逻辑可以被组织在一个或多个清晰定义的规则文件中。
速度和可扩展性 由由 网络算法(Rete algorithm),跳跃算法(Leaps algorithm)提供了非常高效的方式根据业务对象的数据匹配规则。 Drools的Rete算法已经是一个成熟的算法。在Drools的帮助下,应用程序变得非常可扩展。如果频繁更改请求,可以添加新规则,而无需修改现有规则。
知识集中化
通过使用规则,您创建一个可执行的知识库(知识库)。这是商业政策的一个真理点。理想情况下,规则是可读的,它们也可以用作文档。
复杂性提高
需要学习新的规则语法
引入新组件的风险
org.springframework.boot
spring-boot-starter-web
org.drools
drools-core
7.6.0.Final
org.drools
drools-compiler
7.6.0.Final
org.drools
drools-templates
7.6.0.Final
org.kie
kie-api
7.6.0.Final
org.kie
kie-spring
7.6.0.Final
复制代码
说明:本文drools的引用采用的是7.0+版本
@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
复制代码
说明:
如果大家对于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,但是存在如下问题
关于这些问题将在后续的文章中进行详细讲解。