springboot integrated rule engine drools

Overview of the rule engine

The main idea of the rule engine is to separate the business decision-making part of the application, and use the predefined semantic template to write business decisions (business rules), which are configured and managed by users or developers when needed.

Welcome to pay attention to the personal public account [Have a good time learning technology] exchange and study

Usage scenario

For example, when shopping in a mall, you can get 100 off when you spend over 300, 200 if you spend over 500, etc., and these rules may change at any time. If this requirement is realized, what should we do under normal circumstances?

if…else

pseudocode

if(amount >= 300) {<!-- -->
    amount -= 100;
} else if(amount >= 500) {<!-- -->
    amount -= 200;
}

Strategy pattern

pseudocode

interface Strategy {<!-- -->
    reductionAmount(int amount);
}

class Strategy1 {<!-- -->
    reductionAmount(int amount);
}

class Strategy2 {<!-- -->
    reductionAmount(int amount);
}

The above methods can realize the function, but what if the discount changes frequently? Do different products have different rules? Moreover, these rules are heavily coupled with the code, and each rule change requires retesting and deployment.

Introduction to Drools

drools is an open source rule engine developed by the JBoss organization based on the java language. It can liberate complex and changeable business rules from hard coding and store them in files or specific storage media in the form of rule scripts (such as Stored in the database), so that changes in business rules can take effect immediately in the online environment without modifying the project code or restarting the server.

drools official website: https://www.drools.org/

drools Chinese website: Drools Chinese website | A powerful open source rule engine based on java

Drools source code download address: https://github.com/kiegroup/drools

springboot integrated drools case

Business scenario

order amount discount amount
100-200 10
200-500 20
500-1000 50
1000-2000 100
2000-5000 300
5000-10000 500
more than 10000 1000

Create a springboot project

Create a maven project and import drools related dependencies

<!-- https://mvnrepository.com/artifact/org.drools/drools-compiler -->
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-compiler</artifactId>
    <version>7.73.0.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.drools/drools-mvel -->
<dependency>
    <groupId>org.drools</groupId>
    <artifactId>drools-mvel</artifactId>
    <version>7.73.0.Final</version>
</dependency>
<!-- test -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
</dependency>

resources/META-INF/kmodule.xml

<?xml version="1.0" encoding="UTF-8"?>
<kmodule xmlns="http://jboss.org/kie/6.0.0/kmodule">
  
    <!--
    name: Specify the name of kbase, which can be arbitrary, but needs to be unique
    packages: Specifies the directory of the rule file, which needs to be filled in according to the actual situation, otherwise it cannot be loaded into the rule file
    default: Specifies whether the current kbase is the default
    -->
    <kbase name="fdf" packages="com.fandf.rules" default="true">
        <!--
        name: Specify the name of ksession, which can be arbitrary, but needs to be unique
        default: Specifies whether the current session is the default
        -->
        <ksession name="fdf-rule" default="true"/>
    </kbase>
</kmodule>

Create entity order

package com.fandf.test.entity;
  
import lombok.Data;
  
import java.math.BigDecimal;
  
/**
* @author fandongfeng
* @date 2023/5/3 19:17
*/
@Data
public class Order {<!-- -->
  
  
    /**
    * Price before order discount
    */
    private BigDecimal originalPrice;
    /**
    * Price after order discount
    */
    private BigDecimal realPrice;
  
}

Create a rule file resources/rules/orderDiscount.drl

// Order discount rules must be consistent with the package of kmodule.xml
package com.fandf.rules
import com.fandf.test.entity.Order
import java.math.BigDecimal
  
// Rule 1: 100-200 discount 10
rule "order_discount_1"
    when
        $order: Order(originalPrice >= 100 & amp; & amp; originalPrice < 200) // match the pattern, find the Order object in the rule engine (working memory), and name it $order
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(10)));
        System.out.println("Rule 1 is successfully matched, and the order amount is discounted by 10 yuan");
end
  
// Rule 2: 200-500 discount 20
rule "order_discount_2"
    when
        $order: Order(originalPrice >= 200 & amp; & amp; originalPrice < 500)
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(20)));
        System.out.println("Rule 2 is successfully matched, and the order amount is discounted by 20 yuan");
