快速整合和使用 drools 規則引擎

乔京飞發表於2024-04-09

規則引擎技術的主要思想是將應用程式中的業務規則分離出來,業務規則不再以程式程式碼的形式駐留在系統中,而是儲存在獨立的檔案或者資料庫中,完全獨立於程式。業務人員可以像管理資料一樣對業務規則進行管理。業務規則在程式執行時被載入到規則引擎中供應用系統呼叫。

drools 是當前比較流行的規則引擎技術,由 JBoss 組織提供的基於 Java 語言開發的開源規則引擎,可以將複雜且多變的業務規則從硬編碼中解放出來,以規則指令碼的形式存放在檔案或特定的儲存介質中(例如資料庫),使得業務規則的變更不需要修改專案程式碼、不需要重啟伺服器就可以立即生效。

本篇部落格的 demo 以個稅計算器為例,介紹如何使用 drools 規則引擎,有關具體技術細節,限於篇幅有限,這裡不會介紹,具體細節可以參考官網。

drools官網地址:https://drools.org

drools原始碼下載地址:https://github.com/kiegroup/drools


一、搭建工程

搭建一個 SpringBoot 工程,具體結構如下:

image

drools 規則引擎將規則編寫在以 .drl 為字尾的檔案中,drl 檔案預設也是使用 Java 語言編寫,所以學習起來很容易。

IDEA 自帶 drools 外掛,在 drl 檔案中編寫規則時有智慧提示效果,非常方便。一般情況下,我們使用 IDEA 編寫業務規則,預設情況下 drl 檔案會被打包到專案 jar 包中,為了方便後續調整規則,我們可以將 drl 檔案的內容,儲存到資料庫中或者 oss 雲盤中,程式在執行時從 jar 包外部讀取規則內容。

本篇部落格的 demo 沒有建立 application.yml 檔案,預設情況下 springboot 的 web 啟動埠為 8080

我們看一下 pom 檔案的內容,對於 drools 來說只需要引入一個包即可:drools-compiler

<?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
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.jobs</groupId>
    <artifactId>springboot_drools</artifactId>
    <version>1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--drools規則引擎-->
        <dependency>
            <groupId>org.drools</groupId>
            <artifactId>drools-compiler</artifactId>
            <version>7.10.0.Final</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.4.5</version>
            </plugin>
        </plugins>
    </build>
</project>

二、程式碼細節展示

本篇部落格的 demo 是個稅計算器,我們建立一個用於向規則引擎傳遞資料的實體類

package com.jobs.entity;

import lombok.Data;

@Data
public class Calculation {
    //稅前工資
    private double wage;
    //應納稅所得額
    private double wagemore;
    //稅率
    private double cess;
    //速算扣除數
    private double preminus;
    //扣稅額
    private double wageminus;
    //稅後工資
    private double actualwage;
}

這裡不考慮繳納社保和專項扣除等因素,個稅計算的規則如下:

image

因此我們對應的在 rule.drl 中編寫的規則如下:

//當前規則檔案用於計算個人所得稅
package calculation
import com.jobs.entity.Calculation

//計算應納稅所得額
rule "tax_setWagemore"
    salience 100
    //設定生效日期
    date-effective "2022-10-01"
    no-loop true
    when
        $cal:Calculation(wage > 0)
    then
        double wagemore = $cal.getWage() - 5000;
        $cal.setWagemore(wagemore);
        update($cal);
end

//設定稅率、速算扣除數
rule "tax_3000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal:Calculation(wagemore <= 3000)
    then
        $cal.setCess(0.03);//稅率
        $cal.setPreminus(0);//速算扣除數
        update($cal);
end

rule "tax_12000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal:Calculation(wagemore > 3000 &&wagemore <= 12000)
    then
        $cal.setCess(0.1);//稅率
        $cal.setPreminus(210);//速算扣除數
        update($cal);
end


rule "tax_25000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal : Calculation(wagemore > 12000 &&wagemore <= 25000)
    then
        $cal.setCess(0.2);
        $cal.setPreminus(1410);
        update($cal);
end

rule "tax_35000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal : Calculation(wagemore > 25000 &&wagemore <= 35000)
    then
        $cal.setCess(0.25);
        $cal.setPreminus(2660);
        update($cal);
end

rule "tax_55000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal : Calculation(wagemore > 35000 &&wagemore <= 55000)
    then
        $cal.setCess(0.3);
        $cal.setPreminus(4410);
        update($cal);
end

rule "tax_80000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal : Calculation(wagemore > 55000 &&wagemore <= 80000)
    then
        $cal.setCess(0.35);
        $cal.setPreminus(7160);
        update($cal);
end

rule "tax_max"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal : Calculation(wagemore > 80000)
    then
        $cal.setCess(0.45);
        $cal.setPreminus(15160);
        update($cal);
end

rule "tax_result"
    salience 80
    when
        $cal : Calculation(wage > 0 && wagemore > 0 && cess > 0)
    then
        //扣稅額
        double wageminus = $cal.getWagemore() * $cal.getCess() - $cal.getPreminus();
        double actualwage = $cal.getWage() - wageminus;
        $cal.setWageminus(wageminus);
        $cal.setActualwage(actualwage);
        System.out.println("--稅前工資:"+$cal.getWage());
        System.out.println("--應納稅所得額:"+$cal.getWagemore());
        System.out.println("--稅率:" + $cal.getCess());
        System.out.println("--速算扣除數:" + $cal.getPreminus());
        System.out.println("--扣稅額:" + $cal.getWageminus());
        System.out.println("--稅後工資:" + $cal.getActualwage());
end

本篇部落格的 demo 在每次被呼叫時,都去讀取 rule.drl 的內容,具體程式碼在 RuleService 中實現:

