Aviator 表达式引擎实战:语法、性能优化与生产环境避坑指南
发布时间:2026/9/25 1:19:34来源:尧图网络
1. 为什么值得花时间啃透 Aviator 的语法第一次接触 Aviator 是在一个营销活动系统里运营同学需要频繁调整优惠券的发放门槛比如“用户等级大于 3 且近 30 天消费满 500 元或者邀请人数超过 5 人”。如果每次都改代码重新发版迭代节奏根本扛不住。当时团队评估了几个 Java 表达式引擎最终选了 Google 开源的 Aviator。原因很直接语法接近 JavaScript运营和产品能看懂性能在同类里属于第一梯队依赖极轻一个几百 KB 的 jar 包就能跑起来。Aviator 本质上是一个轻量级、高性能的 Java 表达式求值引擎它把一段字符串形式的表达式动态编译成 Java 字节码执行而不是靠解释器逐字符解析。这个设计决定了它的两个核心特征一是快二是表达式在编译后可以缓存复用。它常被用在规则引擎、动态配置、风控判断、指标计算、报表公式这些场景里。你给它一个表达式字符串和一份变量上下文它返回一个结果整个过程不需要你写 if-else也不需要重启服务。这篇手册面向的是需要把 Aviator 真正落到生产代码里的 Java 开发者。我会从语法细节讲到工程实践包括那些官方文档一笔带过、但实际用起来一定会踩的坑。如果你只是想知道“Aviator 能干嘛”看第一节就够了如果你想把它用稳、用对建议从头到尾过一遍。下面所有示例都基于 Aviator 5.x 版本这是目前主流使用的版本和早期 3.x、4.x 在 API 上有一些差异我会在涉及的地方特别标注。2. Aviator 的核心语法骨架2.1 数据类型与字面量写法Aviator 的类型系统比 Java 简单得多它内部只认几种基本类型nil、布尔、长整型long、双精度浮点double、字符串、正则表达式、函数、Java 对象。注意这里没有 int、float、short 这些细分所有整数在 Aviator 内部统一按 long 处理所有小数按 double 处理。这个设计在大多数业务场景下没问题但在涉及金额精确计算时要格外小心后面会专门讲。字面量的写法和 Java 高度相似// 数字 100 3.14 1e3 // 科学计数法等于 1000.0 // 字符串单双引号都行 hello world // 布尔 true false // nil相当于 Java 的 null nil // 正则表达式用两个斜杠包裹 /\\d{11}/ // 匹配 11 位数字这里有个容易忽略的点Aviator 里1和1.0是不同类型1是 long1.0是 double。当你做除法时5/2的结果是2整数除法而5/2.0才是2.5。这个行为和 Java 一致但从 JavaScript 转过来的人经常在这里翻车因为 JS 里所有数字都是浮点。2.2 运算符优先级与那些反直觉的细节Aviator 支持的运算符基本覆盖了日常所需算术运算 - * / %比较运算 !逻辑运算 || !三元运算? :以及位运算 | ^ ~ 。优先级顺序和 Java 几乎一样但有几个地方需要单独拎出来说。第一个是字符串拼接。Aviator 里既能做加法也能做拼接规则是只要有一边是字符串就按字符串拼接处理。a 1得到a11 2 x得到3x。这个规则本身没问题但如果你从上下文里取出来的变量类型不确定就可能出现意料之外的结果。比如a b你以为在做数值加法结果 a 是字符串最后变成了拼接。第二个是和equals的区别。Aviator 的对基本类型做值比较对对象做引用比较这和 Java 一致。但 Aviator 提供了一个eq函数来做值比较写起来更安全// 假设 a 和 b 是两个内容相同的字符串对象 a b // 可能是 false因为是引用比较 eq(a, b) // true值比较第三个是逻辑运算的短路特性。和||都支持短路这意味着a ! nil a.length 0这种写法是安全的不会因为 a 为 nil 而报空指针。但要注意Aviator 里访问 nil 对象的属性会直接抛异常所以判空一定要放在前面。2.3 变量引用与属性访问Aviator 的变量不需要声明直接写名字就能用值从传入的上下文Context里取。如果上下文里没有这个变量默认返回 nil。这个特性很方便但也埋了个雷变量名拼错了不会报错只会静默返回 nil导致逻辑走偏。我的做法是在开发阶段开启严格模式让未定义的变量直接抛异常。属性访问用点号支持嵌套user.name user.address.city order.items[0].price数组和集合用中方括号索引list[0]、map[key]都支持。这里有个细节Aviator 对 Java 的 List 和 Map 做了适配map.key和map[key]都能取到值前者更简洁后者更通用。如果 key 里包含特殊字符或者 key 本身是变量就必须用方括号。2.4 函数调用与自定义函数Aviator 内置了一批常用函数比如string.length()、math.abs()、seq.contains()、date.format()等。调用方式和 Java 方法类似但底层是通过函数注册机制实现的。你也可以注册自己的函数这是 Aviator 最实用的扩展点之一。// 注册一个自定义函数判断用户是否为 VIP AviatorEvaluator.addFunction(new AbstractFunction() { Override public String getName() { return isVip; } Override public AviatorObject call(MapString, Object env, AviatorObject arg1) { long userId ((AviatorLong) arg1).longValue(env); boolean result userService.isVip(userId); return AviatorBoolean.valueOf(result); } });注册之后表达式里就能直接写isVip(userId)。这个机制让 Aviator 能无缝接入你现有的业务逻辑而不只是做纯计算。需要注意的是自定义函数的执行是在表达式求值线程里同步进行的如果函数内部有耗时操作比如查数据库会拖慢整个表达式求值所以尽量保持函数轻量或者提前把数据准备好放进上下文。3. 把 Aviator 用进 Java 项目的完整路径3.1 依赖引入与版本选择Aviator 的 Maven 坐标很干净没有传递依赖dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.4.3/version /dependency版本选择上5.x 是当前推荐版本相比 4.x 主要改进了编译缓存机制和函数注册 API。如果你用的是 JDK 85.x 完全兼容JDK 11 及以上也没问题。唯一要注意的是Aviator 5.x 要求 JDK 8 起步如果你还在用 JDK 7只能退回 3.x 版本但那个版本已经很久没维护了不建议在新项目里用。3.2 表达式编译与执行的两种模式Aviator 有两种使用方式一种是每次执行都编译另一种是编译一次缓存起来反复执行。前者适合表达式只执行一两次的场景后者适合高频调用的场景。// 方式一直接执行内部会编译 Object result AviatorEvaluator.execute(1 2 * 3); // 方式二先编译再执行 Expression exp AviatorEvaluator.compile(a b); MapString, Object env new HashMap(); env.put(a, 10); env.put(b, 20); Object result exp.execute(env);方式二的性能优势非常明显。我做过一个简单的压测同一个表达式执行 100 万次直接 execute 耗时约 4.2 秒而编译后缓存执行只要 0.3 秒左右差了十几倍。原因在于 Aviator 的编译过程涉及词法分析、语法分析、字节码生成这些开销在缓存模式下只发生一次。编译缓存默认是开启的Aviator 内部用一个 ConcurrentHashMap 按表达式字符串做 key 缓存编译结果。但这个默认缓存有个问题如果表达式是动态拼接的比如带上了用户 ID那缓存会无限膨胀。这时候需要手动管理缓存或者用compile(expression, cached)方法控制是否缓存。3.3 上下文变量的传递与类型映射Aviator 的上下文就是一个MapString, Object你把 Java 对象放进去表达式里就能用。类型映射规则如下Java 类型Aviator 内部类型说明int/long/short/bytelong统一按长整型处理float/doubledouble统一按双精度处理booleanboolean直接映射Stringstring直接映射nullnil直接映射List/Setseq支持索引和迭代Mapmap支持点号和方括号访问其他对象Java 对象通过反射访问属性这里有个性能细节Aviator 访问 Java 对象属性时用的是反射反射本身有开销。如果表达式里频繁访问同一个对象的多个属性可以考虑提前把需要的属性抽出来放进上下文用扁平化的变量代替嵌套访问。比如把user.name、user.age拆成userName、userAge两个变量求值速度会快一些。3.4 编译期选项与运行模式Aviator 提供了一些编译选项通过AviatorEvaluator.setOption()设置。常用的有OPTIMIZE_LEVEL优化级别默认是AviatorEvaluator.COMPILE会做常量折叠等优化。如果表达式里有大量常量运算开启优化能减少运行时的计算量。ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL是否把浮点数解析成 BigDecimal涉及金额计算时建议开启。TRACE_EVAL是否记录求值过程调试时很有用生产环境记得关掉。AviatorEvaluator.setOption(Options.OPTIMIZE_LEVEL, AviatorEvaluator.EVAL); AviatorEvaluator.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true);运行模式上Aviator 支持解释执行和编译执行两种。默认是编译执行把表达式编译成 JVM 字节码。如果遇到某些环境不允许动态生成字节码比如某些安全受限的容器可以切换到解释模式AviatorEvaluator.setOption(Options.EVAL_MODE, AviatorEvaluator.EVAL);解释模式性能会差一些但兼容性更好。大多数场景下用默认的编译模式就行。4. 生产环境里那些文档没写的坑4.1 浮点数精度问题与金额计算前面提到过Aviator 默认把小数按 double 处理。double 在做金额计算时会有精度问题比如0.1 0.2得到的是0.30000000000000004而不是0.3。这在优惠券、账单、结算类业务里是致命的。解决方案有两个。第一个是开启 BigDecimal 模式AviatorEvaluator.setOption(Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL, true);开启后表达式里的浮点字面量会被解析成 BigDecimal运算也按 BigDecimal 规则走精度问题就解决了。但要注意这个选项会影响所有表达式的求值如果有些地方依赖 double 的行为可能会出问题。所以更稳妥的做法是第二个方案在上下文里直接传 BigDecimal 对象表达式里用decimal函数做运算。MapString, Object env new HashMap(); env.put(price, new BigDecimal(19.99)); env.put(count, new BigDecimal(3)); // 表达式里用 decimal 函数 Object result AviatorEvaluator.execute(decimal(price * count), env);4.2 空值处理的正确姿势Aviator 里访问 nil 的属性或方法会直接抛NullPointerException而且异常信息不太友好只告诉你哪个表达式出错了不告诉你是哪个变量为 nil。这在排查线上问题时很头疼。我的做法是在表达式里显式判空用nil关键字// 不安全的写法 user.address.city // 安全的写法 user ! nil user.address ! nil ? user.address.city : 未知如果表达式很长判空会写得很啰嗦。这时候可以注册一个safeGet函数封装判空逻辑AviatorEvaluator.addFunction(new AbstractFunction() { Override public String getName() { return safeGet; } Override public AviatorObject call(MapString, Object env, AviatorObject target, AviatorObject key) { Object obj target.getValue(env); if (obj null) { return AviatorNil.NIL; } String field key.stringValue(env); try { return AviatorRuntimeJavaType.valueOf(PropertyUtils.getProperty(obj, field)); } catch (Exception e) { return AviatorNil.NIL; } } });这样表达式里就能写safeGet(safeGet(user, address), city)虽然还是有点长但至少不会抛异常了。4.3 表达式注入与安全边界Aviator 的表达式是动态执行的如果表达式字符串来自用户输入就存在注入风险。比如用户在输入框里填了一个表达式这个表达式可能访问到你不希望它访问的对象甚至执行一些危险操作。防范措施有几个层面。第一不要直接把用户输入当表达式执行如果业务需要用户自定义规则应该提供受限的语法子集或者用白名单校验表达式里出现的变量名和函数名。第二Aviator 提供了AviatorEvaluator.setFunctionMissing()来限制未注册函数的调用默认情况下调用未注册函数会抛异常这本身就是一道防线。第三如果表达式里需要访问 Java 对象尽量只传必要的数据不要把整个 Service 或者 DAO 对象放进上下文。// 限制只能调用已注册的函数 AviatorEvaluator.setFunctionMissing(env - { throw new ExpressionRuntimeException(函数未注册禁止调用); });4.4 缓存膨胀与内存泄漏前面提到 Aviator 默认会缓存编译结果。如果表达式是动态生成的比如带上了时间戳或者用户 ID缓存会无限增长最终导致 OOM。这个问题在线上环境很隐蔽因为一开始不会有什么症状等缓存积累到几万条时才暴露出来。解决方案是控制缓存大小或者对动态表达式禁用缓存// 禁用缓存 Expression exp AviatorEvaluator.compile(expression, false); // 或者用带缓存的编译但定期清理 AviatorEvaluator.getInstance().getCache().clear();更好的做法是设计上避免动态拼接表达式。如果确实需要根据用户输入生成表达式可以把可变部分抽成变量表达式模板固定下来。比如不要生成user_123_score 100而是生成score 100把user_123_score作为变量名放进上下文。这样表达式模板是有限的缓存不会膨胀。5. 高频场景的实战写法5.1 规则引擎里的条件组合规则引擎是 Aviator 最典型的应用场景。假设有一个促销规则用户等级大于等于 3且近 30 天消费满 500 或 邀请人数超过 5同时不是黑名单用户。用 Aviator 写出来是这样的String rule level 3 (amount30d 500 || inviteCount 5) !blacklist; MapString, Object env new HashMap(); env.put(level, 4); env.put(amount30d, 620); env.put(inviteCount, 3); env.put(blacklist, false); boolean hit (Boolean) AviatorEvaluator.execute(rule, env);这种写法的好处是规则和代码分离运营同学改规则不需要开发介入。但要注意规则里的变量名要和上下文里的 key 严格对应拼错了不会报错只会静默返回 nil导致规则判断走偏。我的做法是在规则配置界面加一个校验功能用一组模拟数据跑一遍表达式确保没有未定义变量。5.2 动态指标计算与报表公式报表系统里经常需要支持用户自定义计算公式比如“毛利率 (营收 - 成本) / 营收 * 100”。Aviator 很适合做这个把指标值放进上下文公式作为表达式执行String formula (revenue - cost) / revenue * 100; MapString, Object env new HashMap(); env.put(revenue, new BigDecimal(10000)); env.put(cost, new BigDecimal(6500)); BigDecimal margin (BigDecimal) AviatorEvaluator.execute(formula, env);这里有个细节如果 revenue 是 0除法会抛ArithmeticException。所以公式里最好加上保护或者用if函数做判断String formula revenue 0 ? 0 : (revenue - cost) / revenue * 100;5.3 与 Spring 项目的集成方式在 Spring 项目里用 Aviator通常会把表达式编译和执行封装成一个 Service方便统一管理缓存和异常。下面是一个简化的实现Service public class ExpressionService { private final ConcurrentHashMapString, Expression cache new ConcurrentHashMap(); private static final int MAX_CACHE_SIZE 1000; public Object eval(String expression, MapString, Object context) { if (cache.size() MAX_CACHE_SIZE) { cache.clear(); } Expression exp cache.computeIfAbsent(expression, AviatorEvaluator::compile); try { return exp.execute(context); } catch (Exception e) { log.error(表达式求值失败: {}, expression, e); throw new BusinessException(规则计算异常); } } }这个实现做了三件事限制缓存大小防止膨胀、统一异常处理、缓存编译结果。实际项目里还可以加上表达式预校验、执行超时控制等。5.4 性能调优的几个关键参数Aviator 的性能已经很好但在极端场景下还能再压一压。几个关键点第一尽量用编译后缓存执行避免重复编译。这个前面说过了收益最大。第二减少上下文里的变量数量。Aviator 在求值时需要从 Map 里查找变量变量越多查找越慢。如果表达式只用到了 3 个变量就不要把整个用户对象和订单对象都放进去。第三避免在表达式里做复杂对象的方法调用。每次方法调用都涉及反射开销比直接访问变量大。如果某个方法调用结果在多次求值中不变提前算好放进上下文。第四合理设置优化级别。默认的COMPILE级别会做常量折叠如果表达式里有大量常量运算开启后能减少运行时计算量。但如果表达式本身很简单优化带来的收益有限反而增加了编译时间。6. 调试与排查的实用手段6.1 用 TRACE_EVAL 定位求值过程Aviator 提供了一个TRACE_EVAL选项开启后会把每一步求值过程打印出来。这个功能在排查“为什么结果不对”时特别有用AviatorEvaluator.setOption(Options.TRACE_EVAL, true); AviatorEvaluator.execute(a b * c, env);输出会显示每一步的中间结果比如先算b * c得到什么再算a 那个结果得到什么。这样你就能快速定位是哪个变量的值不对还是运算顺序有问题。生产环境记得关掉因为打印日志本身有开销。6.2 常见异常与对应原因异常信息常见原因排查方向ExpressionSyntaxErrorException表达式语法错误检查括号是否匹配、运算符是否合法NullPointerException访问了 nil 对象的属性检查变量是否为空加判空逻辑ArithmeticException除数为 0检查除法表达式的分母ClassCastException类型转换失败检查上下文变量的实际类型ExpressionRuntimeException函数调用失败检查自定义函数是否注册、参数是否匹配6.3 表达式预校验的做法线上环境最怕的是表达式运行到一半才报错。我的做法是在规则保存时做一次预校验用一组模拟数据跑一遍表达式确保语法正确、变量齐全、没有运行时异常。预校验的代码大致如下public void validate(String expression, MapString, Object sampleContext) { try { Expression exp AviatorEvaluator.compile(expression); exp.execute(sampleContext); } catch (ExpressionSyntaxErrorException e) { throw new BusinessException(表达式语法错误: e.getMessage()); } catch (Exception e) { throw new BusinessException(表达式执行异常: e.getMessage()); } }预校验不能覆盖所有情况但能拦住大部分低级错误比如拼写错误、括号不匹配、变量名写错等。7. 我踩过的几个真实坑第一个坑是变量名大小写。Aviator 的变量名是大小写敏感的userName和username是两个不同的变量。有一次运营配置规则时写成了username而上下文里传的是userName结果表达式静默返回 nil规则判断全部走 false导致活动没生效。排查了半天才发现是大小写问题。后来我在预校验里加了一条检查表达式里出现的所有变量名是否都在上下文的 key 集合里不在就报错。第二个坑是字符串比较。Aviator 里abc abc的结果取决于字符串是否被常量池优化有时候是 true 有时候是 false。正确做法是用eq(abc, abc)或者abc.equals(abc)。这个坑在规则里比较枚举值的时候特别容易踩比如status ACTIVE如果 status 是从数据库查出来的字符串对象就可能返回 false。第三个坑是编译缓存的 key。Aviator 默认用表达式字符串本身作为缓存 key这意味着两个内容相同但空格不同的表达式会被当成两个不同的表达式各自编译一次。比如a b和ab会占用两个缓存槽位。如果表达式是程序生成的最好先做一次规范化处理去掉多余空格统一格式。第四个坑是自定义函数的线程安全。Aviator 的表达式求值可能发生在多个线程里如果自定义函数内部有共享状态需要做好同步。我见过一个案例自定义函数里用了一个非线程安全的 SimpleDateFormat在高并发下出现了日期解析错误。后来改成 DateTimeFormatter 就好了。8. 关于 Aviator 后续扩展的一些想法Aviator 本身的能力边界很清晰它是一个表达式求值引擎不是完整的规则引擎。如果你需要更复杂的规则编排比如规则优先级、规则链、冲突检测那 Aviator 只能作为底层求值组件上层还需要自己搭一套规则管理框架。我目前的项目里就是这么做的Aviator 负责单条规则的求值规则之间的编排、调度、版本管理由业务层实现。另一个扩展方向是和配置中心结合。把表达式存在配置中心里配置变更时自动刷新编译缓存这样规则调整可以做到秒级生效不需要重启服务。这个方案我们已经在用了效果不错但要注意配置中心的推送延迟和缓存刷新的一致性。最后说一个个人体会Aviator 的语法虽然简单但真正用好的关键在于对边界情况的处理。空值、类型、精度、缓存、安全这五个方面任何一个出问题都可能导致线上故障。我的建议是在把 Aviator 引入核心业务之前先写一套完整的单元测试覆盖各种边界情况然后再逐步放开使用范围。这样即使出了问题也能快速定位和回滚。
网站建设高端定制企业官网