引言
Rule-Engine 是一個專家系統程式,它對輸入資料執行規則,假如任何前提匹配,它就會執行相應的操縱,市場上有很多可用的規則引擎實現。但是,假如您想建立自己的簡樸規則引擎來滿意您的大部分要求,那麼我們開始吧。它可能無法滿意所有要求,但您可以瞭解規則引擎的實現,並可以根據需要對其進行修改。
技術棧
我們將使用以下技術棧:
SpringBoot 2.0.5.RELEASE:將規則引擎專案建立為 rest API。Maven:專案框架MVEL 2.4:編寫規則的表示式語言。PostgreSQL 9.4 資料庫:用於儲存規則。Spring JPA:用於資料庫連線
架構圖

我們按照上面的實現框架,一步步實現Rule-Engine。
我們使用 SpringBoot 和 Maven 框架將規則引擎實現為 Rest API。了編寫規則,我們使用表示式語言 (MVEL) 和一些特定領域的關鍵字我們將規則儲存在資料庫 (pg) 中我們實現了一個抽象推理引擎,用於實現不同領域特定的推理引擎。例如,我們可以在一個應用程式中同時編寫貸款和保險相關的規則引擎。我們實施關鍵字解析器來解析所有特定於域的關鍵字。我們實現了兩個解析器。一種用於解析 MVEL 表示式,另一種用於 DSL。我們使用postman進行 API 測試的測試。
1.規則語言
規則是一組前提,後跟一組操縱。規則主要以if-then形式表示。為了編寫規則,我們使用 MVEL expression language + Domain Specific Language 。則的語言 = MVEL + DSLDSL 或領域特定語言意味著,它不是 MVEL 的一部分。它是由我們自己為特定領域建立的。代替編寫多個規則或表示式,我們可以將它們組合成一個,並可以使用關鍵字進行對映。 DSL 關鍵字使其易於使用和可重用。例如,要批准任何人的貸款,是否必需檢查銀行當年的目標是否完成?那麼,為了在規則中提及它,我們可能需要在 MVEL 中編寫多個語句或表示式。但是,假如我們建立一個關鍵字,如“bank.target_done”,它將返回一個簡樸的 true 或 false。在這個關鍵字後面,我們進行所有計算以獲得當年的目標狀態。現在我們可以在編寫規則的任何地方使用它。在此規則引擎中,我們對 DSL 使用以下格局,這與 MVEL 不同。$(resolver-keyword.sub-keyword)Resolver 關鍵字可以看作是所有子關鍵字的鍵空間。例如,bank 是一個解析器關鍵字,所有與銀行相關的關鍵字,如 interest_rate、targe_done 或 branch 等都是子關鍵字,經由一些計算後返回一些值。此格局用於將所有相同的相關關鍵字組合在一起。
例子
假設我們的需求是這樣的,我們必需為貸款申請建立一個規則引擎,以下是批准貸款的規則。
Domain: LoanRule 1: A person is eligible for home loan?if: 1. He has monthly salary more than 50K. 2. And his credit score is more than 800. 3. And requested loan amount is less than 40L. 4. And bank"s current year target not done for home loan.then: 1. Approve the home loan. 2. Sanction 80% of requested loan amount.Rule 2: A person is eligible for home loan?if: 1. He has monthly salary more than 35K and less than 50K. 2. And his credit score is less than 500. 3. And requested loan amount is less than 20L. 4. And bank"s current year target not done for home loan.then: 1. Approve home loan. 2. Sanction 60% of requested loan amount.
讓我們將這些規則以 MVEL + DSL 的形式轉換為儲存在 DB 中。
MVEL + DSL形式的規則:
#Loan Rule 1:Condition: input.monthlySalary >= 50000 && input.creditScore >= 800 && input.requestedLoanAmount < 4000000 && $(bank.target_done) == falseAction: output.setApprovalStatus(true); output.setSanctionedPercentage(90); output.setProcessingFees(8000);#Loan Rule 2:Condition: input.monthlySalary >= 35000 && input.monthlySalary <= 50000 && input.creditScore <= 500 && input.requestedLoanAmount < 2000000 && $(bank.target_done) == falseAction: output.setApprovalStatus(true); output.setSanctionedPercentage(60); output.setProcessingFees(2000);
在此示例中, $(bank.target_done) 是特定於域的關鍵字。這裡將解決並返回有關貸款的銀行狀態,今年的住房貸款目標是否完成(真/假)? 除此以外,其他都是MVEL的表達形式。另外,這裡的 input 和 output 關鍵字代表兩個 java 物件。輸入物件作為輸入資料傳遞給規則引擎,並根據該資料執行所有貸款規則。而最後,它會以輸出物件的形式返回結果。在這個例子中,我們有 UserDetails 是一個輸入物件,LoanDetails 是一個輸出結果物件。