end
  
// Rule 3: 500-1000 discount 50
rule "order_discount_3"
    when
        $order: Order(originalPrice >= 500 & amp; & amp; originalPrice < 1000)
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(50)));
        System.out.println("Rule 3 is successfully matched, and the order amount is discounted by 50 yuan");
end
// Rule 4: 1000-2000 discount 100
rule "order_discount_4"
    when
        $order: Order(originalPrice >= 1000 & amp; & amp; originalPrice < 2000)
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(100)));
        System.out.println("Rule 4 is successfully matched, and the order amount is discounted by 100 yuan");
end
// Rule five: 2000-5000 discount 300
rule "order_discount_5"
    when
        $order: Order(originalPrice >= 2000 & amp; & amp; originalPrice < 5000)
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(300)));
        System.out.println("Rule 5 is successfully matched, and the order amount is discounted by 300 yuan");
end
// Rule 6: 5000-10000 discount 500
rule "order_discount_6"
    when
        $order: Order(originalPrice >= 5000 & amp; & amp; originalPrice < 10000)
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(500)));
        System.out.println("Rule 6 is successfully matched, and the order amount is discounted by 500 yuan");
end
// Rule 7: 1000 discount for more than 10000
rule "order_discount_7"
    when
        $order: Order(originalPrice >= 10000)
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(1000)));
        System.out.println("Rule 7 is successfully matched, and the order amount is discounted by 1,000 yuan");
end

Write a test class OrderTest

package com.fandf.test.entity;
  
import com.fandf.test.Application;
import org.junit.jupiter.api.Test;
import org.kie.api.KieServices;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.springframework.boot.test.context.SpringBootTest;
  
import java.math.BigDecimal;
  
@SpringBootTest(classes = Application. class)
public class OrderTest {<!-- -->
  
    @Test
    public void test(){<!-- -->
        KieServices kieServices = KieServices.Factory.get();
        // Get the Kie container object default container object
        KieContainer kieContainer = kieServices. newKieClasspathContainer();
        // Get the session object from the Kie container object (the default session object
        KieSession kieSession = kieContainer. newKieSession();

        Order order = new Order();
        order.setOriginalPrice(BigDecimal.valueOf(180));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());

        kieSession = kieContainer. newKieSession();
        order.setOriginalPrice(BigDecimal.valueOf(300));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());

        kieSession = kieContainer. newKieSession();
        order.setOriginalPrice(BigDecimal.valueOf(600));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());

        kieSession = kieContainer. newKieSession();
        order.setOriginalPrice(BigDecimal.valueOf(1200));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());

        kieSession = kieContainer. newKieSession();
        order.setOriginalPrice(BigDecimal.valueOf(3000));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());

        kieSession = kieContainer. newKieSession();
        order.setOriginalPrice(BigDecimal.valueOf(8000));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());

        kieSession = kieContainer. newKieSession();
        order.setOriginalPrice(BigDecimal.valueOf(12000));
        // Insert the order object into the working memory
        kieSession.insert(order);
        // match object
        // Activate the rules, and the drools framework automatically performs rule matching. If the match is successful, execute
        kieSession.fireAllRules();
        System.out.println("Price before discount: " + order.getOriginalPrice() + ", price after discount: " + order.getRealPrice());



        // close the session
        kieSession.dispose();

    }
  
}

Execution output

