设计并用Java实现一个简易的规则引擎
文章目录
- 前言
- 正文
- 一、代码结构
- 二、核心代码
- 2.1 上下文数据接口 ContextData.java
- 2.2 规则接口 Rule.java
- 2.3 规则引擎接口 RuleEngine.java
- 2.4 规则引擎上下文类 RuleEngineContext.java
- 2.5 规则引擎默认实现类 DefaultRuleEngine.java
- 2.6 执行时出错监听器接口 ExecuteOnErrorListener.java
- 2.7 规则引擎执行模版类 RuleEngineExecuteTemplate.java
- 三、测试业务
- 3.1 新增折扣价格上下文实现类DiscountPriceContextData.java
- 3.2 新增固定金额折扣规则实现类 FixedAmountDiscountRule.java
- 3.3 新增百分比折扣规则实现类 PercentageDiscountRule.java
- 3.4 新增满减折扣规则实现类 FullReductionDiscountRule.java
- 3.5 编写测试类 Test.java
- 3.6 测试类运行结果
前言
使用规则引擎可以很方便的实现一些比较复杂的业务逻辑。
本文介绍的简易版,是一个小的通用代码结构。
通过组装业务数据,创建执行模版,最终执行,获取到最终结果。
大家在复杂场景下,也可以选用Drools来实现。
Drools 是一个开源的业务规则管理系统,它提供了基于Java的规则引擎,用于实现复杂的事件处理和业务逻辑。Drools 支持声明式编程,允许开发人员将业务逻辑从应用程序代码中分离出来,这使得业务逻辑的管理和变更更加灵活和便捷。
正文
一、代码结构
二、核心代码
maven依赖文件:
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
2.1 上下文数据接口 ContextData.java
package com.song.tools.ruleengine.core;
/**
* 上下文数据
*
* @author song tools
*/
public interface ContextData {
/**
* 校验上下文数据
*/
default void validProperty() {
}
}
2.2 规则接口 Rule.java
package com.song.tools.ruleengine.core;
/**
* 规则接口
*
* @author song tools
*/
public interface Rule<ContextData extends com.song.tools.ruleengine.core.ContextData> {
/**
* 执行前
*
* @param contextData 上下文数据
*/
default void before(ContextData contextData) {
contextData.validProperty();
}
/**
* 执行
*
* @param context 上下文
*/
void execute(RuleEngineContext<ContextData> context);
/**
* 执行后
*
* @param contextData 上下文数据
*/
default void after(ContextData contextData) {
}
}
2.3 规则引擎接口 RuleEngine.java
package com.song.tools.ruleengine.core;
/**
* 规则引擎
*
* @author song tools
*/
public interface RuleEngine<ContextData extends com.song.tools.ruleengine.core.ContextData> {
/**
* 注册规则
*
* @param ruleKey 规则key
* @param rule 规则
*/
void registerRule(String ruleKey, Rule<ContextData> rule);
/**
* 执行规则
*
* @param context 上下文
*/
void executeRule(RuleEngineContext<ContextData> context) throws Exception;
/**
* 规则数量
*
* @return 规则数量
*/
int ruleSize();
}
2.4 规则引擎上下文类 RuleEngineContext.java
package com.song.tools.ruleengine.core;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 规则引擎上下文
*
* @author song tools
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class RuleEngineContext<ContextData extends com.song.tools.ruleengine.core.ContextData> {
/**
* 上下文数据
*/
private ContextData contextData;
}
2.5 规则引擎默认实现类 DefaultRuleEngine.java
package com.song.tools.ruleengine.core;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 默认实现的规则引擎
*
* @author song tools
*/
public class DefaultRuleEngine<ContextData extends com.song.tools.ruleengine.core.ContextData> implements RuleEngine<ContextData> {
private final Map<String, Rule<ContextData>> RULE_MAP;
public DefaultRuleEngine() {
RULE_MAP = new LinkedHashMap<>(16);
}
@Override
public void registerRule(String ruleKey, Rule<ContextData> rule) {
RULE_MAP.put(ruleKey, rule);
}
@Override
public void executeRule(RuleEngineContext<ContextData> context) {
for (Rule<ContextData> rule : RULE_MAP.values()) {
rule.before(context.getContextData());
rule.execute(context);
rule.after(context.getContextData());
}
}
@Override
public int ruleSize() {
return RULE_MAP.size();
}
}
2.6 执行时出错监听器接口 ExecuteOnErrorListener.java
package com.song.tools.ruleengine.core;
/**
* 执行时出错监听器
*
* @author song tools
*/
public interface ExecuteOnErrorListener<ContextData extends com.song.tools.ruleengine.core.ContextData> {
void onError(ContextData contextData, Exception e);
}
2.7 规则引擎执行模版类 RuleEngineExecuteTemplate.java
package com.song.tools.ruleengine.core;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 规则执行模版
*
* @author song tools
*/
@Getter
@Slf4j
public class RuleEngineExecuteTemplate<ContextData extends com.song.tools.ruleengine.core.ContextData> {
/**
* 规则引擎
*/
private final RuleEngine<ContextData> ruleEngine;
/**
* 规则索引
*/
private final AtomicInteger ruleIndex;
/**
* 执行失败监听器
*/
private ExecuteOnErrorListener<ContextData> executeOnErrorListener;
public RuleEngineExecuteTemplate() {
this(new DefaultRuleEngine<>());
}
public RuleEngineExecuteTemplate(RuleEngine<ContextData> ruleEngine) {
Objects.requireNonNull(ruleEngine, "ruleEngine is null");
this.ruleIndex = new AtomicInteger(1);
this.ruleEngine = ruleEngine;
}
/**
* 注册规则
*
* @param rule 规则
* @return this 当前模版对象
*/
public RuleEngineExecuteTemplate<ContextData> registerRule(Rule<ContextData> rule) {
String ruleKey = rule.getClass().getSimpleName();
return this.registerRule(ruleKey, rule);
}
/**
* 注册规则
*
* @param rules 规则列表
* @return this 当前模版对象
*/
public RuleEngineExecuteTemplate<ContextData> registerRule(List<Rule<ContextData>> rules) {
for (Rule<ContextData> rule : rules) {
this.registerRule(rule);
}
return this;
}
/**
* 注册规则
*
* @param ruleKey 规则key
* @param rule 规则
* @return this 当前模版对象
*/
public RuleEngineExecuteTemplate<ContextData> registerRule(String ruleKey, Rule<ContextData> rule) {
ruleEngine.registerRule(ruleKey + ruleIndex.getAndIncrement(), rule);
return this;
}
public RuleEngineExecuteTemplate<ContextData> registerExecuteOnErrorListener(ExecuteOnErrorListener<ContextData> executeOnErrorListener) {
this.executeOnErrorListener = executeOnErrorListener;
return this;
}
/**
* 执行规则
*/
public void execute(ContextData contextData) throws Exception {
Objects.requireNonNull(contextData, "contextData is null");
if (ruleEngine.ruleSize() == 0) {
throw new IllegalArgumentException("ruleEngine not found register rule");
}
// 组装上下文
RuleEngineContext<ContextData> context = new RuleEngineContext<>();
context.setContextData(contextData);
try {
// 执行规则
ruleEngine.executeRule(context);
} catch (Exception e) {
log.error("execute rule error", e);
if (Objects.nonNull(executeOnErrorListener)) {
executeOnErrorListener.onError(context.getContextData(), e);
}
throw e;
}
}
}
三、测试业务
假设现在需要灵活的计算单个商品的价格折扣相关的内容。
3.1 新增折扣价格上下文实现类DiscountPriceContextData.java
package com.song.tools.ruleengine;
import com.song.tools.ruleengine.core.ContextData;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Objects;
/**
* 折扣价格上下文(对于单件商品,简单场景的计算)
*
* @author song tools
*/
@Data
public class DiscountPriceContextData implements ContextData {
/**
* 临时价格(存在多个计算规则时,使用它进行传递计算的中间值)
*/
private BigDecimal calculatedTempPrice;
/**
* 原价
*/
private BigDecimal originalPrice;
/**
* 折扣后价格
*/
private BigDecimal discountedPrice;
@Override
public void validProperty() {
Objects.requireNonNull(originalPrice, "原价不能为空");
}
}
3.2 新增固定金额折扣规则实现类 FixedAmountDiscountRule.java
package com.song.tools.ruleengine;
import com.song.tools.ruleengine.core.Rule;
import com.song.tools.ruleengine.core.RuleEngineContext;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class FixedAmountDiscountRule implements Rule<DiscountPriceContextData> {
private final BigDecimal discountAmount;
public FixedAmountDiscountRule(BigDecimal discountAmount) {
this.discountAmount = discountAmount;
}
@Override
public void execute(RuleEngineContext<DiscountPriceContextData> context) {
DiscountPriceContextData contextData = context.getContextData();
// 临时价格
BigDecimal calculatedTempPrice = contextData.getCalculatedTempPrice();
// 原始价格
BigDecimal originalPrice = contextData.getOriginalPrice();
if (calculatedTempPrice != null) {
originalPrice = calculatedTempPrice;
}
// 进行精确减法运算,并确保结果不会小于0
BigDecimal discountedPrice = (originalPrice.subtract(discountAmount).max(BigDecimal.ZERO)).setScale(2, RoundingMode.HALF_UP);
// 设置折扣后的价格
contextData.setDiscountedPrice(discountedPrice);
contextData.setCalculatedTempPrice(discountedPrice);
}
@Override
public void before(DiscountPriceContextData contextData) {
Rule.super.before(contextData);
}
@Override
public void after(DiscountPriceContextData contextData) {
Rule.super.after(contextData);
}
}
3.3 新增百分比折扣规则实现类 PercentageDiscountRule.java
package com.song.tools.ruleengine;
import com.song.tools.ruleengine.core.Rule;
import com.song.tools.ruleengine.core.RuleEngineContext;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class PercentageDiscountRule implements Rule<DiscountPriceContextData> {
private static final BigDecimal ONE_HUNDRED = new BigDecimal("100");
private final BigDecimal discountPercentage;
public PercentageDiscountRule(BigDecimal discountPercentage) {
this.discountPercentage = discountPercentage;
}
@Override
public void execute(RuleEngineContext<DiscountPriceContextData> context) {
DiscountPriceContextData contextData = context.getContextData();
// 临时价格
BigDecimal calculatedTempPrice = contextData.getCalculatedTempPrice();
// 原始价格
BigDecimal originalPrice = contextData.getOriginalPrice();
if (calculatedTempPrice != null) {
originalPrice = calculatedTempPrice;
}
// 计算折扣率
BigDecimal discountRate = discountPercentage.divide(ONE_HUNDRED, 2, RoundingMode.HALF_UP);
// 计算折扣后的价格(四舍五入,保留2位小数)
BigDecimal discountedPrice = originalPrice.multiply(discountRate).setScale(2, RoundingMode.HALF_UP);
contextData.setDiscountedPrice(discountedPrice);
contextData.setCalculatedTempPrice(discountedPrice);
}
@Override
public void before(DiscountPriceContextData contextData) {
Rule.super.before(contextData);
}
@Override
public void after(DiscountPriceContextData contextData) {
Rule.super.after(contextData);
}
}
3.4 新增满减折扣规则实现类 FullReductionDiscountRule.java
package com.song.tools.ruleengine;
import com.song.tools.ruleengine.core.Rule;
import com.song.tools.ruleengine.core.RuleEngineContext;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 满减折扣规则
*
* @author song tools
*/
public class FullReductionDiscountRule implements Rule<DiscountPriceContextData> {
/**
* 满减金额,满xx金额数
*/
private final BigDecimal fullReductionBoundAmount;
/**
* 满减折扣金额,减去xx金额数
*/
private final BigDecimal fullReductionDiscountAmount;
public FullReductionDiscountRule(BigDecimal fullReductionBoundAmount, BigDecimal fullReductionDiscountAmount) {
this.fullReductionBoundAmount = fullReductionBoundAmount;
this.fullReductionDiscountAmount = fullReductionDiscountAmount;
}
public void execute(RuleEngineContext<DiscountPriceContextData> context) {
DiscountPriceContextData contextData = context.getContextData();
// 临时价格
BigDecimal calculatedTempPrice = contextData.getCalculatedTempPrice();
// 原始价格
BigDecimal originalPrice = contextData.getOriginalPrice();
if (calculatedTempPrice != null) {
originalPrice = calculatedTempPrice;
}
// 比较金额是否满足满减条件
if (originalPrice.compareTo(fullReductionBoundAmount) >= 0) {
BigDecimal discountedPrice = originalPrice.subtract(fullReductionDiscountAmount).setScale(2, RoundingMode.HALF_UP);
contextData.setDiscountedPrice(discountedPrice);
contextData.setCalculatedTempPrice(discountedPrice);
}
}
}
3.5 编写测试类 Test.java
编排不同的规则,不通的顺序,验证最终的执行结果。
package com.song.tools.ruleengine;
import com.song.tools.ruleengine.core.DefaultRuleEngine;
import com.song.tools.ruleengine.core.RuleEngineContext;
import com.song.tools.ruleengine.core.RuleEngineExecuteTemplate;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 测试规则执行
*
* @author song tools
*/
public class Test {
public static void main(String[] args) throws Exception {
System.out.println("----------------test1----------------");
test1();
System.out.println("----------------test1----------------");
System.out.println("----------------test2----------------");
test2();
System.out.println("----------------test2----------------");
}
private static void test1() {
// 创建规则引擎
DefaultRuleEngine<DiscountPriceContextData> ruleEngine = new DefaultRuleEngine<>();
// 创建规则+注册规则
ruleEngine.registerRule("percentageDiscountRule1", new PercentageDiscountRule(BigDecimal.valueOf(95)));
ruleEngine.registerRule("fixedAmountDiscountRule1", new FixedAmountDiscountRule(BigDecimal.valueOf(12.34)));
ruleEngine.registerRule("percentageDiscountRule2", new PercentageDiscountRule(BigDecimal.valueOf(80)));
ruleEngine.registerRule("fixedAmountDiscountRule2", new FixedAmountDiscountRule(BigDecimal.valueOf(12.4)));
// 创建上下文数据
RuleEngineContext<DiscountPriceContextData> context = new RuleEngineContext<>();
DiscountPriceContextData discountPriceContextData = new DiscountPriceContextData();
discountPriceContextData.setOriginalPrice(BigDecimal.valueOf(100));
context.setContextData(discountPriceContextData);
// 执行规则引擎
ruleEngine.executeRule(context);
// 打印执行结果
System.out.println(discountPriceContextData.getDiscountedPrice());
System.out.println(BigDecimal.valueOf(100).multiply(BigDecimal.valueOf(0.95)).subtract(BigDecimal.valueOf(12.34)));
System.out.println(BigDecimal.valueOf(82.66).multiply(BigDecimal.valueOf(0.8).setScale(2, RoundingMode.HALF_UP)).subtract(BigDecimal.valueOf(12.4)).setScale(2, RoundingMode.HALF_UP));
}
private static void test2() throws Exception {
// 创建上下文数据
DiscountPriceContextData discountPriceContextData = new DiscountPriceContextData();
discountPriceContextData.setOriginalPrice(BigDecimal.valueOf(100));
// 创建规则引擎执行模板
RuleEngineExecuteTemplate<DiscountPriceContextData> ruleEngineExecuteTemplate = new RuleEngineExecuteTemplate<>();
// 注册规则&执行规则
ruleEngineExecuteTemplate.registerRule(new PercentageDiscountRule(BigDecimal.valueOf(95))) // 95折
.registerRule(new FixedAmountDiscountRule(BigDecimal.valueOf(12.34))) // 减12.34元
.registerRule(new PercentageDiscountRule(BigDecimal.valueOf(80))) // 80折
.registerRule(new FullReductionDiscountRule(BigDecimal.valueOf(50), BigDecimal.valueOf(10))) // 满50减10
.registerRule(new FixedAmountDiscountRule(BigDecimal.valueOf(12.4))) // 减12.4元
.registerRule(new FullReductionDiscountRule(BigDecimal.valueOf(40), BigDecimal.valueOf(19.9))) // 满40减19.9元
.execute(discountPriceContextData);
// 打印最终结果
System.out.println(discountPriceContextData.getDiscountedPrice());
}
}
3.6 测试类运行结果
----------------test1----------------
53.73
82.66
53.73
----------------test1----------------
----------------test2----------------
23.83
----------------test2----------------