4. 复杂审批规则引擎:规则引擎选型与动态配置

说到审批流里的复杂规则,我见过不少团队一开始用硬编码写if-else,结果业务一变更,代码改得想哭。其实,规则引擎就是专门干这个的——把业务规则从代码里抽出来,让它们变成可配置、可热加载的独立模块。

这一章,咱们就聊聊规则引擎的选型、动态配置、热加载,还有那个容易踩坑的冲突检测。

4.1 规则引擎选型:Drools vs EasyRules

选型这事儿,说白了就是看你的场景有多复杂。我个人的经验是:别一上来就上重型引擎,也别啥都手写。

对比维度 Drools EasyRules
规则复杂度 支持复杂推理、前向链、后向链 适合简单条件-动作规则
学习曲线 陡峭,需要理解DRL语法 平缓,纯Java注解即可
性能 Rete算法优化,适合大量规则 线性匹配,规则少时够用
热加载 支持,但需处理KieSession 简单,重新加载Rules即可
适用场景 风控、医疗诊断、复杂审批 简单审批、配置校验、小规则集

我在项目中遇到过这样一个情况:一个OA审批系统,刚开始只有十几条规则,用EasyRules跑得挺欢。结果业务扩张到几百条规则,还涉及多条件交叉判断,EasyRules就开始吃力了。后来迁移到Drools,虽然初期配置麻烦点,但Rete算法的效率确实高。

我的建议:

  • 规则少于50条,且逻辑简单 → EasyRules
  • 规则超过100条,或需要复杂推理 → Drools
  • 团队有Java功底但没时间学DRL → EasyRules
  • 需要规则版本管理、冲突检测 → Drools

4.2 动态规则配置:让业务人员也能改规则

动态配置的核心,就是把规则存到数据库或配置中心,而不是写在代码里。嗯,这里要注意:规则的结构设计决定了后续的灵活性。

我常用的做法是设计一个规则配置表:

-- 规则配置表
CREATE TABLE approval_rule (
  id BIGINT PRIMARY KEY,
  rule_name VARCHAR(100),
  rule_condition TEXT,   -- 条件表达式,如 "amount > 10000"
  rule_action TEXT,      -- 动作,如 "route_to:VP"
  priority INT,          -- 优先级,数字越小越优先
  status VARCHAR(10),    -- ENABLED / DISABLED
  version INT,
  create_time DATETIME
);

然后,在Java里用SPEL或MVEL解析这些条件。举个例子:

// 使用MVEL解析动态条件
public boolean evaluateCondition(String condition, Map<String, Object> facts) {
    return MVEL.evalToBoolean(condition, facts);
}

// 调用示例
Map<String, Object> facts = new HashMap<>();
facts.put("amount", 15000);
facts.put("department", "Finance");
boolean result = evaluateCondition("amount > 10000 && department == 'Finance'", facts);
// 返回 true

避坑指南:我曾经把规则条件直接存成字符串,结果业务人员写了个语法错误的条件,整个审批流崩了。后来我加了一层语法校验,在保存规则时先用MVEL预编译一次,编译失败直接报错。

4.3 规则热加载:不停机更新规则

热加载,说白了就是让规则在运行时更新,不用重启应用。你想想看,一个OA系统白天几百人在用,你为了改一条规则重启服务,那不得被骂死?

实现热加载,我一般用两种方式:

  • 轮询方式:每隔30秒检查数据库规则版本号,有变化就重新加载。
  • 事件通知:用Redis的Pub/Sub或消息队列,规则变更时主动通知应用。

这里给个Drools热加载的示例:

public class DroolsRuleLoader {
    private KieContainer kieContainer;
    private volatile long lastLoadTime = 0;

    public void reloadIfNeeded() {
        // 检查数据库中的规则版本
        long latestVersion = ruleRepository.getLatestVersion();
        if (latestVersion > lastLoadTime) {
            synchronized (this) {
                // 重新构建KieContainer
                KieServices ks = KieServices.Factory.get();
                KieFileSystem kfs = ks.newKieFileSystem();
                
                // 从数据库加载规则内容
                List<Rule> rules = ruleRepository.findAllEnabled();
                for (Rule rule : rules) {
                    String drlContent = buildDrl(rule);
                    kfs.write("src/main/resources/" + rule.getId() + ".drl", 
                              drlContent);
                }
                
                KieBuilder kb = ks.newKieBuilder(kfs).buildAll();
                this.kieContainer = ks.newKieContainer(kb.getKieModule().getReleaseId());
                this.lastLoadTime = latestVersion;
                
                System.out.println("规则已热加载,版本时间: " + latestVersion);
            }
        }
    }
}

