四個流行的Java開源規(guī)則引擎和入門
在許多業(yè)務(wù)系統(tǒng)中,會有大量的業(yè)務(wù)規(guī)則配置,而且隨著政策制度、業(yè)務(wù)流程甚至是管理者的決策發(fā)生變化,這些業(yè)務(wù)規(guī)則也需要進(jìn)行更改。這種變化在一些行業(yè)特別頻繁,并且要求快速響應(yīng)。
規(guī)則引擎的作用是為了適應(yīng)這種變更需求,實(shí)現(xiàn)業(yè)務(wù)系統(tǒng)快速且低成本的更新。一般是將業(yè)務(wù)規(guī)則的配置單獨(dú)拿出來,使之與業(yè)務(wù)系統(tǒng)保持低耦合,如果這個用于配置的模塊做得足夠通用且獨(dú)立,那么它就可以成為一個規(guī)則引擎系統(tǒng)。通過規(guī)則引擎可以快速響應(yīng)業(yè)務(wù)規(guī)則的變化。這種方式不需要修改代碼,減少了修改業(yè)務(wù)代碼之后出現(xiàn)錯誤的可能性,如果規(guī)則引擎提供前端操作界面,還能夠支持業(yè)務(wù)人員輕松上手配置業(yè)務(wù)規(guī)則。
本文主要分享一些基于Java的規(guī)則引擎,這些規(guī)則引擎是目前比較流行的項(xiàng)目,包括:Drolls、Easy RulesRuleBook、OpenL Tablets。并簡單介紹這些規(guī)則引擎的使用方式。
1.Drools
https://www.drools.org/
https://github.com/kiegroup/drools
Drools是一個業(yè)務(wù)規(guī)則管理系統(tǒng)(BRMS)。主要功能模塊包括:核心業(yè)務(wù)規(guī)則引擎(BRE)、Web創(chuàng)作和規(guī)則管理應(yīng)用程序(Drools Workbench)、決策模型和符號(DMN)模型以及用于開發(fā)的IDE插件(idea、eclipse等)。
Drools體系架構(gòu)如下圖所示:
Drools架構(gòu)的執(zhí)行步驟如下:
- 將規(guī)則加載到規(guī)則庫中,該規(guī)則庫始終保持可用。
- 事實(shí)(Facts)被保存到工作內(nèi)存(Working Memory)中,它們可以被修改或撤回。
- Pattern Matcher將新的或現(xiàn)有的事實(shí)與規(guī)則進(jìn)行匹配, 這個過程稱為模式匹配,該過程由規(guī)則引擎執(zhí)行。
- agenda在沖突解決策略的幫助下管理沖突規(guī)則的執(zhí)行順序。
以下是SpringBoot的Drools使用例子。
(1)定義Pom.xml
創(chuàng)建一個基本的springBoot應(yīng)用程序,并將drools依賴項(xiàng)添加到pom.xml。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.praveen.drools.example</groupId>
<artifactId>springboot-drools-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-drools-demo</name>
<description>Demo project for Spring Boot with Drools Engine</description>
<properties>
<java.version>11</java.version>
<drools.version>7.67.0.Final</drools.version>
<springfox-swagger2.version>3.0.0</springfox-swagger2.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>${drools.version}</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-decisiontables</artifactId>
<version>${drools.version}</version>
</dependency>
<!-- swagger ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-boot-starter</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${springfox-swagger2.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)創(chuàng)建一個名為DroolsConfig.java的java配置類。
package com.praveen.drools.example.configuration;
import com.praveen.drools.example.service.CustomerCategorizeService;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieModule;
import org.kie.api.runtime.KieContainer;
import org.kie.internal.io.ResourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Drools Config.
* @author Praveen.Nair
*/
@Configuration
public class DroolsConfig {
private static final String RULES_CUSTOMER_RULES_DRL = "rules/customer-category.drl";
@Bean
public KieContainer kieContainer() {
final KieServices kieServices = KieServices.Factory.get();
KieFileSystem kieFileSystem = kieServices.newKieFileSystem();
kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_CUSTOMER_RULES_DRL));
KieBuilder kb = kieServices.newKieBuilder(kieFileSystem);
kb.buildAll();
KieModule kieModule = kb.getKieModule();
return kieServices.newKieContainer(kieModule.getReleaseId());
}
}
這個配置類創(chuàng)建一個springbean KieContainer,通過加載應(yīng)用程序/resources文件夾下的規(guī)則文件來構(gòu)建規(guī)則引擎。
創(chuàng)建KieFileSystem實(shí)例并從應(yīng)用程序的resources目錄加載DRL文件。接著使用KieService和KieBuilder創(chuàng)建KieContainer并將其配置為spring bean。
(3)創(chuàng)建模型類
創(chuàng)建名為CustomerRequest的Pojo類和字段。
package com.praveen.drools.example.model;
import java.util.Objects;
import java.util.StringJoiner;
/**
* Customer request POJO.
* @author Praveen.Nair
*/
public final class CustomerRequest {
private final long id;
private final Integer age;
private final String gender;
private final Integer numberOfOrders;
public CustomerRequest(long id, Integer age, String gender, Integer numberOfOrders) {
this.id = id;
this.age = age;
this.gender = gender;
this.numberOfOrders = numberOfOrders;
}
public long getId() {
return id;
}
public Integer getAge() {
return age;
}
public String getGender() {
return gender;
}
public Integer getNumberOfOrders() {
return numberOfOrders;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CustomerRequest that = (CustomerRequest) o;
return id == that.id &&
Objects.equals(age, that.age) &&
Objects.equals(gender, that.gender) &&
Objects.equals(numberOfOrders, that.numberOfOrders);
}
@Override
public int hashCode() {
return Objects.hash(id, age, gender, numberOfOrders);
}
@Override
public String toString() {
return new StringJoiner(", ", CustomerRequest.class.getSimpleName() + "[", "]")
.add("id=" + id)
.add("age=" + age)
.add("gender=" + gender)
.add("numberOfOrders='" + numberOfOrders + "'")
.toString();
}
}
我們將這個類作為請求對象參數(shù)傳給規(guī)則引擎,并且將字段作為輸入發(fā)送到定義的規(guī)則中,以便為派生customerType。
另外,再定義了一個名為CustomerCategory.java的java枚舉,用于保存客戶類別,規(guī)則引擎根據(jù)該值派生客戶類型。
package com.praveen.drools.example.model;
/**
* Customer Categories.
*/
public enum CustomerCategory {
GENERAL, KIDS, SENIOR_CITIZEN, SUSPENDED;
public String getValue() {
return this.toString();
}
}
創(chuàng)建一個名為CustomerType的響應(yīng)POJO類,如下所示。
package com.praveen.drools.example.model;
import java.util.Objects;
import java.util.StringJoiner;
/**
* CustomerType Response model.
* @author Praveen.Nair
*/
public class CustomerType {
private CustomerCategory customerType;
public CustomerCategory getCustomerType() {
return customerType;
}
public void setCustomerType(CustomerCategory customerType) {
this.customerType = customerType;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
CustomerType that = (CustomerType) o;
return customerType == that.customerType;
}
@Override
public int hashCode() {
return Objects.hash(customerType);
}
@Override
public String toString() {
return new StringJoiner(", ", CustomerType.class.getSimpleName() + "[", "]")
.add("customerType=" + customerType)
.toString();
}
}
(4) 定義Drools規(guī)則
創(chuàng)建一個名為customer-category.drl的drools規(guī)則文件,并將該文件放在目錄/src/main/resources/rules下。
import com.praveen.drools.example.model.CustomerRequest
import com.praveen.drools.example.model.CustomerCategory;
global com.praveen.drools.example.model.CustomerType customerType;
dialect "mvel"
rule "Categorize customer based on age"
when
CustomerRequest(age < 20)
then
customerType.setCustomerType(CustomerCategory.KIDS);
end
rule "Categorize senior citizen customer based on age"
when
CustomerRequest(age > 50)
then
customerType.setCustomerType(CustomerCategory.SENIOR_CITIZEN);
end
rule "Categorize customer based on number of orders"
when
CustomerRequest(numberOfOrders == 0)
then
customerType.setCustomerType(CustomerCategory.SUSPENDED);
end
rule "Categorize customer general case"
when
CustomerRequest((gender == "M" || gender == "F") && age > 20 && age < 50)
then
customerType.setCustomerType(CustomerCategory.GENERAL);
end
需要在DRL文件中import 使用到的模型。定義一個名為customerType的全局參數(shù),作為多個規(guī)則之間共享數(shù)據(jù)。
DRL文件可以包含一個或多個規(guī)則??梢允褂胢vel語法來指定規(guī)則。此外,每個規(guī)則都可以使用rule關(guān)鍵字進(jìn)行描述。
然后定義when-then語法來指定規(guī)則的條件。根據(jù)Customer請求的輸入值,我們將設(shè)置customerType結(jié)果。
(5) 添加服務(wù)層和控制層
創(chuàng)建一個名為CustomerCategorizeService的服務(wù)類,并添加以下內(nèi)容。
package com.praveen.drools.example.service;
import com.praveen.drools.example.model.CustomerRequest;
import com.praveen.drools.example.model.CustomerType;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
/**
* Customer Categorization service.
* @author Praveen.Nair
*/
public class CustomerCategorizeService {
private final KieContainer kieContainer;
public CustomerCategorizeService(KieContainer kieContainer) {
this.kieContainer = kieContainer;
}
public CustomerType getCustomerType(CustomerRequest customerRequest) {
CustomerType customerType = new CustomerType();
KieSession kieSession = kieContainer.newKieSession();
kieSession.setGlobal("customerType", customerType);
kieSession.insert(customerRequest);
kieSession.fireAllRules();
kieSession.dispose();
return customerType;
}
}
使用注入的KieContainer實(shí)例創(chuàng)建KieSession實(shí)例。返回一個CustomerType類型的全局參數(shù),這個CustomerType將用于保存規(guī)則執(zhí)行結(jié)果。
使用insert方法將customerRequest傳遞給DRL文件,然后我們通過調(diào)用fireAllRules方法觸發(fā)所有規(guī)則,最后通過調(diào)用KieSession的dispose方法終止會話。
接著開發(fā)一個Controller 將服務(wù)發(fā)布為一個API: /API/getCustomerType。API的入?yún)镃ustomerRequest對象,返回類型為CustomerType。Controller代碼如下所示:
package
com.praveen.drools.example.web;
import com.praveen.drools.example.model.CustomerRequest;
import com.praveen.drools.example.model.CustomerType;
import com.praveen.drools.example.service.CustomerCategorizeService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/getCustomerType")
public class CustomerCategorizeController {
private final CustomerCategorizeService customerCategorizeService;
public CustomerCategorizeController(
CustomerCategorizeService customerCategorizeService) {
this.customerCategorizeService = customerCategorizeService;
}
@PostMapping
public ResponseEntity<CustomerType> getCustomer(@RequestBody CustomerRequest customerRequest) {
CustomerType customerType = customerCategorizeService.getCustomerType(customerRequest);
return new ResponseEntity<>(customerType, HttpStatus.OK);
}
}
2.Easy Rules
https://github.com/j-easy/easy-rules
EasyRule是一個輕量級的規(guī)則引擎。它提供了用于創(chuàng)建規(guī)則的抽象以及規(guī)則引擎API,它通過運(yùn)行一組規(guī)則來檢測條件并執(zhí)行操作。
以下是EasyRule的一些核心特性:
- 輕量級類庫和容易上手
- 基于POJO的開發(fā)與注解的編程模型
- 基于MVEL表達(dá)式的編程模型(適用于極簡單的規(guī)則,一般不推薦)
- 支持根據(jù)簡單的規(guī)則創(chuàng)建組合規(guī)則
- 方便且適用于java的抽象的業(yè)務(wù)模型規(guī)則
以下是Java中使用EasyRules的例子:
(1)在Maven中引入依賴包
<dependency>
<groupId>org.jeasy</groupId>
<artifactId>easy-rules-core</artifactId>
<version>3.3.0</version>
</dependency>
(2)定義規(guī)則
Easy Rules提供了一些選項(xiàng)來創(chuàng)建規(guī)則:
- 聲明性注解;
- API;
- 表達(dá)式語言;
- 規(guī)則描述符。
定義方式如下面Java代碼:
@Rule(name = "cart total rule", description = "Give 10% off when shopping cart is greater than $200" )
public class CartTotalRule {
@Condition
public boolean cartTotal(@Fact("cart") Cart cart) {
return cart.isGreaterThanTwoHundered;
}
@Action
public void giveDiscount(@Fact("cart") Cart cart) {
cart.setTotalDiscount(200);
}
}
(3)最后基于事實(shí)數(shù)據(jù)執(zhí)行規(guī)則引擎
public class CartTotalRuleTest {
public static void main(String[] args) {
// define facts
Facts facts = new Facts();
facts.put("cart", get_customer_cart);
// define rules
Rule cartTotalRUle = CartTotalRule
Rules rules = new Rules();
rules.register(cartTotalRUle);
// fire rules on known facts
RulesEngine rulesEngine = new DefaultRulesEngine();
rulesEngine.fire(rules, facts);
}
}
3.RuleBook
https://github.com/deliveredtechnologies/rulebook
RuleBook提供了一個簡單、靈活并且直觀的DSL。RuleBook提供了易于使用的Lambda特定語言或POJO方式來定義規(guī)則,Java開發(fā)人員可以通過帶注解的POJO來組織大規(guī)模規(guī)則集合,替代那些又臭又長的“if/else”。
以下是在Java使用RuleBook的Demo。
(1)Maven
<dependency>
<groupId>com.deliveredtechnologies</groupId>
<artifactId>rulebook-core</artifactId>
<version>${version}</version>
</dependency>
(2)Java定義規(guī)則
public class Cart{
private double cartTotal;
private String cartId;
private Customer user;
private List cartEntries;
//getter and setter
}
public class ShoppingCartRule extends CoRRuleBook {
@Override
public void defineRules() {
//give 10% off when cart total is greater than $200
addRule(RuleBuilder.create().withFactType(Cart.class).withResultType(Double.class)
.when(facts -> facts.getOne().getCartTotal() > 200)
.then((facts, result) -> result.setValue(20))
.stop()
.build());
}
(3)執(zhí)行規(guī)則:
public class CartPromotionRule {
public static void main(String[] args) {
RuleBook cartPromotion = RuleBookBuilder.create(ShoppingCartRule.class).withResultType(Double.class)
.withDefaultResult(0.0)
.build();
NameValueReferableMap facts = new FactMap();
facts.setValue("cart", new Cart(450.0, 123456, customer, entries));
cartPromotion.run(facts);
}
}
4.OpenL Tablets
http://openl-tablets.org/
https://github.com/openl-tablets/openl-tablets
OpenL Tablets 是一個基于 Java和Excel決策表工具的業(yè)務(wù)規(guī)則引擎(BRE)和業(yè)務(wù)規(guī)則管理系統(tǒng)(BRMS)。
主要包括以下幾個部分:
- Business Rules Engines(業(yè)務(wù)規(guī)則引擎)
- WebStudio
- Web services(web服務(wù))
- Rule repository(基于JCR的實(shí)現(xiàn)的規(guī)則庫)
以下是在Java中使用OpenL Tablets的例子。
(1)Maven
<dependency>
<groupId>org.openl</groupId>
<artifactId>org.openl.core</artifactId>
<version>${version}</version>
</dependency>
<dependency>
<groupId>org.openl.rules</groupId>
<artifactId>org.openl.rules</artifactId>
<version>${version}</version>
</dependency>
(2)java實(shí)現(xiàn)
public class Main {
private CartPromotionRules instance;
public static void main(String[] args) {
Main rules = new Main();
// setup user and case here
rules.process(aCase);
}
public void process(Case aCase) {
EngineFactory engineFactory = new RulesEngineFactory(
getClass().getClassLoader()
.getResource("rules.xls"), CartPromotionRules.class);
instance = engineFactory.newEngineInstance();
instance.executePromotion(aCase, new Response());
}
}