表达式引擎Aviator实战
概述
Aviator是一门高性能、轻量级的Java语言实现的表达式动态求值引擎。其设计目标是轻量级和高性能,相对于Groovy、JRuby的笨重,Aviator非常小,不过Aviator的语法受限,它并不是一门完整的语言,只是语言的一小部分集合。定位是介于Groovy这样重量级脚本语言和IKExpression这样轻量级表达式引擎之间。
Aviator的实现思路与其它轻量级的求值器不同,其它求值器是通过解释的方式运行,而Aviator是直接将表达式编译成Java字节码,交给JVM去执行。
功能
- 支持大部分运算操作符:算数运算符、关系运算符、逻辑操作符、正则表达式匹配操作符、三元表达式,支持操作符的优先级及括号的强制优先级
- 支持函数调用和自定义函数
- 自动类型转换,当执行操作时,会自动判断操作数类型并做相应的转换,无法转换就抛异常
- 支持传入变量,支持类似
a.b.c的嵌套变量访问
限制:没有if else、do while等语句,没有赋值语句,没有位运算符;
使用场景
- 公式计算
- 动态脚本控制
- 规则判断以及规则引擎
实战
pom文件引入:
<dependency><groupId>com.googlecode.aviatorgroupId><artifactId>aviatorartifactId>
dependency>
入门
Aviator的数值类型只支持Long和Double,任何整数都将会转换成Long,任何浮点数都会转换成Double,包括用户传入的变量数值。不支持科学计数法,仅支持十进制。
nil对象
nil是Aviator内置的常量,类似Java中的null,空值。nil跟null不同在于,Java中null只能使用在==、!=的比较运算符,而nil还可以使用>、>=、<、<=等比较运算符。Aviator规定,任何对象都比nil大,除nil本身。用户传入的变量如果为null,将自动以nil替代。nil与String相加时,跟Java一样显示为null。
Aviator并不支持日期类型,如果要比较日期,你需要将日期写字符串的形式,并且要求是形如yyyy-MM-dd HH:mm:ss:SS的字符串,否则都将报错。字符串跟java.util.Date比较时将自动转换为Date对象进行比较。
要访问变量a中的某个属性b,可通过a.b访问到,即Aviator支持变量的嵌套访问。
Aviator在表达式级别支持正则表达式,通过//括起来的字符序列构成一个正则表达式,可用于匹配(作为=~操作符的右操作数)、比较大小,匹配仅能与字符串进行匹配。匹配成功后,Aviator会自动将匹配成功的分组放入$num的变量中,$0指代整个匹配的字符串,$1表示第一个分组,以此类推。正则表达式规则跟Java完全一样,内部使用java.util.regex.Pattern编译。
类型转换规则
- Java的byte,short,int,long都转化为Long类型,Java的float,double都将转化为Double类型。Java的char String都将转化为String。Java的null都将转为nil
- 当两个操作符都是Double或Long时,各自按照Double或Long的类型执行
- 当两个操作符中某一个是Double时,另一个操作数也将转换成Double,按照Double类型执行
- 任何类型与String相加,结果为String
- 任何类型都比nil大,除了nil本身
- nil在打印或与字符串相加时,显示为null
- 形如
yyyy-MM-dd HH:mm:ss:SS的字符串,在与java.util.Date做比较时将尝试转换成java.util.Date对象比较 - 没有规定的类型转换操作,除了未知的变量类型之间,都将抛出异常
访问数组和集合
可以通过中括号去访问数组和java.util.List对象,可以通过map.key访问java.util.Map中key对应的value。
两种模式
- 默认AviatorEvaluator以编译速度优先:
AviatorEvaluator.setOptimize(AviatorEvaluator.COMPILE); - 修改为运行速度优先,这会做更多的编译优化:
AviatorEvaluator.setOptimize(AviatorEvaluator.EVAL);
@Test
public void testAviator() {log.info(AviatorEvaluator.execute("1 + 2 + 3") + "");log.info(AviatorEvaluator.execute("3 > 1 && 2 != 4 || true") + "");log.info((String) AviatorEvaluator.execute("3 > 0 ? 'yes': no"));// 三元表达式对于两个分支的结果类型并不要求一致,可以是任何类型// 以下5个皆为trueAviatorEvaluator.execute("nil == nil");AviatorEvaluator.execute("3 > nil");AviatorEvaluator.execute("true != nil");AviatorEvaluator.execute("' ' > nil");AviatorEvaluator.execute("a == nil");// 内置函数log.info("string.length('hello') = " + AviatorEvaluator.execute("string.length('hello')"));log.info("string.contains('hello', 'h') = " + AviatorEvaluator.execute("string.contains('hello', 'h')"));log.info("math.pow(-3, 2) = " + AviatorEvaluator.execute("math.pow(-3, 2)"));log.info("math.sqrt(9.0) = " + AviatorEvaluator.execute("math.sqrt(9.0)"));final List<String> list = new ArrayList<String>();list.add("hello");list.add(" world");final int[] array = new int[3];array[0] = 0;array[1] = 1;array[2] = 3;final Map<String, Date> map = new HashMap<>();map.put("date", new Date());Map<String, Object> env = new HashMap<>();env.put("name", "johnny");String str = "'hello ' + name";log.info((String) AviatorEvaluator.execute(str, env));// 对比, 被@Deprecated的方法log.info((String) AviatorEvaluator.exec(str, "johnny"));env.put("date", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS").format(new Date()));env.put("foo", new Foo(100, new Date()));env.put("email", "killmesoftly2022@gmail.com");env.put("list", list);env.put("array", array);env.put("map", map);// 正则log.info((String) AviatorEvaluator.execute("email=~/\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}/ ? $0:'unknow'", env));// 嵌套引用log.info((String) AviatorEvaluator.execute("'[foo i='+ foo.i + ' year='+(foo.date.year+1900)+ ' month='+foo.date.month +']' ", env));// 日期对比log.info(AviatorEvaluator.execute("date > '2009-12-20 00:00:00:00' ", env) + "");// 数组和列表log.info(AviatorEvaluator.execute("list[0]+list[1]+'\nsum of array='+(array[0]+array[1]+array[2]) +'\ntoday is '+map.date ", env) + "");}
依赖的实体Bean类为:
@Data
@AllArgsConstructor
private static class Foo { int i;Date date;
}
运算符
- 算术运算符:包括
+ - * / %五个二元运算符,和一元运算符-。其中- * / %和一元的-仅能作用于Number类型。"+"不仅能用于Number类型,还可以用于String的相加,或字符串与其他对象的相加。任何类型与String相加,结果为String。 - 逻辑运算符:一元否定运算符
!,逻辑与&&,逻辑或||。逻辑运算符的操作数只能为Boolean。&&和||都执行短路规则 - 关系运算符:也叫比较运算符,包括
<,<=,>,>=,==,!=,不仅可以用于数值,也可以用于Number、String、Pattern、Boolean等,甚至是用户传入的任何两个都实现java.lang.Comparable接口的对象之间。变量之间以及其他类型与nil之间的关系比较,不同类型除了nil之外不能相互比较。任何对象都比nil大,除nil之外。 - 匹配运算符:匹配运算符"=~"用于String和Pattern的匹配,它的左操作数必须为String,右操作数必须为Pattern。匹配成功后,Pattern的分组将存于变量$num,num为分组索引。
内置函数
| 函数名称 | 说明 |
|---|---|
| sysdate() | 返回当前日期对象java.util.Date |
| rand() | 返回一个介于0-1的随机数,double类型 |
| print([out],obj) | 打印对象,如果指定out,向out打印,否则输出到控制台 |
| now() | 返回System.currentTimeMillis |
| string.substring(s,begin[,end]) | 截取字符串s,从begin到end,end如果忽略的话,将从begin到结尾,与java.util.String.substring一样。 |
| math.log(d) | 求d的自然对数 |
| math.log10(d) | 求d以10为底的对数 |
| map(seq,fun) | 将函数fun作用到集合seq每个元素上,返回新元素组成的集合 |
| filter(seq,predicate) | 将谓词predicate作用在集合的每个元素上,返回谓词为true的元素组成的集合 |
| include(seq,element) | 判断element是否在集合seq中,返回boolean值 |
| sort(seq) | 排序集合,仅对数组和List有效,返回排序后的新集合 |
| reduce(seq,fun,init) | fun接收两个参数,集合元素,累积init,用于将fun作用在集合每个元素和初始值上面,返回最终的init值 |
| seq.eq(value) | 返回一个谓词,用来判断传入的参数是否跟value相等,一般用于filter函数,如filter(seq,seq.eq(3)) 过滤返回等于3的元素组成的集合 |
| seq.exists() | 返回判断存在,即不为nil的谓词 |
自定义函数
实现com.googlecode.aviator.runtime.function.AbstractFunction接口,并注册到AviatorEvaluator即可使用
public class UserDefinedFunction extends AbstractFunction {/*** 自定义函数名称*/@Overridepublic String getName() {return "add";}@Overridepublic AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {Number num1 = FunctionUtils.getNumberValue(arg1, env);Number num2 = FunctionUtils.getNumberValue(arg2, env);return new AviatorDouble(num1.doubleValue() + num2.doubleValue());}
}
使用自定义函数之前需要通过addFunction()方式注册,否则报错com.googlecode.aviator.exception.FunctionNotFoundException: Function not found: add。
// 注册函数
AviatorEvaluator.addFunction(new UserDefinedFunction());
log.info("add(1, 1) = " + AviatorEvaluator.execute("add(1, 1)"));
AviatorEvaluator.removeFunction("add");
// log.info("add(1, 1) = " + AviatorEvaluator.execute("add(1, 1)"));
编译表达式
每次执行Aviator.execute()时,背后都经过编译和执行的操作。那可以先编译表达式,拿到编译后结果,然后传入不同的env来重复使用编译结果,提高性能。
/*** 编译表达式和未编译表达式性能测试*/
@Test
public void testCompile() {String expression = "a * (b - c)";Map<String, Object> env = new HashMap<>();env.put("a", 3.3);env.put("b", 2.2);env.put("c", 1.1);int num = 10000;// 编译表达式Expression compliedExp = AviatorEvaluator.compile(expression);Stopwatch watch = Stopwatch.createStarted();for (int i = 0; i < num; i++) {compliedExp.execute(env);}log.info(String.format("预编译耗时为%dms", watch.elapsed(TimeUnit.MILLISECONDS)));watch.stop().start();for (int i = 0; i < num; i++) {AviatorEvaluator.execute(expression, env);}log.info(String.format("无编译耗时为%dms", watch.elapsed(TimeUnit.MILLISECONDS)));
}
输出:
ExpressionTest [testCompile:65] 预编译耗时为13ms
ExpressionTest [testCompile:70] 无编译耗时为2435ms
通过complie方法可以将表达式编译成Expression的中间对象,当要执行表达式时传入Map对象直接调用Expression的execute方法即可。
编译后结果可以自己缓存,也可以交给Aviator缓存,AviatorEvaluator内部维护有一个全局缓存池:
/*** 预编译缓存举例*/
@Test
public void testCompileCache() {String expression1 = "a + b + c";Expression exp1 = AviatorEvaluator.compile(expression1, true);Expression exp2 = AviatorEvaluator.compile(expression1, true);Expression exp3 = AviatorEvaluator.compile(expression1, false);log.info("exp1 == exp2 : " + (exp1 == exp2));log.info("exp1 == exp3 : " + (exp1 == exp3));
}
输出:
ExpressionTest [testCompileCache:79] exp1 == exp2: true
ExpressionTest [testCompileCache:80] exp1 == exp3: false
将cached设置为true时,那下次编译同一个表达式时将直接返回上一次编译结果。使缓存失效:public static void invalidateCache(String expression)。
规则引擎
没有规则引擎时,有些逻辑复杂的业务代码,只能通过不断的增添if - else来满足复杂的业务场景,当if - else过多时,会导致代码极难阅读,虽然能通过策略模式来优化if - else,但依旧存在开发周期缓慢、发布上线的问题。
所以需要规则引擎,尤其是风控系统。
规则引擎的优势:
- 降低开发成本,提高规则变更调整优化的效率和规则上线的速度
- 业务人员独立配置业务规则,开发人员无需理解,以往需要业务人员告诉开发人员,开发人员需要理解才能开发,并且还需要大量的测试来确定是否正确,而且开发结果还容易和提出的业务有偏差,种种都导致开发成本上升
- 增加业务的透明度,业务人员配置之后其它人业务人员也能看到
Aviator也可用于规则引擎
将业务人员配置的规则转换成一个规则字符串,然后将该规则字符串保存进数据库中,当使用该规则时,只传递该规则所需要的参数,便可以直接计算出结果,开发人员无需再为这些规则编写任何代码。
public class AviatorRuleEngine {// 规则可以保存在MySQL或Redis中Map<Integer, String> ruleMap = new HashMap<>();public AviatorRuleEngine() {// 判断是不是资深顾客ruleMap.put(1, "age >= 18 && sumConsume > 2000 && vip");// 资深顾客要求修改ruleMap.put(2, "age > 10 && sumConsume >= 8000 && vip && avgYearConsume >= 1000");}public static void main(String[] args) {AviatorRuleEngine aviator = new AviatorRuleEngine();// 选择规则,传入规则所需要的参数log.info("公式1:" + aviator.getResult(1, 20, 3000, false));log.info("公式2:" + aviator.getResult(2, 23, 8000, true, 2000));}public Object getResult(int ruleId, Object... args) {String rule = ruleMap.get(ruleId);return AviatorEvaluator.exec(rule, args);}
}
输出:
公式1:false
公式2:true
参考
本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!
