一、前言
在介绍规则引擎之前,先引入两个业务案例。
1.1、案例解析
案例一:有若干题目供学生作答,根据学生作答的正确率,依据某规则给本次作答判定作答星级。
规则定义:
| 正确率(%) | 星级 |
|---|---|
| 0-20 | 0 |
| 20-50 | 1 |
| 50-80 | 2 |
| 80-100 | 3 |
案例二:魔卡小程序中有一个闯关成功获得经验值的场景,并且根据经验值范围确定用户的段位。
规则定义:
| 经验值 | 段位 |
|---|---|
| 0~50 | 青铜1 |
| 50~300 | 青铜2 |
| 300~550 | 青铜3 |
| 550~800 | 白银1 |
| 800~1050 | 白银2 |
| 1050~1300 | 白银3 |
| 1300~1550 | 黄金1 |
| 1550~1800 | 黄金2 |
| 1800~2050 | 黄金3 |
| 2050~2300 | 钻石1 |
| 2300~2550 | 钻石2 |
| 2550~2800 | 钻石3 |
| 2800~3050 | 钻石4 |
| 3050~3550 | 钻石5 |
| 3550~4050 | 王者1 |
| 4050~4550 | 王者2 |
| 4550~5050 | 王者3 |
| ... | ... |
| 每获得500经验值可升一级 | 王者N(N最大值为99) |
规则说明:从青铜1到钻石5,其经验值范围是不规则的;从钻石5开始经验值每提升500,对应段位提升一级。
1.2、常规业务实现
这两种业务逻辑,实现起来很简单,无非是通过多个if else串联起来就能实现。
案例一实现方式:
/**
* 计算星级
* @param accuracy
* @return
*/
public Integer calStarLevel (Integer 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;
}
}
案例二实现方式:
/**
* 计算段位
* @param experience
* @return
*/
public String calLevel (Integer experience) {
// 正确率校验
if (experience == null || experience < 0) {
return null;
}
// 超过王者段位门槛,进入王者段位判断逻辑
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;
}
}
看到方案一和方案二的实现方式,没有人会认为这是好的代码实现。存在如下弊端:
1、if else太多,程序可读性不高。
2、规则和代码耦合,规则可能随时变化,写死在程序里,不够灵活。
3、规则发生变化,需要修改代码,重新发版上线,繁琐。
有没有比较简洁的实现方式,并且又能适应规则频繁的变化?规则引擎或许是一个好的解决方案!
二、规则引擎
2.1、种类
常见的规则引擎有:Aviator、Drools、EasyRules等,每一种规则引擎都有其优点和缺点,更细节的大家可以去研究,基于Aviator的高性能、轻量级等优点,本分享选用Aviator规则引擎展开。
三、规则引擎的应用
以下我们通过案例二来说明规则引擎的应用。
应用工作流:
3.1、规则翻译
在使用规则引擎之前,我们需要将规则文本翻译成程序能识别的格式,比如说json。
案例二:
[
{
"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"
}
]
说明:
1、将规则翻译为一个json数组,每一个元素代表一种规则,
2、其中rule是一个逻辑表达式,变量v代表经验值,level表示命中该规则的结果。
3、语义要明确:元素之间两两互斥,不能存在重叠的规则。
3.2、规则配置和解析
为了适应规则快速变化的需求,我们翻译之后的规则需要配置在配置中心,配置更改,业务服务器可以实时的感知到。配置好后,我们需要对配置的规则进行解析和实时更改监听。
@Slf4j
@Service
public class ChallengeConfigLocalCacheService implements InitializingBean {
@Autowired
private KVConfig kvConfig;
@Autowired
private BizNotifyHolder bizNotifyHolder;
private static final String challengeLevelExperienceMappingKey = "challenge_level_experience_mapping";
// 可重入读写锁
private ReadWriteLock lock = new ReentrantReadWriteLock();
/**
* 闯关段位和经验值映射关系配置
*/
public List<ChallengeLevelExperienceMappingVO> challengeLevelExperienceMappingList = Lists.newArrayList();
@Override
public void afterPropertiesSet() 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异常");
context.setDetail("challenge_level_experience_mapping:" + value);
bizNotifyHolder.handle(context);
} finally {
lock.writeLock().unlock();
}
}
public List<ChallengeLevelExperienceMappingVO> readChallengeLevelExperienceMapping() {
try {
lock.readLock().lock();
return this.challengeLevelExperienceMappingList;
} finally {
lock.readLock().unlock();
}
}
}
其中loadChallengeLevelExperienceMapping方法从配置中心拉取配置并解析到本地。 readChallengeLevelExperienceMapping方法从本地读取配置。
3.3、规则匹配
读取本地规则,遍历规则列表,如果当前经验值命中当前规则,则返回当前段位。
public String upgradeLevel(String oldLevel, String userId, Integer experience) {
// 计算并触发段位提升
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) {
return item.getLevel();
}
}
}
return null;
}
综上:规则命中逻辑非常简洁,同时规则变更很灵活,不用每次更改都要上线。
四、Avaitor的原理和注意事项
4.1、原理简述
Aviator 的基本过程是将表达式直接翻译成对应的 java 字节码执行,整个过程最多扫两趟(开启执行优先模式,如果是编译优先模式下就一趟),这样就保证了它的性能超越绝大部分解释性的表达式引擎,测试也证明如此;其次,除了依赖 commons-beanutils 这个库之外(用于做反射)不依赖任何第三方库,因此整体非常轻量级,整个 jar 包大小哪怕发展到现在 5.0 这个大版本,也才 430K。同时, Aviator 内置的函数库非常“节制”,除了必须的字符串处理、数学函数和集合处理之外,类似文件 IO、网络等等你都是没法使用的,这样能保证运行期的安全,如果你需要这些高阶能力,可以通过开放的自定义函数来接入。因此总结它的特点是: • 高性能 • 轻量级 • 一些比较有特色的特点: • 支持运算符重载 • 原生支持大整数和 BigDecimal 类型及运算,并且通过运算符重载和一般数字类型保持一致的运算方式。 • 原生支持正则表达式类型及匹配运算符 =~ • 类 clojure 的 seq 库及 lambda 支持,可以灵活地处理各种集合 • 开放能力:包括自定义函数接入以及各种定制选项
4.2、引擎模式
1、AviatorEvaluator.EVAL,默认值,以运行时的性能优先,编译会花费更多时间做优化,目前会做一些常量折叠、公共变量提取的优化。
2、AviatorEvaluator.COMPILE,以编译的性能优先,不会做任何编译优化,牺牲一定的运行性能。
适用场景:
AviatorEvaluator.EVAL:适合长期运行的表达式。(表达式稳定不变)
AviatorEvaluator.COMPILE:适合需要频繁编译表达式的场景。(表达式变动频繁)
Aviator有两个非常重要的操作:compile(编译)、execute(执行)。
/**
* 编译
*/
public static Expression compile(final String expression) {
return compile(expression, false);
}
/**
* 执行
*/
public static Object execute(final String expression, final Map<String, Object> env) {
return execute(expression, env, false);
}
使用这两个方法,有两个问题:
1、每次都重新编译,如果你的脚本没有变化,这个开销是浪费的,非常影响性能。
2、编译每次都产生新的匿名类,这些类会占用 JVM 方法区(Perm 或者 metaspace),内存逐步占满,并最终触发full gc。
通过查看源码,发现compile和execute有一个重载方法: compile(final String expression, final boolean cached) execute(final String expression, final Map<String, Object> env, final boolean cached) 参数cached表示是否使用编译缓存。
/**
* 编译
*/
public static Expression compile(final String expression, final boolean cached) {
return getInstance().compile(expression, cached);
}
/**
* 执行
*/
public static Object execute(final String expression, final Map<String, Object> env, final boolean cached) {
return getInstance().execute(expression, env, cached);
}
1、使用第二种方法并传入cached=ture。 2、在启动类中启用编译缓存:AviatorEvaluator.getInstance().setCachedExpressionByDefault(true);
// 也可以在启动类中一次性设置:
public class MokaServerApplication extends SpringBootServletInitializer {
public static void main(String[] args) {
AviatorEvaluator.getInstance().setCachedExpressionByDefault(true);
SpringApplication.run(MokaServerApplication.class, args);
}
}
Aviator工作原理:
五、结语
以上两个案例,是规则引擎的两个非常简单的应用,这里只是作为一种解决问题的思路,我理解有规则的地方,就可以出现规则引擎的身影,也就是说它的适用场景非常广泛,希望它能出现在你的业务开发逻辑中。谢谢!