CC 咖啡猫的工作空间 Coding Space

规则引擎实践

规则引擎将业务规则从代码中分离出来,让非技术人员也能动态管理业务逻辑。


1. 为什么需要规则引擎

传统方式 规则引擎方式
规则写死在代码里 规则可配置、可热更新
修改规则需要发版 运营人员实时调整
规则复杂时代码难以维护 规则外置,逻辑清晰
规则变化频繁时开发负担重 规则变化由业务方主导

典型场景:营销活动、风控策略、定价规则、审批流程


2. 规则引擎对比

引擎 特点 适用场景 学习成本
Drools 功能强大、语法完善、支持复杂规则链 复杂企业规则
Aviator 轻量、表达式求值、高性能 简单条件判断
EasyRules 简单注解驱动、易于集成 轻量规则
QLExpress 阿里开源、电商场景优化 交易规则
SpEL Spring内置、无需额外依赖、功能丰富 Spring项目中的简单条件

3. 设计要点

3.1 规则模型

// 规则定义
public class Rule {
    private String ruleId;           // 规则ID
    private String ruleName;         // 规则名称
    private String ruleGroup;       // 规则分组
    private Integer priority;       // 优先级(数字越小越优先)
    private String condition;        // 条件表达式
    private String action;          // 执行动作
    private Boolean enabled;        // 是否启用
    private LocalDateTime startTime; // 生效时间
    private LocalDateTime endTime;   // 失效时间
}

// 规则上下文(输入数据)
public class RuleContext {
    private Map<String, Object> facts;  // 事实数据
    private Map<String, Object> result; // 规则执行结果
}

// 规则执行结果
public class RuleResult {
    private boolean matched;         // 是否匹配
    private String ruleId;           // 命中的规则ID
    private Object actionResult;     // 执行结果
    private String message;          // 说明信息
}

3.2 规则表设计

CREATE TABLE rule_definition (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    rule_id     VARCHAR(64) UNIQUE NOT NULL,
    rule_name   VARCHAR(100) NOT NULL COMMENT '规则名称',
    rule_group  VARCHAR(50) COMMENT '规则分组',
    priority    INT DEFAULT 100 COMMENT '优先级',
    condition   TEXT NOT NULL COMMENT '条件表达式',
    action      TEXT COMMENT '执行动作',
    enabled     TINYINT DEFAULT 1 COMMENT '1-启用 0-禁用',
    start_time  DATETIME COMMENT '生效时间',
    end_time    DATETIME COMMENT '失效时间',
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME,
    INDEX idx_rule_group (rule_group),
    INDEX idx_enabled (enabled)
) COMMENT '规则定义表';

CREATE TABLE rule_execute_log (
    id          BIGINT PRIMARY KEY AUTO_INCREMENT,
    rule_id     VARCHAR(64),
    input_data  TEXT COMMENT '输入数据JSON',
    output_data TEXT COMMENT '输出结果JSON',
    execute_time INT COMMENT '执行耗时ms',
    success     TINYINT DEFAULT 1,
    error_msg   VARCHAR(500),
    created_at  DATETIME
) COMMENT '规则执行日志';

4. 实现方案

4.1 Aviator 轻量实现

@Service
public class AviatorRuleEngine {

    // 缓存编译后的规则
    private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();

    public RuleResult execute(Rule rule, RuleContext context) {
        try {
            // 获取或编译表达式
            Expression exp = expressionCache.computeIfAbsent(
                rule.getRuleId(),
                id -> AviatorEvaluator.compile(rule.getCondition())
            );

            // 合并全局变量和facts
            Map<String, Object> env = new HashMap<>(context.getFacts());

            // 执行规则
            Boolean matched = (Boolean) exp.execute(env);

            RuleResult result = new RuleResult();
            result.setMatched(matched);
            result.setRuleId(rule.getRuleId());

            if (matched && rule.getAction() != null) {
                // 执行动作(可以是另一个表达式或脚本)
                Object actionResult = executeAction(rule.getAction(), context);
                result.setActionResult(actionResult);
            }

            return result;
        } catch (Exception e) {
            return RuleResult.fail(rule.getRuleId(), e.getMessage());
        }
    }
}

4.2 Drools 复杂实现

@Configuration
public class DroolsConfig {

    @Bean
    public KieServices kieServices() {
        return KieServices.Factory.get();
    }

    @Bean
    public KieContainer kieContainer() {
        // 从classpath加载规则
        return kieServices().getKieClasspathContainer();
    }
}

@Service
public class DroolsRuleEngine {

    @Autowired
    private KieContainer kieContainer;

    public RuleResult execute(String ruleGroup, RuleContext context) {
        KieSession kieSession = kieContainer.newKieSession(ruleGroup);

        try {
            // 插入事实
            context.getFacts().forEach(kieSession::insert);

            // 插入结果对象
            RuleResult result = new RuleResult();
            kieSession.setGlobal("result", result);

            // 执行规则
            kieSession.fireAllRules();

            return result;
        } finally {
            kieSession.dispose();
        }
    }
}

4.3 SpEL(Spring Expression Language)实现

SpEL是Spring框架内置的表达式语言,无需额外依赖,特别适合Spring Boot项目。

基础SpEL规则引擎实现

@Service
public class SpelRuleEngine {

    private final ExpressionParser parser = new SpelExpressionParser();
    private final Map<String, Expression> expressionCache = new ConcurrentHashMap<>();