[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:03.823 INFO 5536 [-] [main] o.d.c.k.b.impl.ClasspathKieProject Found kmodule: file:/Users/dongfengfan/IdeaProjects/SpringCloudLearning/ fdf-test/target/classes/META-INF/kmodule.xml
[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:04.019 WARN 5536 [-] [main] o.d.c.k.b.impl.ClasspathKieProject Unable to find pom.properties in /Users/dongfengfan/IdeaProjects/SpringCloudLearning/fdf -test/target/classes
[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:04.024 INFO 5536 [-] [main] o.d.c.k.b.impl.ClasspathKieProject Recursed up folders, found and used pom.xml /Users/dongfengfan/IdeaProjects/ Spring Cloud Learning/fdf-test/pom.xml
[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:04.025 INFO 5536 [-] [main] o.d.c.k.b.i.InternalKieModuleProvider Creating KieModule for artifact com.fandf:fdf-test:1.0.0
[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:04.035 INFO 5536 [-] [main] o.d.c.kie.builder.impl.KieContainerImpl Start creation of KieBase: fdf
[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:04.041 WARN 5536 [-] [main] o.d.c.kie.builder.impl.KieBuilderImpl File 'rules/orderDiscount.drl' is in folder 'rules ' but declares package 'com.fandf.rules'. It is advised to have a correspondence between package and folder names.
[fdf-test:0.0.0.0:9002] 2023-05-03 22:25:05.091 INFO 5536 [-] [main] o.d.c.kie.builder.impl.KieContainerImpl End creation of KieBase: fdf
Successfully matched to rule 1, the order amount is discounted by 10 yuan
Price before discount: 180, price after discount: 170
Successfully matched to rule 2, the order amount is discounted by 20 yuan
Price before discount: 300, price after discount: 280
Successfully matched to rule 3, the order amount is discounted by 50 yuan
Price before discount: 600, price after discount: 550
Successfully matched to rule 4, the order amount is discounted by 100 yuan
Price before discount: 1200, price after discount: 1100
Successfully matched to rule 5, the order amount is discounted by 300 yuan
Price before discount: 3000, price after discount: 2700
Successfully matched to rule 6, the order amount is discounted by 500 yuan
Price before discount: 8000, price after discount: 7500
Successfully matched to rule 7, the order amount is discounted by 1,000 yuan
Price before discount: 12000, price after discount: 11000

Drools syntax

rules file

A very important job when using Drools is to write the rule file, usually the suffix of the rule file is .drl.

drl is the abbreviation of Drools Rule Language. Write the specific rule content in the rule file.

keyword description
package Package name, only limited to logical management, queries or functions under the same package name can be called directly
import used to import classes or Static method
global global variable
function custom Function
query query
rule end rule body

Rule Body Grammar

The rule body is the part that judges business rules and processes business results.

rule "ruleName"
    attributes
    when
        LHS
    then
        RHS
end

rule: keyword, indicating the beginning of the rule, and the parameter is the unique name of the rule.

attribute: rule attribute, which is a parameter between rule and when, and is optional.

when: Keyword followed by the conditional part of the rule.

LHS (Left Hand Side): is the generic name for the condition part of the rule. It consists of zero or more conditional elements. If the LHS is empty, it is treated as a conditional element that is always true.

then: Keyword followed by the result part of the rule.

RHS (Right Hand Side): is the general name for the consequence or action part of the rule.

end: keyword, indicating the end of a rule.

Pattern pattern matching

The grammatical structure of pattern is: bind variable name: Object (Field constraint)

Among them, the binding variable name can be omitted, and it is generally recommended to start the naming of the binding variable name with $. If a bind variable name is defined, it can be used in the RHS part of the rule body to manipulate the corresponding Fact object. The Field constraint part is zero or more expressions that need to return true or false.

such as our case

rule "order_discount_1"
    when
        $order: Order(originalPrice >= 100 & amp; & amp; originalPrice < 200) // match the pattern, find the Order object in the rule engine (working memory), and name it $order
    then
        $order.setRealPrice($order.getOriginalPrice().subtract(BigDecimal.valueOf(10)));
        System.out.println("Rule 1 is successfully matched, and the order amount is discounted by 10 yuan");
end
symbol description
greater than
less than
>= greater than or equal to
<= less than or equal to
=== equal to td>
!= is not equal to
contains check a Fact object Whether an attribute value contains a specified object value
not contains Check whether an attribute value of a Fact object does not contain a specified object Value
memberOf Determines whether a property of a Fact object is in one or more collections
not memberOf Judge whether a property of a Fact object is not in one or more collections
matches Determine whether the properties of a Fact object match the provided standard Java regular expressions
not matches Determine whether the properties of a Fact object do not match Provided standard Java regular expressions for matching