来自云龙湖轮廓分明的月亮 发表于 2024-9-8 02:50:00

筹划并用Java实现一个浅易的规则引擎

前言

使用规则引擎可以很方便的实现一些比力复杂的业务逻辑。
本文介绍的浅易版,是一个小的通用代码结构。
通过组装业务数据,创建执行模版,最终执行,获取到最终结果。
各人在复杂场景下,也可以选用Drools来实现。
   Drools 是一个开源的业务规则管理系统,它提供了基于Java的规则引擎,用于实现复杂的变乱处理和业务逻辑。Drools 支持声明式编程,允许开发人员将业务逻辑从应用程序代码中分离出来,这使得业务逻辑的管理和变动更加机动和便捷。
正文

一、代码结构

https://i-blog.csdnimg.cn/direct/2e04c030f9ab48bc8507e5d671900468.png
二、焦点代码

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----------------


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 筹划并用Java实现一个浅易的规则引擎