    public RuleResult execute(Rule rule, RuleContext context) {
        try {
            // 检查规则是否在有效期内
            if (!isRuleValid(rule)) {
                return RuleResult.notValid(rule.getRuleId());
            }

            // 获取或编译表达式
            Expression conditionExp = expressionCache.computeIfAbsent(
                "condition_" + rule.getRuleId(),
                id -> parser.parseExpression(rule.getCondition())
            );

            // 创建评估上下文
            StandardEvaluationContext evalContext = new StandardEvaluationContext();
            context.getFacts().forEach(evalContext::setVariable);

            // 执行条件判断
            Boolean matched = conditionExp.getValue(evalContext, Boolean.class);

            RuleResult result = new RuleResult();
            result.setMatched(matched != null && matched);
            result.setRuleId(rule.getRuleId());

            if (matched != null && matched && rule.getAction() != null) {
                // 执行动作表达式
                Expression actionExp = expressionCache.computeIfAbsent(
                    "action_" + rule.getRuleId(),
                    id -> parser.parseExpression(rule.getAction())
                );
                Object actionResult = actionExp.getValue(evalContext);
                result.setActionResult(actionResult);
            }

            return result;
        } catch (Exception e) {
            return RuleResult.fail(rule.getRuleId(), e.getMessage());
        }
    }

    private boolean isRuleValid(Rule rule) {
        if (!Boolean.TRUE.equals(rule.getEnabled())) {
            return false;
        }
        LocalDateTime now = LocalDateTime.now();
        if (rule.getStartTime() != null && now.isBefore(rule.getStartTime())) {
            return false;
        }
        if (rule.getEndTime() != null && now.isAfter(rule.getEndTime())) {
            return false;
        }
        return true;
    }
}

SpEL语法详解

1. 基础操作符

// 关系操作符
"orderAmount > 100"
"userLevel >= 2"
"status == 'ACTIVE'"

// 逻辑操作符  
"orderAmount > 100 && userLevel >= 2"
"vipUser || orderAmount > 500"
"!(blacklisted)"

// 算术操作符
"orderAmount * 0.9"
"quantity + bonusQuantity"
"total / count"

2. 方法调用

// 字符串方法
"userName.length() > 5"
"email.contains('@')"
"status.toUpperCase() == 'ACTIVE'"

// 数学方法
"T(Math).max(orderAmount, minAmount)"
"T(Math).round(total * 100) / 100.0"

// 自定义方法(需要注册)
// 在EvaluationContext中注册方法
evalContext.registerFunction("calculateDiscount", 
    DiscountService.class.getDeclaredMethod("calculate", Double.class, Integer.class));
// 使用:"#calculateDiscount(orderAmount, userLevel)"

3. 集合操作

// 列表操作
"items.size() > 0"
"items.?[price > 100].size() > 0"  // 过滤价格>100的商品
"items.![price].sum() > 500"       // 计算所有商品价格总和

// Map操作
"userRoles['admin'] == true"
"permissions.containsKey('write')"

4. 变量和属性访问

// 直接访问变量(通过setVariable设置)
"#orderAmount > 100"
"#user.level >= 2"

// 对象属性访问
"#order.amount > 100"
"#user.profile.vipLevel >= 3"

5. 实际规则示例

// 营销规则
条件: "#orderAmount >= 100 && #userLevel >= 2"
动作: "#orderAmount * 0.9"

// 风控规则  
条件: "#loginAttempts > 5 || #ipAddress in {'192.168.1.100', '10.0.0.50'}"
动作: "BLOCK"

// 定价规则
条件: "#product.category == 'electronics' && #user.vipLevel >= 3"
动作: "#basePrice * 0.85"

SpEL高级配置

@Configuration
public class SpelConfig {

    @Bean
    public SpelRuleEngine spelRuleEngine() {
        SpelRuleEngine engine = new SpelRuleEngine();
        
        // 配置自定义函数
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.registerFunction("now", System.class.getMethod("currentTimeMillis"));
        context.registerFunction("formatDate", 
            DateTimeFormatter.class.getMethod("ofPattern", String.class));
        
        // 配置类型转换器
        SimpleTypeConverter typeConverter = new SimpleTypeConverter();
        typeConverter.registerCustomEditor(LocalDateTime.class, new LocalDateTimeEditor());
        context.setTypeConverter(typeConverter);
        
        return engine;
    }
}

5. 规则管理后台

┌─────────────────────────────────────────┐
│           规则管理后台                    │
├─────────────────────────────────────────┤
│ 规则分组:□ 风控 □ 营销 □ 定价           │
│                                         │
│ 规则名称:满减规则                        │
│ 条件:orderAmount >= 100 && userLevel >= 1 │
│ 动作:orderAmount * 0.9                  │
│ 优先级:10                               │
│ 状态:☑ 启用   ☐ 定时生效                │
│                                         │
│        [保存]  [测试]  [发布]            │
└─────────────────────────────────────────┘

测试面板:
输入:{"orderAmount": 200, "userLevel": 2}
结果:✓ 匹配,执行动作 discount=180.0

6. 注意事项

  1. 规则冲突:同类型规则按优先级取最高,或全部执行后合并结果
  2. 规则循环:避免规则A触发B、B又触发A的死循环
  3. 性能问题:规则多时使用索引、分组加载,避免全量加载
  4. 规则回滚:规则变更前先备份,出问题可快速回滚
  5. 规则审计:记录规则变更历史,谁改了什么、什么时候改的
  6. SpEL安全:避免用户输入直接作为SpEL表达式,防止代码注入

7. 选型建议

  • Spring Boot项目 + 简单规则:优先选择 SpEL(零依赖、易集成)
  • 高性能要求 + 简单表达式:选择 Aviator(编译缓存、执行快)
  • 复杂业务规则 + 企业级应用:选择 Drools(功能完整、生态成熟)
  • 轻量级需求 + 快速开发:选择 EasyRules(注解驱动、简单直观)

最后更新:2026/05/13