Reasonable use of the rule engine can greatly reduce code complexity and improve code maintainability. The well-known open source rule engine in the industry is Drools, which is rich in functions, but also relatively large. In some simple scenarios, we only need a simple rule engine to meet the requirements.
This article introduces a small rule engine easy-rules , which is provided as a lib library, supports spring SPEL expressions, and can be well integrated in spring projects.
For specific code, refer to the sample project https://github.com/qihaiyan/springcamp/tree/master/spring-easy-rule
I. Overview
By configuring the business rules in the configuration file, the code can be streamlined, and it is easy to maintain. When the rules are modified, only the configuration file needs to be modified. easy-rules is a compact rule engine that supports spring's SPEL expressions, as well as Apache JEXL expressions and MVL expressions.
2. Add dependencies to the project
Add dependencies in the project's gradle.
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. Configuration file
The sample program puts the business rules into the configuration file, the business rule configuration file (demo-rule.yml) code:
name: "age rule"
description: ""
priority: 1
condition: "#person.getAdult() == false"
actions:
- "T(java.lang.System).out.println(\"Shop: Sorry, you are not allowed to buy alcohol\")"
- "#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 to buy alcohol\")"
The rules in the configuration file are configured through condition, and when the rules are satisfied, the actions configured in actions will be invoked.
The sample project uses spring's SPEL expression for rule configuration. Two rules are configured in the configuration file. The first rule judges whether the rule is satisfied through getAdult() in person
the spring bean, and calls three methods when the rule is satisfied.
In the configuration file of spring-boot itself, application.yml configures the rule file:
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. Configure the rule engine in the code
Configure the rule engine through RuleEngineConfig
this 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;
}
}
ruleEngineTemplate
This spring bean is configured in the configuration file , and the execution of the rule engine is triggered through the ruleEngineTemplate.
5. Execution rule engine
ruleEngineTemplate
After configuration, we can execute the rule engine in the business code to process the business rules configured in the configuration file:
As a demonstration, we put the execution code of the rule engine into the run method of Application, and execute the rule engine immediately after 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 is executed, you can see that it is printed in the console Shop: Sorry, you are not allowed to buy alcohol
. This content corresponds to the configuration we configured in the actions in the rule file "T(java.lang.System).out.println(\"Shop: Sorry, you are not allowed to buy alcohol\")"
, indicating that the rule has been successfully executed.