One, foreword
Before introducing the rules engine, let’s introduce two business cases.
1.1 case analysis
Case 1: there are a number of questions for students to answer, according to the students answer the correct rate, according to a certain rule to determine the answer star.
Rule definition:
Correct rate (%) | The star |
---|---|
0-20 | 0 |
20 to 50 | 1 |
50-80. | 2 |
80-100. | 3 |
Case two: Magic card small program has a successful experience of the experience value of the scene, and according to the experience value range to determine the user segment.
Rule definition:
Experience value | Dan |
---|---|
0 ~ 50 | Bronze 1 |
50 ~ 300 | Bronze 2 |
300 ~ 550 | Bronze 3 |
550 ~ 800 | Silver 1 |
800 ~ 1050 | Silver 2 |
1050 ~ 1300 | Silver 3 |
1300 ~ 1550 | Gold 1 |
1550 ~ 1800 | Gold 2 |
1800 ~ 2050 | Gold 3 |
2050 ~ 2300 | Diamond 1 |
2300 ~ 2550 | Diamond 2 |
2550 ~ 2800 | Diamond 3 |
2800 ~ 3050 | Diamond 4 |
3050 ~ 3550 | Diamond 5 |
3550 ~ 4050 | King 1 |
4050 ~ 4550 | The king 2 |
4550 ~ 5050 | King 3 |
. | . |
Gain one level for every 500 experience points | King N(N Max 99) |
Rule description: The range of experience values from bronze 1 to diamond 5 is irregular; Starting with Diamond 5, you gain one level of experience for every 500 you gain.
1.2. Routine business implementation
These two kinds of business logic can be implemented simply by concatenating several if else.
Case 1 Implementation method:
/** * calculate the star *@param accuracy
* @return* /
public Integer calStarLevel (Integer accuracy) {
// check the accuracy
if (accuracy == null || accuracy < 0 || accuracy > 100) {
return null;
}
if (accuracy >= 0 && accuracy < 20) {
return 0;
} else if (accuracy >= 20 && accuracy < 50) {
return 1;
} else if (accuracy >= 50 && accuracy < 80) {
return 2;
} else if (accuracy >= 80 && accuracy <= 100) {
return 3;
} else {
return null; }}Copy the code
Case 2 Implementation method:
/** * count the segment *@param experience
* @return* /
public String calLevel (Integer experience) {
// check the accuracy
if (experience == null || experience < 0) {
return null;
}
// Pass the threshold to enter the judgment logic of the king section
Integer levelWZExperienceThreshold = 3050;
if (experience >= levelWZExperienceThreshold ) {
Integer level = (experience - levelWZExperienceThreshold) / 500 + 1;
return (level > 99)?"WZ99" : "WZ" + level;
}
if (experience >= 0 && experience < 50) {
return "QT1";
} else if (experience >= 50 && experience < 300) {
return "QT2";
} else if (experience >= 300 && experience < 550) {
return "QT3";
} else if (experience >= 550 && experience < 800) {
return "BY1";
} else if (experience >= 800 && experience < 1050) {
return "BY2";
} else if (experience >= 1050 && experience < 1300) {
return "BY3";
} else if (experience >= 1300 && experience < 1550) {
return "HJ1";
} else if (experience >= 1550 && experience < 1800) {
return "HJ2";
} else if (experience >= 1800 && experience < 2050) {
return "HJ3";
} else if (experience >= 2050 && experience < 2300) {
return "ZS1";
} else if (experience >= 2300 && experience < 2550) {
return "ZS2";
} else if (experience >= 2550 && experience < 2800) {
return "ZS3";
} else if (experience >= 2800 && experience < 3050) {
return "ZS4";
} else if (experience >= 3050 && experience < 3550) {
return "ZS5";
} else {
return null; }}Copy the code
No one would argue that this is a good code implementation when they look at the way scenarios 1 and 2 are implemented. There are the following disadvantages:
1, if else too much, the program is not readable.
2, rules and code coupling, rules may change at any time, write dead in the program, not flexible enough.
3, rules change, need to modify the code, re-issue version online, cumbersome.
Is there a cleaner implementation that can accommodate frequent rule changes? A rules engine might be a good solution!
Rule engine
2.1 type,
Common rule engines include: Aviator, Drools, EasyRules, etc. Each rule engine has its advantages and disadvantages. You can study it in more details. Based on the advantages of high performance and lightweight Aviator, this share uses the Aviator rule engine.
Third, the application of rules engine
Here we use case two to illustrate the application of the rules engine. Application workflow:
3.1 Translation of rules
Before using the rules engine, we need to translate the rule text into a format that the program can recognize, such as JSON.Copy the code
Case 2:
[{"level": "QT1"."rule": "v>=0 && v<50"
},
{
"level": "QT2"."rule": "v>=50 && v<300"
},
{
"level": "QT3"."rule": "v>=300 && v<550"
},
{
"level": "BY1"."rule": "v>=550 && v<800"
},
{
"level": "BY2"."rule": "v>=800 && v<1050"
},
{
"level": "BY3"."rule": "v>=1050 && v<1300"
},
{
"level": "HJ1"."rule": "v>=1300 && v<1550"
},
{
"level": "HJ2"."rule": "v>=1550 && v<1800"
},
{
"level": "HJ3"."rule": "v>=1800 && v<2050"
},
{
"level": "ZS1"."rule": "v>=2050 && v<2300"
},
{
"level": "ZS2"."rule": "v>=2300 && v<2550"
},
{
"level": "ZS3"."rule": "v>=2550 && v<2800"
},
{
"level": "ZS4"."rule": "v>=2800 && v<3050"
},
{
"level": "ZS5"."rule": "v>=3050 && v<3550"}]Copy the code
Description:
1. Translate the rule into a JSON array where each element represents a rule.
2. Rule is a logical expression, the variable V represents the experience value, and level represents the result of matching the rule.
3. Clear semantics: elements are mutually exclusive and cannot overlap rules.
3.2 Rule Configuration and resolution
In order to adapt to the rapidly changing requirements of rules, the translated rules need to be configured in the configuration center, and the business server can sense the configuration changes in real time. Once configured, we need to parse the configured rules and listen for changes in real time.
@Slf4j
@Service
public class ChallengeConfigLocalCacheService implements InitializingBean {
@Autowired
private KVConfig kvConfig;
@Autowired
private BizNotifyHolder bizNotifyHolder;
private static final String challengeLevelExperienceMappingKey = "challenge_level_experience_mapping";
// Reentrant read-write lock
private ReadWriteLock lock = new ReentrantReadWriteLock();
/** * The mapping between entry levels and experience values is configured */
public List<ChallengeLevelExperienceMappingVO> challengeLevelExperienceMappingList = Lists.newArrayList();
@Override
public void afterPropertiesSet(a) throws Exception {
loadChallengeLevelExperienceMapping(kvConfig.challengeLevelExperienceMapping);
}
@ApolloConfigChangeListener(value = {"application", "props"})
public void watchConfigChange(ConfigChangeEvent changeEvent) {
if (changeEvent.isChanged(challengeLevelExperienceMappingKey)) {
String newValue = changeEvent.getChange(challengeLevelExperienceMappingKey).getNewValue();
log.info("challenge_level_experience_mapping changed. new value = {}", newValue); loadChallengeLevelExperienceMapping(newValue); }}private void loadChallengeLevelExperienceMapping(String value) {
try {
lock.writeLock().lock();
this.challengeLevelExperienceMappingList = JacksonUtil.json2List(value, ChallengeLevelExperienceMappingVO.class);
} catch (Exception e) {
BizNotifyContext context = new BizNotifyContext();
context.setNotifyType(BizNotifyTypeEnum.ZYL.getCode());
context.setTitle("Challenge_level_experience_mapping error loading configuration");
context.setDetail("challenge_level_experience_mapping:" + value);
bizNotifyHolder.handle(context);
} finally{ lock.writeLock().unlock(); }}public List<ChallengeLevelExperienceMappingVO> readChallengeLevelExperienceMapping(a) {
try {
lock.readLock().lock();
return this.challengeLevelExperienceMappingList;
} finally{ lock.readLock().unlock(); }}}Copy the code
The loadChallengeLevelExperienceMapping method from the configuration and analysis to the local center pull configuration. Read from the local configuration readChallengeLevelExperienceMapping method.
3.3. Rule Matching
Read the local rule, traverse the rule list, if the current experience value matches the current rule, return the current segment.
public String upgradeLevel(String oldLevel, String userId, Integer experience) {
// Calculates and triggers a bit promotion
List<ChallengeLevelExperienceMappingVO> levelExperienceMappingList = challengeConfigLocalCacheService.readChallengeLevelExperienceMapping();
for (ChallengeLevelExperienceMappingVO item : levelExperienceMappingList) {
Map<String, Object> env = new HashMap<>(1);
env.put("v", experience);
boolean hit = (Boolean) AviatorEvaluator.execute(item.getRule(), env, true);
if (hit) {
int ret = challengeUserResultMapper.updateLevel(userId, oldLevel, item.getLevel());
if (ret > 0) {
returnitem.getLevel(); }}}return null;
}
Copy the code
To sum up: the rule hitting logic is very simple, and the rule change is very flexible, so there is no need to change every time.
Iv. Principles and precautions of Avaitor
4.1. Brief introduction of principle
Aviator’s basic process is to translate expressions directly into their Java bytecode counterparts for execution, with up to two scans (on in execution-first mode, or once in compile-first mode). This ensures that it outperforms most interpretive expression engines, and has been proven to do so in tests. Second, it doesn’t rely on any third party libraries except for the Commons -beanutils library (for reflection), so it’s very lightweight overall, with the entire JAR package size being 430K even at the current 5.0 release. At the same time, Aviator built-in function library is very “control”, in addition to the necessary string processing, mathematical functions and collection processing, such as file IO, network, etc., are not available, so that the runtime security, if you need these advanced capabilities, can be accessed through the open custom functions. So it has the following features: • High performance • lightweight • some of the more interesting features: • Support for operator overloading • Natively supports large integer and BigDecimal types and operations, and operates in a manner consistent with normal number types through operator overloading. • Native support for regular expression types and matching operators =~ • Clojure’s SEQ library and lambda-like support for flexible handling of collections • Open capabilities: includes custom function access as well as various customization options
4.2. Engine Mode
1. Aviatorevaluator. EVAL, the default value, takes runtime performance first, compilation takes more time to optimize, currently doing some optimization for constant folding and public variable extraction.
2, AviatoreValuator.compile, compile performance first, do not do any compilation optimization, sacrifice certain performance.
Applicable scenarios:
Aviatorevaluator. EVAL: An expression suitable for long running. (Expression is stable)
Aviatorevaluator.compile: Suitable for scenarios where expressions are compiled frequently. (Expression changes frequently)
Aviator has two very important operations: compile and execute.
/** * compile */
public static Expression compile(final String expression) {
return compile(expression, false);
}
/** * Execute */
public static Object execute(final String expression, final Map<String, Object> env) {
return execute(expression, env, false);
}
Copy the code
Using these two methods, there are two problems:
Recompile every time. If your script doesn’t change, this overhead is wasteful and can affect performance.
Each compilation creates new anonymous classes that occupy the JVM’s method area (Perm or Metaspace), gradually filling up memory, and eventually triggering the Full GC.
Compile and execute have an overloaded method: compile(final String expression, final boolean cached) execute(final String expression, final Map<String, Object> env, Final Boolean cached) the cached parameter indicates whether a compilation cache is used.
/** * compile */
public static Expression compile(final String expression, final boolean cached) {
return getInstance().compile(expression, cached);
}
/** * Execute */
public static Object execute(final String expression, final Map<String, Object> env, final boolean cached) {
return getInstance().execute(expression, env, cached);
}
Copy the code
1. Use the second method and pass cached=ture. 2, start the class enabled the compilation cache: AviatorEvaluator. GetInstance (). SetCachedExpressionByDefault (true);
// It can also be set once in the startup class:
public class MokaServerApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
AviatorEvaluator.getInstance().setCachedExpressionByDefault(true); SpringApplication.run(MokaServerApplication.class, args); }}Copy the code
Aviator works by:
Five, the conclusion
The above two cases are two very simple applications of rule engine. Here they are just as a way to solve the problem. I understand that where there are rules, rule engine can appear, that is to say, its application scenarios are very wide. Thank you very much!