spring boot,

中文版

The Lightweight Rules Engine easy-rules

qihaiyan qihaiyan Follow Mar 26, 2023 · 7 mins read

Using a rules engine properly can greatly reduce code complexity and improve code maintainability. The best-known open-source rules engine in the industry is Drools — feature-rich, but also quite heavy. In some simpler scenarios, a lightweight rules engine is all we need to meet the requirements. This post introduces a small rules engine, easy-rules, provided as a library. It supports Spring SPEL expressions and integrates nicely into Spring projects.

The complete code is available in the example project https://github.com/qihaiyan/springcamp/tree/main/spring-easy-rule

1. Overview

Moving business rules into configuration files streamlines the code and makes it easier to maintain: when a rule changes, only the configuration file needs to be modified. easy-rules is a compact rules engine that supports Spring SPEL expressions, as well as Apache JEXL expressions and MVL expressions.

2. Adding the Dependencies to the Project

Add the dependencies in the project’s gradle build.

build.gradle:

plugins {
    id 'org.springframework.boot' version '3.0.5'
    id 'io.spring.dependency-management' version '1.1.0'
    id 'java'
}

group = 'cn.springcamp'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
    testCompileOnly {
        extendsFrom testAnnotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation "org.springframework.boot:spring-boot-starter-json"
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.jeasy:easy-rules-core:4.1.0'
    implementation 'org.jeasy:easy-rules-spel:4.1.0'
    implementation 'org.jeasy:easy-rules-support:4.1.0'
    annotationProcessor 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    testImplementation "org.springframework.boot:spring-boot-starter-test"
    testImplementation 'org.junit.vintage:junit-vintage-engine'
    testImplementation 'org.junit.vintage:junit-vintage-engine'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:2022.0.1"
    }
}

test {
    useJUnitPlatform()
}

3. The Configuration File

The example program puts the business rules into a configuration file. The business rule configuration file (demo-rule.yml):

name: "age rule"
description: ""
priority: 1
condition: "#person.getAdult() == false"
actions:
  - "T(java.lang.System).out.println(\"Shop: Sorry, you are not allowed\")"
  - "#person.setAdult(true)"
  - "#person.setAge(18)"
---
name: "alcohol rule"
description: ""
priority: 1
condition: "#person.getAdult() == true"
actions:
  - "T(java.lang.System).out.println(\"Shop: you are now allowed\")"

Rules in the configuration file are defined through a condition; when a rule is satisfied, the actions configured in actions are invoked. The example project uses Spring SPEL expressions for rule configuration. Two rules are configured in the file: the first rule checks whether the rule is satisfied through getAdult() on the person Spring bean, and when the rule is satisfied, three methods are invoked.

Configure the rule files in Spring Boot’s own configuration file, application.yml:

rule:
  skip-on-first-failed-rule: true
  skip-on-first-applied-rule: false
  skip-on-first-non-triggered-rule: true
  rules:
    - rule-id: "demo"
      rule-file-location: "classpath:demo-rule.yml"

4. Configuring the Rules Engine in Code

The rules engine is configured through the RuleEngineConfig Spring configuration class:

@Slf4j
@EnableConfigurationProperties(RuleEngineConfigProperties.class)
@Configuration
public class RuleEngineConfig implements BeanFactoryAware {
    @Autowired(required = false)
    private List<RuleListener> ruleListeners;

    @Autowired(required = false)
    private List<RulesEngineListener> rulesEngineListeners;

    private BeanFactory beanFactory;

    @Bean
    public RulesEngineParameters rulesEngineParameters(RuleEngineConfigProperties properties) {
        RulesEngineParameters parameters = new RulesEngineParameters();
        parameters.setSkipOnFirstAppliedRule(properties.isSkipOnFirstAppliedRule());
        parameters.setSkipOnFirstFailedRule(properties.isSkipOnFirstFailedRule());
        parameters.setSkipOnFirstNonTriggeredRule(properties.isSkipOnFirstNonTriggeredRule());
        return parameters;
    }

    @Bean
    public RulesEngine rulesEngine(RulesEngineParameters rulesEngineParameters) {
        DefaultRulesEngine rulesEngine = new DefaultRulesEngine(rulesEngineParameters);
        if (!CollectionUtils.isEmpty(ruleListeners)) {
            rulesEngine.registerRuleListeners(ruleListeners);
        }
        if (!CollectionUtils.isEmpty(rulesEngineListeners)) {
            rulesEngine.registerRulesEngineListeners(rulesEngineListeners);
        }
        return rulesEngine;
    }

    @Bean
    public BeanResolver beanResolver() {
        return new BeanFactoryResolver(beanFactory);
    }

    @Bean
    public RuleEngineTemplate ruleEngineTemplate(RuleEngineConfigProperties properties, RulesEngine rulesEngine) {
        RuleEngineTemplate ruleEngineTemplate = new RuleEngineTemplate();
        ruleEngineTemplate.setBeanResolver(beanResolver());
        ruleEngineTemplate.setProperties(properties);
        ruleEngineTemplate.setRulesEngine(rulesEngine);
        return ruleEngineTemplate;
    }

    @Bean
    public RuleListener defaultRuleListener() {
        return new RuleListener() {
            @Override
            public boolean beforeEvaluate(Rule rule, Facts facts) {
                return true;
            }

            @Override
            public void afterEvaluate(Rule rule, Facts facts, boolean b) {
                log.info("-----------------afterEvaluate-----------------");
                log.info(rule.getName() + rule.getDescription() + facts.toString());
            }

            @Override
            public void beforeExecute(Rule rule, Facts facts) {
                log.info("-----------------beforeExecute-----------------");
                log.info(rule.getName() + rule.getDescription() + facts.toString());
            }

            @Override
            public void onSuccess(Rule rule, Facts facts) {
                log.info("-----------------onSuccess-----------------");
                log.info(rule.getName() + rule.getDescription() + facts.toString());
            }

            @Override
            public void onFailure(Rule rule, Facts facts, Exception e) {
                log.info("-----------------onFailure-----------------");
                log.info(rule.getName() + "----------" + rule.getDescription() + facts.toString() + e.toString());
            }
        };
    }

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }
}

The configuration defines the ruleEngineTemplate Spring bean; the execution of the rules engine is triggered through ruleEngineTemplate.

5. Executing the Rules Engine

Once ruleEngineTemplate is configured, we can execute the rules engine in business code to process the business rules configured in the configuration file:

For demonstration, we put the rules engine execution code in the run method of Application, so the rules engine fires as soon as the program starts:

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired
    RuleEngineTemplate ruleEngineTemplate;

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

    @Override
    public void run(String... args) {
        Person person = new Person();
        Facts facts = new Facts();
        facts.put("person", person);
        ruleEngineTemplate.fire("demo", facts);

    }
}

After the program runs, the console prints Shop: Sorry, you are not allowed, which corresponds to the "T(java.lang.System).out.println(\"Shop: Sorry, you are not allowed\")" action we configured in the actions of the rule file — proof that the rule was executed successfully.

qihaiyan
Written by qihaiyan
业精于勤而荒于嬉,行成于思而毁于随