ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Aviator表达式引擎语法手册:从基础到高级实践

Aviator表达式引擎语法手册:从基础到高级实践 1. 为什么我要认真写一份Aviator语法手册第一次接触Aviator是在一个规则引擎项目里。当时业务方要求把促销折扣、会员等级、限购数量这些逻辑从代码里抽出来做成运营人员能自己改的配置。我第一反应是用脚本语言但引入Groovy或JavaScript引擎太重启动慢、内存占用高安全边界也不好控制。后来同事扔给我一个jar包说试试这个Google的叫Aviator。我花了一个下午把官方文档啃完又花了两天把项目里的规则全部迁移过去实测下来启动速度快得离谱单条表达式求值基本在微秒级别。Aviator是一个轻量级、高性能的Java表达式求值引擎它把字符串形式的表达式编译成Java字节码来执行所以速度非常接近原生Java代码。它最初是Google内部使用的一个工具后来开源出来在规则引擎、动态配置、数据过滤、指标计算这些场景里用得很多。如果你正在做风控规则、促销引擎、报表公式、动态阈值告警这类需求Aviator基本是绕不开的选项。这篇文章我会把Aviator的语法体系从头到尾梳理一遍包括基础类型、运算符、函数调用、变量作用域、自定义函数、编译缓存这些核心内容同时穿插我在实际项目里踩过的坑和总结出来的实操技巧。不管你是刚听说Aviator的新手还是已经用过但想系统整理一遍的老手这篇手册都能直接拿来参考。2. Aviator核心设计思路与选型考量2.1 为什么是Aviator而不是其他表达式引擎Java生态里表达式引擎不少常见的有JUEL、MVEL、SpEL、OGNL、Groovy Shell还有JEXL。我在选型阶段把这些都试了一遍最后选Aviator主要基于几个维度的对比。引擎执行方式性能依赖体积语法友好度安全控制Aviator编译成字节码极高极小高好SpEL解释执行部分编译中等中高一般MVEL编译成字节码高中高一般Groovy编译成字节码高大极高差JUEL解释执行低小中好OGNL解释执行低小中差Aviator最大的优势在于它把表达式编译成JVM字节码执行时几乎没有解释开销。官方给出的基准测试里Aviator的求值速度比JUEL快几十倍比Groovy也快不少。同时它的jar包只有几百KB不依赖任何第三方库这在一些对包体积敏感的场景里非常关键。另一个重要考量是安全。Groovy Shell这类引擎功能太强能直接调用Java的反射、文件IO、系统命令如果表达式来源不可控风险极大。Aviator默认只暴露有限的函数和运算符不支持直接调用任意Java方法安全边界清晰得多。当然它提供了自定义函数扩展机制你可以按需开放能力。2.2 编译缓存机制是性能的关键Aviator的性能优势很大程度上来自它的编译缓存。每次调用AviatorEvaluator.execute()时引擎会先根据表达式字符串计算一个缓存key如果缓存里已经有编译好的结果就直接复用不再重新编译。这个设计意味着同一个表达式第一次执行会慢一些编译开销后续执行就非常快。我在项目里做过一个简单的压测同一个表达式执行一百万次第一次编译大概花了十几毫秒后续每次求值平均在0.5微秒左右。如果每次都重新编译一百万次的总耗时会在十几秒级别差距是数量级的。注意Aviator默认使用LRU缓存缓存大小可以通过AviatorEvaluator.setCachedExpressionByDefault(true)和AviatorEvaluator.setCacheSize()来调整。如果你的表达式是动态生成的、数量非常大要留意缓存命中率必要时可以关闭缓存或者自己管理编译结果。2.3 表达式与脚本的边界Aviator严格来说是一个表达式引擎不是脚本引擎。它支持多行语句、变量赋值、条件分支、循环但整体设计还是围绕“求值”这个核心。它没有类定义、没有模块系统、没有异常处理语法。这个边界很重要因为很多人一开始会把它当脚本语言用写着写着发现某些功能不支持就觉得很别扭。我的建议是把Aviator定位成“可配置的计算逻辑”而不是“可编程的业务脚本”。规则判断、公式计算、数据转换、条件过滤这些场景非常适合复杂的业务流程编排、状态机、事务控制还是应该放在Java代码里。理解这个边界用起来会顺手很多。3. Aviator基础语法体系详解3.1 数据类型与字面量写法Aviator支持的数据类型包括nil、boolean、long、double、string、list、map、function、java type。注意它没有int类型整数统一用long表示浮点数统一用double。这个设计简化了类型系统但在和Java交互时要注意类型转换。字面量的写法很直观// 数字 AviatorEvaluator.execute(1); // long AviatorEvaluator.execute(1.5); // double AviatorEvaluator.execute(1e3); // double科学计数法 // 字符串单引号和双引号都支持 AviatorEvaluator.execute(hello); AviatorEvaluator.execute(\world\); // 布尔 AviatorEvaluator.execute(true); AviatorEvaluator.execute(false); // nil相当于null AviatorEvaluator.execute(nil); // list AviatorEvaluator.execute([1, 2, 3]); AviatorEvaluator.execute([a, b, c]); // map AviatorEvaluator.execute({name: Tom, age: 18});字符串里如果需要转义用反斜杠。Aviator支持\n、\t、\\、\、\这些常见转义符。另外它支持Unicode转义比如\u4e2d表示“中”。实操心得Aviator的字符串拼接用号但如果一边是字符串一边是数字它会自动把数字转成字符串。这个行为在大多数场景下符合直觉但要注意1 2 a的结果是3a而a 1 2的结果是a12。从左到右依次计算类型在遇到字符串时发生转换。3.2 运算符全解析Aviator的运算符体系比较完整我按类别整理一下。算术运算符、-、*、/、%。注意除法如果两个操作数都是long结果也是long会截断小数部分。比如5 / 2结果是2不是2.5。要得到小数结果至少有一个操作数要是double比如5.0 / 2或者5 / 2.0。比较运算符、!、、、、。Aviator的比较运算符支持数字、字符串、日期等类型。字符串比较是按字典序。这里有个坑和equals的行为不完全一样。对于基本类型比较值对于对象类型比较引用equals比较内容。但在Aviator里字符串的实际上做了内容比较这点和Java不同要特别注意。逻辑运算符、||、!。支持短路求值左边为false时右边不执行||左边为true时右边不执行。这个特性在写规则时很有用可以避免空指针。三元运算符condition ? expr1 : expr2。这个在规则表达式里用得非常多比如age 18 ? adult : minor。位运算符、|、^、~、、、。这些在处理标志位、权限掩码时有用。正则匹配运算符~。比如hello123 ~ \\d返回true。这个在数据校验场景里很方便。// 正则匹配示例 AviatorEvaluator.execute(13800138000 ~ ^1[3-9]\\\\d{9}$);注意正则表达式里的反斜杠在Aviator字符串里需要双重转义。写的时候容易漏建议先在Java里测试好正则再嵌入Aviator表达式。3.3 变量与作用域Aviator的变量通过env参数传入是一个Map。表达式里直接写变量名即可引用。MapString, Object env new HashMap(); env.put(name, Tom); env.put(age, 18); Object result AviatorEvaluator.execute(name is age years old, env);变量名区分大小写支持字母、数字、下划线但不能以数字开头。Aviator还支持嵌套属性访问比如user.name、order.items[0].price。这个特性在对象图比较深的时候非常方便。MapString, Object env new HashMap(); MapString, Object user new HashMap(); user.put(name, Tom); env.put(user, user); AviatorEvaluator.execute(user.name, env); // 返回 Tom对于Java BeanAviator会自动调用getter方法。比如user.getName()可以写成user.name。它也支持调用无参方法比如hello.length()。变量作用域方面Aviator有全局变量和局部变量的概念。通过let关键字可以定义局部变量作用域限于当前表达式块。AviatorEvaluator.execute(let x 10; let y 20; x y);实操心得在规则引擎场景里我习惯把所有外部输入都放到env里表达式内部尽量只用let定义中间变量。这样规则的可测试性更好因为只要构造env就能复现任何规则执行。3.4 条件分支与循环Aviator支持if-else语句和for循环这让它能处理稍微复杂一点的逻辑。// if-else AviatorEvaluator.execute(if (age 18) { adult } else { minor }); // for循环 AviatorEvaluator.execute(let sum 0; for i in range(1, 10) { sum sum i }; sum);range(start, end)生成一个从start到end-1的序列。也支持range(start, end, step)指定步长。还有while循环和break、continue关键字。不过说实话我在实际项目里很少在Aviator里写循环因为大部分计算逻辑用内置函数或者Java预处理更合适。循环更适合处理list的遍历求和这类简单场景。// 遍历list AviatorEvaluator.execute(let sum 0; for x in [1,2,3,4,5] { sum sum x }; sum);3.5 函数调用与内置函数库Aviator内置了一批常用函数覆盖字符串、数学、集合、日期等类别。调用方式和Java类似函数名(参数1, 参数2)。字符串函数string.length()、string.substring(start, end)、string.indexOf(str)、string.replace(old, new)、string.split(regex)、string.trim()、string.toUpperCase()、string.toLowerCase()、string.contains(str)、string.startsWith(prefix)、string.endsWith(suffix)。数学函数math.abs(x)、math.ceil(x)、math.floor(x)、math.round(x)、math.max(a, b)、math.min(a, b)、math.pow(a, b)、math.sqrt(x)、math.log(x)、math.random()。集合函数seq.size(list)、seq.contains(list, element)、seq.get(list, index)、seq.join(list, separator)、seq.sort(list)、seq.reverse(list)。日期函数date.format(date, pattern)、date.parse(str, pattern)、date.now()、date.year(date)、date.month(date)、date.day(date)。// 字符串处理 AviatorEvaluator.execute(hello world.substring(0, 5)); // hello AviatorEvaluator.execute(a,b,c.split(,)); // [a, b, c] // 数学计算 AviatorEvaluator.execute(math.max(10, 20)); // 20 AviatorEvaluator.execute(math.round(3.6)); // 4 // 集合操作 AviatorEvaluator.execute(seq.join([1,2,3], -)); // 1-2-3注意Aviator的函数调用是大小写敏感的。math.max不能写成Math.max。另外不同版本的Aviator内置函数列表可能有差异建议以你使用的版本对应的官方文档为准。4. 自定义函数与高级特性实操4.1 自定义函数的完整实现步骤内置函数覆盖不了所有业务需求自定义函数是Aviator最实用的扩展点。实现一个自定义函数需要继承com.googlecode.aviator.runtime.function.AbstractFunction类实现call方法。import com.googlecode.aviator.runtime.function.AbstractFunction; import com.googlecode.aviator.runtime.type.AviatorObject; import com.googlecode.aviator.runtime.type.AviatorLong; import com.googlecode.aviator.runtime.type.AviatorRuntimeJavaType; import java.util.Map; public class AddFunction extends AbstractFunction { Override public String getName() { return add; } Override public AviatorObject call(MapString, Object env, AviatorObject arg1, AviatorObject arg2) { long a ((Number) arg1.getValue(env)).longValue(); long b ((Number) arg2.getValue(env)).longValue(); return AviatorLong.valueOf(a b); } }注册函数AviatorEvaluator.addFunction(new AddFunction()); AviatorEvaluator.execute(add(3, 5)); // 8如果参数数量不固定可以继承AbstractVariadicFunction实现variadicCall方法。public class SumFunction extends AbstractVariadicFunction { Override public String getName() { return sum; } Override public AviatorObject variadicCall(MapString, Object env, AviatorObject... args) { long total 0; for (AviatorObject arg : args) { total ((Number) arg.getValue(env)).longValue(); } return AviatorLong.valueOf(total); } }实操心得自定义函数的返回值一定要用AviatorObject的子类包装不能直接返回Java对象。常见的包装类有AviatorLong、AviatorDouble、AviatorString、AviatorBoolean、AviatorNil、AviatorRuntimeJavaType。如果返回的是复杂对象用AviatorRuntimeJavaType.valueOf(obj)包一下。4.2 编译表达式与重复执行如果同一个表达式要执行很多次建议先编译再执行避免每次查找缓存的开销。Expression expression AviatorEvaluator.compile(a b * c); MapString, Object env new HashMap(); env.put(a, 1); env.put(b, 2); env.put(c, 3); Object result expression.execute(env); // 7编译后的Expression对象是线程安全的可以缓存起来复用。我在项目里通常用一个ConcurrentHashMap来管理表达式缓存key是表达式字符串value是编译后的Expression。private static final ConcurrentHashMapString, Expression EXPRESSION_CACHE new ConcurrentHashMap(); public static Expression getExpression(String expressionStr) { return EXPRESSION_CACHE.computeIfAbsent(expressionStr, AviatorEvaluator::compile); }4.3 使用AviatorScript编写更复杂的逻辑Aviator从5.0版本开始引入了AviatorScript支持更完整的脚本语法包括函数定义、模块导入、异常处理等。如果你用的是较新版本可以这样写AviatorEvaluator.execute(fn add(a, b) { return a b; } add(3, 5));AviatorScript还支持lambda表达式、try-catch、throw等特性。不过要注意这些高级特性会带来额外的复杂度和安全考量。如果只是做简单的规则求值用基础表达式就够了。4.4 与Java集合和Bean的深度交互Aviator对Java集合的支持很完善。List可以用下标访问Map可以用key访问还支持in运算符判断元素是否存在。MapString, Object env new HashMap(); env.put(list, Arrays.asList(1, 2, 3, 4, 5)); env.put(map, Collections.singletonMap(key, value)); AviatorEvaluator.execute(list[0], env); // 1 AviatorEvaluator.execute(map.key, env); // value AviatorEvaluator.execute(3 in list, env); // true AviatorEvaluator.execute(seq.size(list), env); // 5对于Java BeanAviator通过反射调用getter。如果Bean有getName()方法表达式里写bean.name即可。它也支持调用有参方法比如bean.calculate(10)。注意反射调用有性能开销如果表达式执行频率很高建议把Bean的属性预先提取到Map里用Map代替Bean传入env。我在一个日调用量上亿的规则引擎里做过这个优化整体吞吐提升了大概30%。5. 常见问题排查与避坑指南5.1 类型转换异常怎么排查Aviator最常见的报错就是类型转换异常。比如把字符串当数字用或者把null参与运算。AviatorEvaluator.execute(abc 1); // 结果是 abc1不报错 AviatorEvaluator.execute(nil 1); // 报错nil不能参与算术运算排查思路先确认env里传入的变量类型是否符合预期再检查表达式里是否有隐式类型转换。Aviator的报错信息通常会指出哪个位置出了类型问题结合表达式字符串定位即可。一个实用的技巧是在表达式里用nil判断做防御AviatorEvaluator.execute(a ! nil ? a 1 : 0, env);5.2 缓存导致的“诡异”问题Aviator的编译缓存有时候会导致一些看起来很奇怪的问题。比如你修改了自定义函数的实现但表达式执行结果还是旧的。这是因为表达式编译后函数调用已经被内联到字节码里了。解决办法修改自定义函数后调用AviatorEvaluator.clearCache()清空缓存或者重启应用。在开发环境里我习惯在每次修改规则后手动清一次缓存。另一个缓存相关的问题是内存泄漏。如果表达式是动态生成的、数量无限增长LRU缓存会不断淘汰旧表达式但如果你自己用Map缓存Expression对象就要注意控制大小。5.3 性能优化的几个关键点第一尽量复用编译后的Expression对象不要每次execute都传字符串。第二env里的变量尽量用简单类型避免深层嵌套的Bean。第三表达式本身要简洁避免在表达式里做复杂计算能预处理的预处理。第四如果表达式数量有限可以调大缓存如果表达式数量巨大且重复率低考虑关闭缓存。优化手段适用场景预期收益复用Expression对象高频执行同一表达式减少缓存查找开销预提取Bean属性对象图较深减少反射调用简化表达式逻辑复杂减少字节码指令数调整缓存大小表达式数量多提高缓存命中率关闭缓存表达式几乎不重复避免缓存维护开销5.4 安全使用的注意事项如果表达式来源不可控比如用户输入一定要做安全限制。Aviator默认不允许调用任意Java方法但通过自定义函数可能引入风险。建议只注册必要的自定义函数函数内部做好参数校验限制表达式长度和复杂度对表达式执行设置超时可以通过Future超时实现。提示AviatorEvaluator提供了setOption方法可以设置一些运行选项比如是否允许赋值、是否允许循环等。在安全敏感的场景里可以关闭这些特性。5.5 版本兼容性速查Aviator不同版本之间有一些行为差异。比如4.x和5.x在AviatorScript语法上有较大变化内置函数列表也有调整。升级版本时要做充分的回归测试。我整理了一个简单的兼容性对照特性4.x5.x基础表达式支持支持AviatorScript不支持支持lambda不支持支持try-catch不支持支持模块导入不支持支持内置函数数量较少较多如果你的项目还在用4.x升级到5.x之前要重点测试自定义函数和复杂表达式的行为。6. 我在实际项目中的使用体会Aviator在我手里最主要的用途是规则引擎和动态配置。有一个项目是做电商促销的运营人员需要配置“满100减20”、“会员额外9折”、“限购3件”这类规则。最开始这些规则是硬编码在Java里的每次调整都要发版。后来迁移到Aviator运营在后台配置表达式保存后即时生效研发效率提升非常明显。迁移过程中最大的感受是表达式要尽量保持简单。我见过同事写了一个两百多字符的表达式里面嵌套了三元运算符、函数调用、正则匹配后来出了问题排查了半天。我的做法是把复杂规则拆成多个简单表达式分步计算中间结果放到env里传递。这样每个表达式都容易理解和测试。另一个体会是测试覆盖要跟上。表达式是字符串编译器不会帮你检查逻辑错误。我通常会给每个规则写对应的单元测试构造不同的env输入验证输出是否符合预期。Aviator本身提供了AviatorEvaluator.execute的API写测试很方便。最后分享一个小技巧在开发阶段可以开启Aviator的调试模式打印编译后的字节码和求值过程对理解表达式执行机制很有帮助。具体方式是设置系统属性aviator.debug为true或者在代码里调用AviatorEvaluator.setOption(Options.TRACE, true)。这个功能在排查复杂表达式的性能问题时特别有用能看到每一步的执行耗时。
返回列表