Input Data:
public class UserDetails { Double monthlySalary; Integer creditScore; Double requestedLoanAmount; //Few other variables}
Output Result:
public class LoanDetails { Boolean approvalStatus; Float sanctionedPercentage; Double processingFees; //Few other variables}
2. 在資料庫中儲存規則
以後將這些規則寫成MVEL+DSL的形式,儲存在Postgres DB中。您可以根據您的可行性使用任何其他資料庫。
假如您已經擁有 Postgres,則建立資料庫 rulebase 並在其中建立一個名為 rules 的表。在這個表中,我們提到了幾個類似column的priority,rule id等詳細使用(見第4節:“建立Rule的模型類”)。
CREATE DATABASE rulebase;CREATE TABLE rules ( rule_namespace varchar(256) not null, rule_id varchar(512) not null, condition varchar(2000), action varchar(2000), priority integer, description varchar(1000), PRIMARY KEY(rule_namespace, rule_id));
現在藉助以下查詢將上述兩條規則插入到 Postgres 資料庫中。
LOAN RULE 1INSERT INTO rules (rule_namespace , rule_id, condition, action, priority, description) VALUES ( "LOAN", "1", "input.monthlySalary >= 50000 && input.creditScore >= 800 && input.requestedLoanAmount < 4000000 && $(bank.target_done) == false", "output.setApprovalStatus(true);output.setSanctionedPercentage(90);output.setProcessingFees(8000);", "1", "A person is eligible for Home loan?");LOAN RULE 2INSERT INTO rules (rule_namespace , rule_id, condition, action, priority, description) VALUES ( "LOAN", "2", "input.monthlySalary >= 35000 && input.monthlySalary <= 50000 && input.creditScore <= 500 && input.requestedLoanAmount < 2000000 && $(bank.target_done) == false","output.setApprovalStatus(true);output.setSanctionedPercentage(60);output.setProcessingFees(2000);", "2", "A person is eligible for Home loan?");

3.建立一個Maven專案
首先建立一個maven工程,在pom檔案中新增如下spring-boot、Postgres、MVEL等依靠。
parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version></parent><properties> <java.version>1.8</java.version> <junit.version>4.12</junit.version> <postgresql.version>9.4-1206-jdbc42</postgresql.version> <mvel.version>2.4.4.Final</mvel.version></properties><dependencies> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency><dependency> <groupId>org.mve</groupId> <artifactId>mvel2</artifactId> </dependency><dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> </dependency></dependencies>
4.為Rule建立模型類
基本上,規則採用 if-then 形式。它基本上包含兩部門,前提和動作。但是我們必需定義更多的資訊來實現它。那些是:名稱空間:用於識別或將域的相同型別的規則合併為一個。Id:每個規則都可以用一些獨一的 Id 來標識。Action:MVEL+DSL形式的Rule動作。優先順序:它將用於決定應首先對輸入資料執行哪個規則。描述:顯示規則的描述。為了儲存所有這些資訊,我們使用規則模型。
public class Rule { RuleNamespace ruleNamespace; String ruleId; String condition; String action; Integer priority; String description;}
以列舉的形式描述規則的域(Rule namespace)
public enum RuleNamespace { LOAN, INSURANCE}
5.資料庫連線服務
對於資料庫連線,我們使用 Spring Data JPA。現在首先在資源目錄中新增 application.properties 並寫入 PostgreSql URL 和憑據。
spring.datasource.url=jdbc:postgresql://localhost:5432/rulebasespring.datasource.username=<your postgre username>spring.datasource.password=<your postgre password>spring.jpa.generate-ddl=true
現在建立資料庫模型和儲存庫以從 Postgres DB 訪問資料。為了實現它,我們使用 Spring Data JPA。為此,我們建立了一個儲存庫 RulesRepository 、資料庫模型 RuleDbModel 和服務 KnowledgeBaseService 。 DB Model:從 DB 獲取資料到 RuleDbModel 形式。
@Table(name = "rules")public class RuleDbModel {@Id@Column(name = "rule_namespace") private String ruleNamespace;@Column(name = "rule_id") private String ruleId;@Column(name = "condition") private String condition;@Column(name = "action") private String action;@Column(name = "priority") private Integer priority;@Column(name = "description") private String description;@Data static class IdClass implements Serializable { private String ruleNamespace; private String ruleId; }}
Repository:在 RulesRepository 類中定義資料庫查詢或函式。
@Repositorypublic interface RulesRepository extends JpaRepository<RuleDbModel, Long> { List<RuleDbModel> findByRuleNamespace(String ruleNamespace); List<RuleDbModel> findAll();}
知識庫服務:根據我們的要求定義訪問規則的方法。
@Servicepublic class KnowledgeBaseService { @Autowired private RulesRepository rulesRepository;public List<Rule> getAllRules(){ //See complete code on git. }public List<Rule> getAllRuleByNamespace(String ruleNamespace){ //See complete code on git. }}
6.DSL解析器:
要解析 DSL 相關關鍵字,我們需要實現一個關鍵字解析器。
DSL解析器介面:
public interface DSLResolver { String getResolverKeyword(); Object resolveValue(String keyword);}
該介面將由每個域特定的關鍵字來實現。例如,BankResolver 用於獲取當前利率和目標完成與否的資訊。 假設這裡的解析器關鍵字是 bank ,子關鍵字是 interest 和 target_done 。然後是 BankResolver 的程式碼:
public class BankResolver implements DSLResolver { private static final String RESOLVER_KEYWORD = "bank"; private static final String INTEREST = "interest"; private static final String TARGET_DONE = "target_done";@Override public String getResolverKeyword() { return RESOLVER_KEYWORD; }@Override public Object resolveValue(String keyword) { if (keyword.equalsIgnoreCase(INTEREST)){ //Code to calculate the current variable interest rates. return 9.0; }if (keyword.equalsIgnoreCase(TARGET_DONE)){ //Code to see the bank target of giving loan for this current year is done or not. return false; }return null; }}
DSL 關鍵字解析器:
我們編寫一個服務 DSLKeywordResolver 來解析所有已實現的關鍵字。這裡 getResolver(String keyword) 方法解析解析器關鍵字並返回該關鍵字解析器的物件引用(示例:BankResolver)。
public class DSLKeywordResolver { Map<String, DSLResolver> dslKeywordResolverList;@Autowired public DSLKeywordResolver(List<DSLResolver> resolverList) { dslKeywordResolverList = resolverList.stream() .collect(Collectors.toMap(DSLResolver::getResolverKeyword, Function.identity())); }public Optional<DSLResolver> getResolver(String keyword) { return Optional.ofNullable(dslKeywordResolverList.get(keyword)); }}
現在編寫 DSLParser,它採用給定的規則表示式並解析 DSL 關鍵字,並將規則表示式中的值替代為相應的關鍵字。
Example:
condition: “input.monthlySalary >= 50000 && input.creditScore >= 800 && input.requestedLoanAmount<4000000 && $(bank.target_done) == false”前提:“input.monthlySalary >= 50000 && input.creditScore >= 800 && input.requestedLoanAmount<4000000 && $(bank.target_done) == false”
TO
condition: “input.monthlySalary >= 50000 && input.cibilScore >= 800 && input.requestedLoanAmount<4000000 && false == false”前提:“input.monthlySalary >= 50000 && input.cibilScore >= 800 && input.requestedLoanAmount<4000000 && false == false”
public class DSLParser {@Autowired private DSLKeywordResolver keywordResolver;public String resolveDomainSpecificKeywords(String expression){ Map<String, Object> dslKeywordToResolverValueMap = executeDSLResolver(expression); return replaceKeywordsWithValue(expression, dslKeywordToResolverValueMap); }}
7. MVEL 解析器
解析 DSL 關鍵字後,表示式將佈滿 MVEL 表示式。現在我們建立一個 MVELParser 來解析這個 MVEL 表示式。
public class MVELParser {public boolean parseMvelExpression( String expression, Map<String, Object> inputObjects){ try { return MVEL.evalToBoolean(expression,inputObjects); }catch (Exception e){ log.error("Can not parse Mvel Expression : {} Error: {}", expression, e.getMessage()); } return false; }}
這裡的 inputObjects 包含輸入資料和輸出結果物件相對於輸入和輸出鍵的對映。
8. RuleParser
RuleParser 是 MVELParser 和 DSLParser 的包裝器。它用於解析規則的前提和操縱。前提對輸入資料執行,動作執行返回輸出結果。 規則解析器分兩步解析規則表示式: 步驟 1) 首先解析特定領域的關鍵字:$(resolver-keyword.sub-keyword) 步驟 2) 解析 MVEL 表示式。
public class RuleParser<INPUT_DATA, OUTPUT_RESULT> {@Autowired protected DSLParser dslParser; @Autowired protected MVELParser mvelParser;private final String INPUT_KEYWORD = "input"; private final String OUTPUT_KEYWORD = "output";public boolean parseCondition(String expression, INPUT_DATA inputData) { String resolvedDslExpression = dslParser.resolveDomainSpecificKeywords(expression); Map<String, Object> input = new HashMap<>(); input.put(INPUT_KEYWORD, inputData); boolean match = mvelParser.parseMvelExpression(resolvedDslExpression, input); return match; }public OUTPUT_RESULT parseAction(String expression, INPUT_DATA inputData, OUTPUT_RESULT outputResult) { String resolvedDslExpression = dslParser.resolveDomainSpecificKeywords(expression); Map<String, Object> input = new HashMap<>(); input.put(INPUT_KEYWORD, inputData); input.put(OUTPUT_KEYWORD, outputResult); mvelParser.parseMvelExpression(resolvedDslExpression, input); return outputResult; }}
9.推理機
推理機是規則引擎的核心部門。它主要分三步對輸入資料執行規則匹配:將事實/前提和資料與規則集進行匹配。它返回一組滿意的規則。RESOLVE:解決衝突的規則集,給出選中的一條規則。EXECUTE:對給定資料執行所選規則的操縱並返回結果輸出。
我們在推理引擎的抽象類中實現所有三個步驟或方法,並在執行方法中呼叫它們。
public abstract class InferenceEngine<INPUT_DATA, OUTPUT_RESULT> {@Autowired private RuleParser<INPUT_DATA, OUTPUT_RESULT> ruleParser;public OUTPUT_RESULT run (List<Rule> listOfRules, INPUT_DATA inputData){//STEP 1 MATCH List<Rule> conflictSet = match(listOfRules, inputData);//STEP 2 RESOLVE Rule resolvedRule = resolve(conflictSet); if (null == resolvedRule){ return null; }//STEP 3 EXECUTE OUTPUT_RESULT outputResult = executeRule(resolvedRule, inputData);return outputResult; }//Here we are using Linear matching algorithm for pattern protected List<Rule> match(List<Rule> listOfRules, INPUT_DATA inputData){ return listOfRules.stream() .filter( rule -> { String condition = rule.getCondition(); return ruleParser.parseCondition(condition, inputData); } ) .collect(Collectors.toList()); }//Here we are using find first rule logic. protected Rule resolve(List<Rule> conflictSet){ Optional<Rule> rule = conflictSet.stream() .findFirst(); if (rule.isPresent()){ return rule.get(); } return null; }protected OUTPUT_RESULT executeRule(Rule rule, INPUT_DATA inputData){ OUTPUT_RESULT outputResult = initializeOutputResult(); return ruleParser.parseAction(rule.getAction(), inputData, outputResult); }protected abstract OUTPUT_RESULT initializeOutputResult(); protected abstract RuleNamespace getRuleNamespace();}
這裡另外兩個抽象方法 initializeOutputResult() 和 getRuleNamespace() 在擴充套件的特定領域推理引擎中實現。 initializeOutputResult() 方法返回輸出結果物件的初始化引用。 getRuleNamespace() 方法返回該推理引擎的 RuleNamespace
示例:為了為貸款應用程式或域建立規則引擎,我們必需擴充套件 InferenceEngine 並且需要實現兩個抽象方法。對於此推理引擎,INPUT_DATA 將是 UserDetails 物件,而 OUTPUT_RESULT 將是 LoanDetails 物件。
public class LoanInferenceEngine extends InferenceEngine<UserDetails, LoanDetails> {@Override protected RuleNamespace getRuleNamespace() { return RuleNamespace.LOAN; }@Override protected LoanDetails initializeOutputResult() { return new LoanDetails(); }}
同樣,我們也可以為其他領域建立推理引擎。例如保險。
public class InsuranceInferenceEngine extends InferenceEngine<PolicyHolderDetails, InsuranceDetails> {@Override protected RuleNamespace getRuleNamespace() { return RuleNamespace.INSURANCE; }@Override protected LoanDetails initializeOutputResult() { return new InsuranceDetails(); }}
對於保險推理引擎,INPUT_DATA 將是 PolicyHolderDetails 物件,而 OUTPUT_RESULT 將是 InsuranceDetails 物件。
10. 規則引擎 REST API
到目前為止,大部分規則引擎的實現已經完成。現在我們必需建立一個 rest API 或控制器以將輸入作為 JSON 傳遞並從規則引擎獲取響應。這裡我們建立了兩個rest controller,一個 /get-all-rules 用於獲取所有規則,另一個 /loan 用於傳遞JSON格局的輸入資料物件並獲取規則執行的結果。
@RestControllerpublic class RuleEngineRestController {@GetMapping(value = "/get-all-rules") public ResponseEntity<?> getAllRules() { List<Rule> allRules = knowledgeBaseService.getAllRules(); return ResponseEntity.ok(allRules); }@PostMapping(value = "/loan") public ResponseEntity<?> runLoanRuleEngine(@RequestBody UserDetails userDetails) { LoanDetails result = (LoanDetails) ruleEngine.run(loanInferenceEngine, userDetails); return ResponseEntity.ok(result); }}
11. Testing
現在我們的規則引擎預備好了。是時候測試它了。為了測試規則引擎 API,我們將使用 postman 工具 首先,讓我們檢視控制器 /get-all-rules 並從資料庫中獲取所有規則。
