拒絕if-else!小而美的規則引擎 Easy Rules 真不錯!
Easy Rules是一個簡單而強大的Java規則引擎,提供以下功能:
- 輕量級框架和易于學習的API
- 基于POJO的開發與注解的編程模型
- 定義抽象的業務規則并輕松應用它們
- 支持從簡單規則創建組合規則的能力
- 支持使用表達式語言(如MVEL和SpEL)定義規則的能力
為何選擇Easy Rules規則引擎
1. 傳統if - else編程的困境
案例一:電商滿減規則頻繁變更(真實生產場景重現)
在電商業務的實際運營中,促銷規則的頻繁變更是一個常見的問題。假設某電商平臺有如下促銷規則:
// 傳統硬編碼方式(噩夢般的代碼片段)
if(user.isVip()){
if(order.getAmount() > 200){
if(order.getItems().stream().anyMatch(i -> i.isPromotion())){
order.applyDiscount(0.8); // 會員滿200且含促銷商品打8折
}
} else if(order.getCreateTime().isAfter(LocalDate.of(2023,11,1))){
order.applyDiscount(0.9); // 雙十一期間會員專屬9折
}
} else {
// 普通用戶規則嵌套層級更深...
}
這種傳統的硬編碼方式存在諸多痛點:
- 維護困難:每當市場部調整規則時,開發者需要在大量的代碼中艱難地尋找邏輯修改點,這不僅效率低下,還容易出錯。
- 發版風險高:發版頻率極高,可能一個月需要進行6次規則修改和上線操作,每次上線都伴隨著一定的風險,如代碼沖突、功能異常等。
- 協作問題:在多人協作開發時,由于代碼結構復雜,很容易引發代碼沖突,增加了開發和維護的難度。
案例二:物聯網設備告警條件嵌套難題
在物聯網設備監控系統中,復雜的告警條件嵌套也是一個常見的問題。某工廠設備監控系統需要進行如下判斷:
if(temperature > 50 || humidity > 80) {
if(pressure < 100 && vibration > 5) {
if(deviceStatus != Status.MAINTENANCE) {
triggerAlarm(AlarmLevel.CRITICAL);
}
}
} else if (runtimeHours > 1000 && !isMaintained) {
triggerAlarm(AlarmLevel.WARNING);
}
// 后續還有8個else if...
這種代碼結構帶來了以下問題:
- 調試困難:在調試過程中,斷點需要穿透10層條件判斷,調試難度極大,耗費大量時間。
- 擴展性差:當需要新增“電壓波動 > 10%”這樣的條件時,需要重構整個邏輯,開發成本高。
- 知識傳遞困難:交接文檔需要繪制3頁流程圖才能清晰說明規則邏輯,給知識傳遞帶來了很大的困難。
可視化對比(代碼量的顯著優化)
代碼量對比
2. 輕量級規則引擎的優勢
場景化演示:從復雜到簡潔的轉變
場景轉變
核心優勢解析
- 解耦的智慧
規則與業務分離:規則與業務代碼實現物理隔離,可以將規則存儲在獨立文件或數據庫中,使代碼結構更加清晰。這樣,業務代碼專注于業務邏輯的處理,而規則代碼則負責規則的定義和管理。
動態加載規則:修改規則無需重新編譯部署,支持動態加載規則。以下是一個動態加載規則的示例:
public void refreshRules() {
List<Rule> newRules = ruleLoader.loadFromDB(); // 從數據庫讀取最新規則
rulesEngine.fire(new Rules(newRules), facts);
}
- 可讀性的提升
自描述性規則:規則具有自描述性,每個規則都可以看作是一個獨立的文檔,便于理解和維護。開發者可以通過規則的名稱、描述和條件等信息,快速了解規則的用途和邏輯。
決策流程可視化:支持決策流程可視化,可以自動生成規則關系圖。例如:
[用戶類型] --> [VIP規則] --> [折扣計算]
\-> [普通用戶規則] --> [滿減計算]
- 擴展性的保障
零侵入式擴展:新增規則對現有代碼零侵入,只需添加新的Rule類即可。這使得系統的擴展性得到了極大的提升,開發者可以根據業務需求隨時添加新的規則。
多規則源支持:支持混合多種規則源,例如數據庫、YAML文件和注解。以下是一個YAML規則文件的示例:
# discount_rule.yml
name: "老用戶回饋規則"
description: "注冊超過3年的用戶額外折扣"
condition: "user.registerYears >= 3"
actions:
- "order.applyAdditionalDiscount(0.95)"
定義規則
大多數業務規則可以由以下定義表示:
- 名稱:規則命名空間中的唯一規則名稱
- 說明:規則的簡要說明
- 優先級:相對于其他規則的規則優先級
- 事實:去匹配規則時的一組已知事實
- 條件:為了匹配該規則,在給定某些事實的情況下應滿足的一組條件
- 動作:當條件滿足時要執行的一組動作(可以添加/刪除/修改事實)
Easy Rules為定義業務規則的每個關鍵點提供了抽象。
在Easy Rules中,一個規則由Rule
接口表示:
public interface Rule {
/**
* 改方法封裝規則的條件(conditions)
* @return 如果提供的事實適用于該規則返回true, 否則,返回false
*/
boolean evaluate(Facts facts);
/**
* 改方法封裝規則的操作(actions)
* @throws 如果在執行過程中發生錯誤將拋出Exception
*/
void execute(Facts facts) throws Exception;
//Getters and setters for rule name, description and priority omitted.
}
evaluate方法封裝了必須求值為TRUE才能觸發規則的條件。
execute方法封裝了在滿足規則條件時應執行的操作。條件和動作Condition
andAction
接口表示。
規則可以用兩種不同的方式定義:
- 通過在POJO上添加注釋,以聲明方式定義
- 通過RuleBuilder API,以編程方式定義
用注解定義規則
這些是定義規則的最常用方法,但如果需要,還可以實現Rule
i接口或繼承BasicRule
類。
@Rule(name = "my rule", description = "my rule description", priority = 1)
publicclass MyRule {
@Condition
public boolean when(@Fact("fact") fact) {
//my rule conditions
returntrue;
}
@Action(order = 1)
public void then(Facts facts) throws Exception {
//my actions
}
@Action(order = 2)
public void finally() throws Exception {
//my final actions
}
}
@Condition注解標記計算規則條件的方法。此方法必須是公共的,可以有一個或多個用@Fact注解的參數,并返回布爾類型。只有一個方法能用@Condition注解。
@Action注解標記要執行規則操作的方法。規則可以有多個操作。可以使用order屬性按指定的順序執行操作。默認情況下,操作的順序為0。
2. 用RuleBuilder API定義規則
Rule rule = new RuleBuilder()
.name("myRule")
.description("myRuleDescription")
.priority(3)
.when(condition)
.then(action1)
.then(action2)
.build();
在這個例子中, Condition實例condition,Action實例是action1和action2。
定義事實
Facts API是一組事實的抽象,在這些事實上檢查規則。在內部,Facts實例持有HashMap<String,Object>,這意味著:
- 事實需要命名,應該有一個唯一的名稱,且不能為空
- 任何Java對象都可以充當事實
這里有一個實例定義事實:
Facts facts = new Facts();
facts.add("rain", true);
Facts 能夠被注入規則條件,action 方法使用 @Fact
注解. 在下面的規則中,rain
事實被注入itRains方法的rain
參數:
@Rule
class WeatherRule {
@Condition
public boolean itRains(@Fact("rain") boolean rain) {
return rain;
}
@Action
public void takeAnUmbrella(Facts facts) {
System.out.println("It rains, take an umbrella!");
// can add/remove/modify facts
}
}
Facts
類型參數 被注入已知的 facts中 (像action方法takeAnUmbrella
一樣).
如果缺少注入的fact, 這個引擎會拋出 RuntimeException
異常.
定義規則引擎
從版本3.1開始,Easy Rules提供了RulesEngine接口的兩種實現:
- DefaultRulesEngine:根據規則的自然順序(默認為優先級)應用規則。
- InferenceRulesEngine:持續對已知事實應用規則,直到不再應用規則為止。
創建一個規則引擎
要創建規則引擎,可以使用每個實現的構造函數:
RulesEngine rulesEngine = new DefaultRulesEngine();
// or
RulesEngine rulesEngine = new InferenceRulesEngine();
然后,您可以按以下方式觸發注冊規則:
rulesEngine.fire(rules, facts);
規則引擎參數
Easy Rules 引擎可以配置以下參數:
圖片
skipOnFirstAppliedRule
:告訴引擎規則被觸發時跳過后面的規則。skipOnFirstFailedRule
:告訴引擎在規則失敗時跳過后面的規則。skipOnFirstNonTriggeredRule
:告訴引擎一個規則不會被觸發跳過后面的規則。rulePriorityThreshold
:告訴引擎如果優先級超過定義的閾值,則跳過下一個規則。版本3.3已經不支持更改,默認MaxInt。
可以使用RulesEngineParameters API指定這些參數:
RulesEngineParameters parameters = new RulesEngineParameters()
.rulePriorityThreshold(10)
.skipOnFirstAppliedRule(true)
.skipOnFirstFailedRule(true)
.skipOnFirstNonTriggeredRule(true);
RulesEngine rulesEngine = new DefaultRulesEngine(parameters);
如果要從引擎獲取參數,可以使用以下代碼段:
RulesEngineParameters parameters = myEngine.getParameters();
這允許您在創建引擎后重置引擎參數。
5分鐘極速入門(Hello World版)
1. 環境搭建(手把手教學)
為什么選擇Maven依賴?Easy Rules的核心庫僅有 217KB,不會造成項目臃腫。只需在pom.xml
中添加:
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>4.1.0</version>
</dependency>
驗證是否成功: 在IDE中新建RulesEngine engine = new DefaultRulesEngine();
若無報錯,則環境配置成功!
2. 第一個規則實戰(帶逐行解析)
場景背景: 假設我們正在開發智能家居系統,需要根據濕度傳感器數據觸發雨天提醒。
代碼深度解讀:
圖片
執行過程全解:
public static void main(String[] args) {
// 模擬傳感器數據(真實項目從MQTT獲取)
Facts facts = new Facts(); // 事實對象(數據容器)
facts.put("humidity", 85); // 放入濕度值
// 創建規則引擎(核心控制器)
RulesEngine engine = new DefaultRulesEngine();
// 裝載規則并執行(點火!)
engine.fire(new Rules(new RainRule()), facts);
// 執行結果:
// 【智能家居】檢測到濕度85%,建議關閉窗戶帶傘出門!
}
3. 可視化規則執行流程(小白秒懂版)
完整執行鏈路圖示:
圖片
關鍵點提醒:
- 一個Facts對象可承載多個數據:
facts.put("temperature", 28);
facts.put("location", "上海");
- 多個規則會按優先級順序執行(默認優先級=0)
- 使用
@Priority
注解調整執行順序:
@Rule(priority = 1) // 數字越大優先級越高
新手常見問題QA:規則沒觸發怎么辦?
- 檢查
@Fact
名稱是否與put時一致 - 確認
@Condition
方法返回true - 添加日志打印調試:
@Action
public void remind() {
System.out.println("規則觸發!"); // 先確認是否執行到此
}
如何同時處理多個規則?
// 一次性加載多個規則
Rules rules = new Rules(new RainRule(), new TempRule(), new WindRule());
engine.fire(rules, facts);
需要我展示如何擴展這個案例,比如增加溫度規則形成組合條件嗎?比如"濕度>80% 且 溫度>30℃"觸發高溫高濕預警?
6大經典場景深度解析
場景1:電商促銷系統(組合優惠精算)
案例3進階實現:VIP折扣與滿減疊加計算
避坑指南:
- 使用
@Priority
控制執行順序(數值越大越先執行) - 折扣計算需采用乘法疊加而非減法,避免出現0元訂單
- 在動作中增加日志記錄,審計實際優惠金額
場景2:物聯網報警系統(多級聯動)
案例4優化版:帶設備狀態判斷的三級報警
圖片
實戰技巧:
- 設備維護狀態作為獨立Fact傳遞
- 優先處理高風險規則(priority=3)
- 動作中集成多種通知渠道(短信/郵件/看板)
場景3:會員等級系統(混合規則源)
案例5增強方案:YAML+注解混合使用
圖片
集成方法:
// 加載YAML規則
RulesLoader loader = new YamlRuleLoader();
Rules yamlRules = loader.load(new File("promotion_rules.yml"));
// 加載注解規則
Rules annoRules = new Rules(new ShareRule());
// 合并執行
engine.fire(yamlRules, facts);
engine.fire(annoRules, facts);
場景4:工單分配系統(動態派單)
案例6增強版:基于值班表的動態分配
@Rule(name = "技術緊急工單")
publicclass TechEmergencyRule {
@Condition
public boolean isTechEmergency(
@Fact("ticket") Ticket ticket,
@Fact("dutyTable") DutyTable table) {
return ticket.getType() == TECH
&& ticket.getPriority() == HIGH
&& table.hasAvailableTechLead();
}
@Action
public void assignToTechLead() {
String techLead = dutyTable.getCurrentTechLead();
ticket.setAssignee(techLead);
dutyTable.markBusy(techLead); // 標記為忙碌狀態
}
}
設計亮點:
- 值班表作為獨立Fact,實時反映工程師狀態
- 自動標記工程師忙碌狀態,避免重復分配
- 可擴展支持輪詢、負載均衡等分配策略
場景5:風控預警系統(時序檢測)
案例7優化版:時間窗口滑動檢測
圖片
性能優化:
- 使用
@Fact
注入預處理的時序數據 - 采用BloomFilter快速過濾低風險設備
- 異步執行風險處理動作
場景6:游戲戰斗系統(狀態管理)
案例8增強版:連招技能狀態機
@Rule(name = "龍卷風連擊")
publicclass TornadoComboRule {
@Condition
public boolean checkComboSequence(
@Fact("queue") CircularFifoQueue<Skill> queue) {
return queue.size() >=3
&& queue.get(0) == Skill.A
&& queue.get(1) == Skill.B
&& queue.get(2) == Skill.C;
}
@Action
public void releaseSuperSkill() {
player.cast(Skill.SUPER_TORNADO);
queue.clear(); // 清空連招隊列
effectPlayer.play("combo_success.wav");
}
}
注意事項:
- 使用Apache Commons的CircularFifoQueue控制隊列長度
- 動作中重置狀態避免重復觸發
- 集成音效/特效等游戲元素
架構師擴展包:
- 規則模板技術:
public abstract class BasePromotionRule implements Rule {
@Condition
public abstract boolean matchCondition(Order order);
@Action
public void applyDiscountTemplate(@Fact("order") Order order) {
order.applyDiscount(getDiscountRate());
log.info("應用{}折扣", getRuleName());
}
protected abstract double getDiscountRate();
}
- 規則性能監控:
engine.registerRuleListener(new RuleListener() {
public void beforeExecute(Rule rule, Facts facts) {
Monitor.startTimer(rule.getName());
}
public void afterExecute(Rule rule, Facts facts) {
long cost = Monitor.stopTimer(rule.getName());
if(cost > 100) {
alertSlowRule(rule.getName(), cost);
}
}
});
Spring Boot集成
配置自動加載:
@Configuration
publicclass RuleEngineConfig {
@Bean
public RulesEngine rulesEngine() {
returnnew DefaultRulesEngine(
new Parameters()
.skipOnFirstNonTriggeredRule(true)
.priorityThreshold(10)
);
}
@Bean
public Rules ruleRegistry() throws IOException {
// 自動掃描帶@Rule注解的Bean
returnnew Rules(
new AnnotationRuleFactory().create(
new ClasspathRuleDefinitionReader(),
new ClassPathResource("rules/").getFile()
)
);
}
@Bean
public ApplicationRunner ruleInitializer(RulesEngine engine, Rules rules) {
return args -> {
// 啟動時預加載驗證規則
engine.fire(rules, new Facts());
logger.info("已成功加載{}條規則", rules.size());
};
}
}
在controller中測試:
@RestController
publicclass PromotionController {
@Autowired
private RulesEngine rulesEngine;
@Autowired
private Rules rules;
@PostMapping("/apply-rules")
public Order applyRules(@RequestBody Order order) {
Facts facts = new Facts();
facts.put("order", order);
rulesEngine.fire(rules, facts);
return order;
}
}
在生產中我們還可以將規則配置設置為熱更新,以@RefreshScope
+ Spring Cloud Config的方式,這樣在配置更新時會自動加載。
總結
Easy Rules 非常適合需要快速實現業務規則引擎的場景。對于中小型項目,Easy Rules 的簡單性和靈活性是一大優勢。
如果項目規則復雜或者性能要求較高,可以考慮結合 Drools 等更強大的規則引擎使用。