package com.jobs.service;

import com.jobs.entity.Calculation;
import org.kie.api.io.ResourceType;
import org.kie.api.runtime.KieSession;
import org.kie.internal.utils.KieHelper;
import org.springframework.stereotype.Service;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

@Service
public class RuleService {

    //呼叫Drools規則引擎實現個人所得稅計算
    public Calculation calculate(Calculation calculation) throws Exception {
        KieSession session = createKieSessionFromDRL("src\\main\\resources\\rule.drl");
        session.insert(calculation);
        session.fireAllRules();
        session.dispose();
        return calculation;
    }

    // 從指定路徑的檔案中,讀取規則內容,建立 KieSession
    public KieSession createKieSessionFromDRL(String drlFullPath) throws Exception{
        //設定規則所使用的日期格式
        System.setProperty("drools.dateformat", "yyyy-MM-dd");

        //讀取規則檔案中的內容,實際專案中,可以把規則內容儲存到資料庫中,或者 web 伺服器上,或者 oss 中
        //當前把規則就存放在檔案中,在專案執行中,在不重啟專案的情況下,可以修改規則內容並立刻生效
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(
                new InputStreamReader(new FileInputStream(drlFullPath), StandardCharsets.UTF_8))) {
            String line;
            while ((line = br.readLine()) != null) {
                sb.append(line);
                sb.append(System.lineSeparator());
            }
        }

        KieHelper kieHelper = new KieHelper();
        kieHelper.addContent(sb.toString(), ResourceType.DRL);
        return kieHelper.build().newKieSession();
    }
}

然後在 RuleController 中對外提供計算個稅的介面,只需要傳遞一個稅前工資額即可計算得出結果

package com.jobs.controller;

import com.jobs.entity.Calculation;
import com.jobs.service.RuleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/rule")
public class RuleController {
    @Autowired
    private RuleService ruleService;

    @RequestMapping("/calculate")
    public Calculation calculate(double wage) {
        try {
            Calculation calculation = new Calculation();
            calculation.setWage(wage);
            calculation = ruleService.calculate(calculation);
            System.out.println(calculation);
            return calculation;
        } catch (Exception ex) {
            ex.printStackTrace();
            return null;
        }
    }
}

為了方便介面呼叫,這裡也提供了前端頁面,程式碼如下:

<!DOCTYPE html>
<html>
<head>
    <!-- 頁面meta -->
    <meta charset="utf-8">
    <title>個人所得稅計算</title>
    <meta name="description" content="個人所得稅計算">
    <meta name="keywords" content="個人所得稅計算">
    <meta content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no" name="viewport">
</head>
<body class="mainBg">
<div id="app">
    <h3 align="center">個人所得稅計算器(使用規則引擎實現)</h3>
    <h3 align="center">在專案執行過程中,你可以在不重啟專案的情況下,修改規則檔案內容,並立刻生效</h3>
    <table align="center" width="25%" border="0">
        <tr>
            <td>稅前月收入</td>
            <td>
                <input type="text" v-model="cal.wage">
            </td>
        </tr>
        <tr>
            <td></td>
            <td>
                <input type="button" value="計  算" @click="calculate()">
            </td>
        </tr>
        <tr>
            <td>應納稅所得額</td>
            <td>
                <input type="text" v-model="cal.wagemore">
            </td>
        </tr>
        <tr>
            <td>稅率</td>
            <td>
                <input type="text" v-model="cal.cess">
            </td>
        </tr>
        <tr>
            <td>速算扣除數</td>
            <td>
                <input type="text" v-model="cal.preminus">
            </td>
        </tr>
        <tr>
            <td>扣稅額</td>
            <td>
                <input type="text" v-model="cal.wageminus">
            </td>
        </tr>
        <tr>
            <td>稅後工資</td>
            <td>
                <input type="text" v-model="cal.actualwage">
            </td>
        </tr>
    </table>
</div>
</body>
<!-- 引入元件庫 -->
<script src="js/vue.js"></script>
<script src="js/axios.js"></script>
<script>
    new Vue({
        el: '#app',
        data:{
            cal:{}
        },
        methods: {
            calculate(){
                if(this.cal.wage <= 5000){
                    alert("稅前月收入需要大於5000");
                    return;
                }
                axios.get("/rule/calculate?wage=" + this.cal.wage).then((res) => {
                    console.log(res);
                    this.cal = res.data;
                });
            }
        }
    });
</script>
</html>

三、驗證成果

啟動專案後,預設情況下 springboot 允許訪問 resources 下的 static 目錄下的靜態頁面 index.html

對於本篇部落格的 demo 來說,訪問 http://localhost:8080 即可檢視到靜態頁面,輸入 10000 元計算個稅,如下圖:

image

從結果可以發現,稅率是 0.1,執行的是 rule.drl 檔案中的名稱為 tax_12000 的規則,此時你可以使用 IDEA 修改一下,比如將稅率修改為 0.2

rule "tax_12000"
    salience 90
    no-loop true
    activation-group "SETCess_Group"
    when
        $cal:Calculation(wagemore > 3000 &&wagemore <= 12000)
    then
        $cal.setCess(0.2);//這裡故意將稅率修改為 0.2
        $cal.setPreminus(210);//速算扣除數
        update($cal);
end

注意不要重啟 IDEA 的專案,此時重新點選頁面中的計算,發現剛剛修改的規則生效了,如下圖所示:

image


OK,以上就是有關 springboot 使用 drools 規則引擎的介紹,有興趣的話可以下載原始碼進行驗證。

本篇部落格的原始碼下載地址:https://files.cnblogs.com/files/blogs/699532/springboot_drools.zip

相關文章