注意:热加载时,正在执行的审批实例怎么办?我建议用版本号隔离——旧实例继续用旧规则,新实例用新规则。别一刀切,否则审批到一半规则变了,业务人员会疯的。

4.4 冲突检测:别让规则打架

规则多了,难免有冲突。比如一条规则说“金额大于1万转给总监”,另一条说“金额大于5万转给CEO”。那3万块钱的审批到底给谁?

冲突检测,我把它分成两类:

  • 直接冲突:相同条件,不同动作。比如条件都是“amount > 10000”,一个动作是“route_to:Manager”,另一个是“route_to:VP”。
  • 间接冲突:条件有包含关系,动作不同。比如“amount > 10000”和“amount > 50000”,动作不同。

检测方法其实不复杂:

public class ConflictDetector {
    
    public List<Conflict> detectConflicts(List<Rule> rules) {
        List<Conflict> conflicts = new ArrayList<>();
        
        for (int i = 0; i < rules.size(); i++) {
            for (int j = i + 1; j < rules.size(); j++) {
                Rule r1 = rules.get(i);
                Rule r2 = rules.get(j);
                
                // 检查条件是否重叠
                if (conditionsOverlap(r1.getCondition(), r2.getCondition())) {
                    // 检查动作是否不同
                    if (!r1.getAction().equals(r2.getAction())) {
                        conflicts.add(new Conflict(r1, r2, "条件重叠但动作不同"));
                    }
                }
            }
        }
        return conflicts;
    }
    
    private boolean conditionsOverlap(String cond1, String cond2) {
        // 这里可以用SAT求解器或简单的区间检测
        // 简单场景:检查数值范围是否有交集
        // 复杂场景:用Z3或Drools的冲突检测API
        return true; // 简化示例
    }
}

我的经验:冲突检测别全自动化,容易误报。我一般做法是:

  1. 保存规则时自动检测,列出潜在冲突
  2. 让业务人员手动确认是否真的冲突
  3. 如果确认冲突,用优先级解决——优先级高的规则胜出

这样既避免了漏检,又不会因为误报影响效率。

4.5 实战:一个完整的审批规则引擎示例

最后,我整合一下上面的思路,给个完整的示例。假设我们要实现一个费用报销审批规则引擎:

// 规则引擎核心类
public class ApprovalRuleEngine {
    
    private RuleRepository ruleRepository;
    private Map<String, RuleExecutor> executors = new ConcurrentHashMap<>();
    
    public ApprovalResult evaluate(ApprovalRequest request) {
        // 1. 加载当前生效的规则
        List<Rule> rules = ruleRepository.findByStatus("ENABLED");
        
        // 2. 按优先级排序
        rules.sort(Comparator.comparingInt(Rule::getPriority));
        
        // 3. 冲突检测(可选,生产环境建议异步做)
        // conflictDetector.detectConflicts(rules);
        
        // 4. 匹配规则
        for (Rule rule : rules) {
            if (matchCondition(rule.getCondition(), request)) {
                // 5. 执行动作
                return executeAction(rule.getAction(), request);
            }
        }
        
        // 6. 默认路由
        return ApprovalResult.defaultRoute();
    }
    
    private boolean matchCondition(String condition, ApprovalRequest request) {
        // 使用MVEL或SPEL解析条件
        Map<String, Object> context = new HashMap<>();
        context.put("amount", request.getAmount());
        context.put("department", request.getDepartment());
        context.put("level", request.getApplicantLevel());
        return MVEL.evalToBoolean(condition, context);
    }
}

这个引擎看起来简单,但实际生产环境里,我还会加上规则缓存、执行日志、性能监控。毕竟,规则引擎一旦出问题,整个审批流就瘫了。

最后说一句:规则引擎不是银弹。如果你的规则逻辑极其简单,或者变更频率极低,用硬编码反而更稳。别为了用技术而用技术,这是我在项目里摔过跟头才明白的。