news 2026/9/25 1:21:23

Aviator 表达式求值引擎语法手册与性能优化实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Aviator 表达式求值引擎语法手册与性能优化实战

1. 为什么我要认真写一份 Aviator 语法手册

第一次接触 Aviator 是在一个规则引擎项目里。业务方要求把“满减、折扣、阶梯计价”这类逻辑从 Java 代码里抽出来,做成运营同学能自己改的配置。当时团队里有人提议用脚本语言,有人提议用 Drools,最后选来选去落在了 Google Aviator 上。原因很朴素:它足够轻,jar 包小,表达式求值快,语法接近 Java,学习成本低,而且不需要引入一整套规则引擎的重型设施。

但真正用起来之后,我发现一个问题:官方文档虽然准确,却偏“参考手册”风格,很多实际开发中会踩的坑、会纠结的选型点,文档里不会展开讲。比如nil和null到底怎么区分,比如自定义函数怎么注册才安全,比如表达式里能不能写多行,比如性能到底在什么量级。这些问题在项目里一个个冒出来,逼着我把 Aviator 的语法和实现细节啃了一遍。

所以这篇内容,我想以“语法手册”为主线,把 Aviator 的核心语法、求值机制、自定义扩展、性能考量和实战避坑经验串起来。它适合正在做规则引擎、动态配置、风控表达式、指标计算的 Java 开发者,也适合准备 Java 面试、想补一补表达式求值引擎这块知识点的同学。读完你至少能做到:看懂 Aviator 表达式、自己写自定义函数、知道哪些写法有性能陷阱、能判断什么场景该用它、什么场景不该用它。

Aviator 本质是一个“表达式求值引擎”,不是完整的脚本语言。它没有循环、没有类定义、没有复杂的控制流,核心就是“给一个表达式字符串和一组变量,算出一个结果”。这个定位决定了它的语法边界,也决定了它的使用姿势。理解这一点,后面很多设计取舍就顺了。

2. Aviator 核心语法全拆解

2.1 字面量与数据类型:从 nil 说起

Aviator 支持的字面量类型包括数字、字符串、布尔值、nil、正则表达式。数字统一用long和double表示,整数默认是long,带小数点的是double。字符串用单引号或双引号都行,转义规则和 Java 类似。

这里最容易被问到的就是nil。Aviator 里的nil是一个特殊值,表示“空”,它和 Java 的null不是一回事。nil在 Aviator 内部是一个Nil对象,参与运算时有自己的规则。比如nil == nil是true,nil == 1是false。而 Java 的null传进表达式后,Aviator 会把它当作nil处理,但如果你在自定义函数里返回null,Aviator 也会转成nil。

// Java 侧传入变量 Map<String, Object> env = new HashMap<>(); env.put("a", null); env.put("b", 1); // 表达式 Object result = AviatorEvaluator.execute("a == nil", env); // true Object result2 = AviatorEvaluator.execute("a == b", env); // false

注意:nil只能用于比较和判空,不要试图对nil做算术运算,否则会抛异常。实际项目里我一般会在表达式入口做一层变量清洗,把业务上的null统一转成nil或者默认值,避免表达式里到处写判空。

布尔值就是true和false,没什么好说的。正则表达式字面量用/pattern/表示,比如/^\d+$/,这个在字符串匹配场景里挺方便,但要注意 Aviator 的正则底层用的是 Java 的Pattern,性能上和 Java 一致。

2.2 运算符优先级与结合性

Aviator 的运算符基本照搬 Java,优先级从高到低大致是:一元运算符、乘除取模、加减、比较、相等、逻辑与、逻辑或、三元、赋值。这里有个细节:Aviator 支持=~和!~做正则匹配,这两个运算符的优先级和比较运算符同级。

实际写表达式时,我强烈建议不要依赖优先级记忆,该加括号就加括号。原因很简单:表达式是给运营或配置人员看的,可读性比省几个字符重要得多。我见过一个线上事故,就是因为a || b && c的优先级理解偏差,导致风控规则放行了不该放行的订单。后来我们团队定了个规矩:所有逻辑组合必须显式加括号。

// 不推荐 AviatorEvaluator.execute("a > 1 || b < 2 && c == 3", env); // 推荐 AviatorEvaluator.execute("(a > 1) || ((b < 2) && (c == 3))", env);

三元运算符condition ? a : b在 Aviator 里是支持的,而且可以嵌套。但嵌套超过两层就会很难读,我一般会拆成多个表达式或者用自定义函数封装。

2.3 变量引用与属性访问

Aviator 的变量引用很直接,直接写变量名就行。如果变量是 Java Bean,可以用obj.field访问属性,也可以用obj.method()调用方法。数组和 List 用arr[0]访问,Map 用map.key或map['key']访问。

这里有个性能点:Aviator 对属性访问做了缓存,第一次访问会反射解析,后续会走缓存。但如果你用的是map['key']这种动态 key,缓存效果会差一些。所以在高频求值场景,尽量用固定属性名。

// 假设 env 里有个 User 对象 env.put("user", new User("张三", 25)); AviatorEvaluator.execute("user.name", env); // 张三 AviatorEvaluator.execute("user.age > 18", env); // true AviatorEvaluator.execute("user.getName()", env); // 张三

提示:Aviator 默认不允许调用任意方法,需要在编译时开启Feature.AllowAllMethod或者用AviatorEvaluator.setFunctionMissing做白名单。生产环境千万别直接开全量方法调用,这是个安全口子。

2.4 集合与序列操作

Aviator 支持seq库,可以对集合做map、filter、reduce等操作。语法是seq.map(list, lambda)这种形式。lambda 用->定义,比如x -> x + 1。

List<Integer> list = Arrays.asList(1, 2, 3, 4, 5); env.put("list", list); // 过滤出大于 2 的元素 Object result = AviatorEvaluator.execute("seq.filter(list, x -> x > 2)", env); // [3, 4, 5] // 求和 Object sum = AviatorEvaluator.execute("seq.reduce(list, 0, (acc, x) -> acc + x)", env); // 15

这个功能在指标计算场景里很好用,比如从一批订单里筛选出有效订单再求和。但要注意,seq库的函数是内置的,如果你用了自定义的AviatorEvaluatorInstance,需要确认seq库已经加载。

2.5 字符串处理与常用函数

Aviator 内置了string库,提供substring、length、contains、startsWith、endsWith、split、trim、toUpperCase、toLowerCase等函数。用法是string.xxx(str, args...)。

AviatorEvaluator.execute("string.contains('hello world', 'world')", env); // true AviatorEvaluator.execute("string.length('hello')", env); // 5 AviatorEvaluator.execute("string.substring('hello', 1, 3)", env); // el

数学函数在math库里,比如math.abs、math.round、math.floor、math.ceil、math.max、math.min、math.pow、math.sqrt。这些在计价、评分场景里很常用。

AviatorEvaluator.execute("math.round(3.14159 * 100) / 100.0", env); // 3.14 AviatorEvaluator.execute("math.max(a, b)", env);

注意:Aviator 的数字运算默认是long和double,没有BigDecimal。如果你做金额计算,直接用它算会有精度问题。我的做法是:表达式里只做逻辑判断,金额计算走自定义函数,内部用BigDecimal。

3. 自定义函数与扩展机制

3.1 为什么需要自定义函数

内置函数覆盖不了业务逻辑。比如“判断用户是否在黑名单”“计算两个日期相差天数”“根据用户等级返回折扣率”,这些都得自己写。Aviator 提供了AviatorFunction接口,实现call方法就行。

public class IsBlackListFunction extends AbstractFunction { @Override public String getName() { return "isBlackList"; } @Override public AviatorObject call(Map<String, Object> env, AviatorObject arg1) { String userId = FunctionUtils.getStringValue(arg1, env); boolean result = BlackListService.contains(userId); return result ? AviatorBoolean.TRUE : AviatorBoolean.FALSE; } }

注册方式有两种:全局注册和实例注册。全局注册用AviatorEvaluator.addFunction,实例注册用AviatorEvaluatorInstance.addFunction。生产环境我建议用实例注册,避免不同业务模块的函数互相污染。

AviatorEvaluatorInstance instance = AviatorEvaluator.newInstance(); instance.addFunction(new IsBlackListFunction());

3.2 函数参数处理与类型转换

Aviator 的函数参数都是AviatorObject,需要自己转成 Java 类型。FunctionUtils提供了一堆工具方法,比如getStringValue、getNumberValue、getBooleanValue。这里有个坑:如果参数是nil,getStringValue会返回null,你得自己判空。

@Override public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) { String str = FunctionUtils.getStringValue(arg1, env); if (str == null) { return AviatorNil.NIL; } int num = FunctionUtils.getNumberValue(arg2, env).intValue(); // ... }

提示:自定义函数里不要抛异常,尽量返回nil或默认值。因为表达式求值过程中抛异常会中断整个流程,而业务上往往希望“某个条件不满足就跳过”。我一般会在函数内部 catch 所有异常,记录日志后返回nil。

3.3 自定义函数的性能考量

自定义函数的调用开销主要在参数装箱和类型转换上。如果函数在热点路径上被频繁调用,可以考虑用AviatorFunction的call重载版本,减少AviatorObject的创建。另外,函数内部不要做重操作,比如查数据库、调远程接口。如果必须做,加缓存。

我做过一个压测:一个简单的自定义函数,每秒能调用 100 万次左右;如果函数内部有一次 Redis 查询,每秒就降到几千次。所以表达式求值本身很快,慢的是你塞进去的业务逻辑。

4. 编译模式与性能优化实战

4.1 解释执行 vs 编译执行

Aviator 默认是解释执行,每次execute都会解析表达式、生成 AST、然后求值。如果同一个表达式要反复求值,应该用compile编译一次,然后反复execute。

Expression expression = AviatorEvaluator.compile("a + b > 100"); for (int i = 0; i < 10000; i++) { Map<String, Object> env = new HashMap<>(); env.put("a", i); env.put("b", i * 2); expression.execute(env); }

编译后的Expression对象是线程安全的,可以缓存起来复用。我一般用ConcurrentHashMap做表达式缓存,key 是表达式字符串,value 是编译后的Expression。

4.2 编译缓存与失效策略

Aviator 自身有编译缓存,AviatorEvaluator.compile默认会缓存。但缓存大小有限,可以通过AviatorEvaluator.setExpressionCacheSize调整。如果表达式是动态生成的,缓存命中率会很低,这时候要考虑自己控制缓存。

// 设置缓存大小 AviatorEvaluator.setExpressionCacheSize(10000); // 或者自己管理缓存 private final ConcurrentHashMap<String, Expression> cache = new ConcurrentHashMap<>(); public Object eval(String expr, Map<String, Object> env) { Expression expression = cache.computeIfAbsent(expr, AviatorEvaluator::compile); return expression.execute(env); }

注意:如果表达式里引用了自定义函数,而函数逻辑变了,缓存里的表达式不会自动失效。所以函数变更后要清缓存,或者用版本号做 key。

4.3 性能实测数据与调优建议

我在一台 4 核 8G 的机器上做过简单压测,表达式是a + b * c > 100 && d == 'test',变量都是基本类型。结果大致是:解释执行每秒约 50 万次,编译执行每秒约 200 万次。如果表达式里有属性访问,性能会降到 20 万次左右。如果用了seq库做集合操作,性能取决于集合大小。

调优建议就几条:热点表达式必须编译;变量尽量用基本类型;避免在表达式里做复杂集合操作;自定义函数加缓存;表达式长度控制在合理范围,太长的表达式解析开销大。

5. 常见问题与避坑指南

5.1 类型转换异常怎么排查

最常见的异常是ClassCastException和NumberFormatException。原因通常是变量类型和表达式预期不符。比如表达式里写a > 10,但a传的是字符串"abc",就会抛异常。

排查方法:在execute外面包一层 try-catch,把表达式和 env 都打出来。另外可以用AviatorEvaluator.execute的validate方法先校验表达式语法。

try { Object result = expression.execute(env); } catch (Exception e) { log.error("表达式求值失败, expr={}, env={}", expr, env, e); throw new BizException("规则计算失败"); }

5.2 nil 与 null 的坑

前面提过,nil和null在 Aviator 里是两套东西。Java 传null进去,Aviator 会转成nil。但如果你在自定义函数里返回null,Aviator 也会转成nil。问题出在比较上:nil == null在 Aviator 里是false,因为null不是 Aviator 的字面量。

所以表达式里判空统一用nil,不要用null。另外,nil参与字符串拼接会变成空字符串,参与数字运算会抛异常。

5.3 表达式注入与安全控制

Aviator 默认不允许调用任意方法,但如果你开了Feature.AllowAllMethod,就等于把 Java 方法调用权交给了表达式编写者。如果表达式来源不可信,这是严重的安全漏洞。

我的做法是:永远不开全量方法调用;自定义函数做白名单;表达式来源做校验,只允许特定字符集;求值超时做限制。Aviator 本身没有超时机制,需要自己在外部用 Future 做超时控制。

ExecutorService executor = Executors.newSingleThreadExecutor(); Future<Object> future = executor.submit(() -> expression.execute(env)); try { return future.get(100, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { future.cancel(true); throw new BizException("表达式求值超时"); }

5.4 常见问题速查表

问题现象可能原因解决方法
ClassCastException变量类型与表达式预期不符检查 env 中变量类型,必要时做转换
NumberFormatException字符串转数字失败用自定义函数做安全转换
表达式求值结果为 nil变量不存在或函数返回 nil检查变量名拼写,函数返回值
性能突然下降表达式未编译或缓存失效检查编译缓存,热点表达式预编译
自定义函数不生效注册到了错误的实例确认注册的 instance 与求值的一致
正则匹配失败正则语法错误或转义问题用 Java Pattern 先验证正则

提示:Aviator 的异常信息有时候不够具体,比如只告诉你“求值失败”,不告诉你是哪个变量。我的经验是,在开发阶段把日志级别调到 DEBUG,Aviator 会打印更多细节。生产环境则要做好兜底,表达式失败不能影响主流程。

6. 实际项目中的使用体会

我在项目里用 Aviator 主要做三件事:一是营销活动的满减折扣计算,二是风控规则的条件判断,三是数据指标的动态聚合。这三类场景对 Aviator 的要求不一样。

营销计算场景,表达式相对固定,但金额精度要求高。我的做法是表达式只做条件判断,比如user.level == 'VIP' && order.amount > 100,具体折扣金额走自定义函数,内部用BigDecimal算。这样既利用了 Aviator 的灵活性,又避开了浮点精度问题。

风控场景,表达式变化频繁,运营同学会自己改。这时候安全控制最重要。我们做了三层防护:表达式字符白名单、自定义函数白名单、求值超时限制。另外所有表达式变更都要走审核流程,上线前用历史数据回跑验证。

指标聚合场景,表达式里会用到seq库。这里要注意集合大小,如果集合有几十万条数据,seq.filter会遍历整个集合,性能很差。我的做法是先在 Java 侧做粗筛,把数据量降下来,再用 Aviator 做精细计算。

最后分享一个小技巧:Aviator 的Expression对象可以拿到getVariableNames(),用来分析表达式依赖了哪些变量。这个在排查“变量未传”问题时特别有用。我一般会在编译后打印一次变量名列表,和实际传入的 env key 做对比,能快速发现遗漏。

Expression expression = AviatorEvaluator.compile(expr); System.out.println(expression.getVariableNames()); // [a, b, c]

这个功能还可以用来做依赖分析,比如一个规则依赖了哪些用户属性,提前做数据准备。踩过几次坑之后,我现在养成了习惯:任何动态表达式上线前,先用getVariableNames过一遍,确认变量来源都覆盖了,再放行。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 1:19:44

问道私服数据库一键导入:多源异构文件识别与自动化迁移方案

简介&#xff1a;本资源是一套专为问道游戏1.6版本定制的数据库快速部署工具&#xff0c;面向游戏运维工程师、后端开发人员及数据库管理员&#xff0c;解决多表结构批量导入耗时长、易出错、版本不一致等实际问题。压缩包为ZIP格式&#xff0c;仅含1个核心SQL文件&#xff08;…

作者头像 李华
网站建设 2026/9/25 1:19:43

零成本自建监控:摄像头+单板机+网盘实现24小时录像与回放

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:19:27

Buck电路误差放大器选型:普通运放与跨导运放对比

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:18:42

气象时序回归实战:从数据清洗到多任务预测

简介&#xff1a;本资源是一份面向高校机器学习课程学习者的综合性大作业实践包&#xff0c;聚焦天气预测这一典型时间序列建模任务&#xff0c;帮助学生系统掌握从数据预处理、特征工程到模型训练与评估的全流程技能。压缩包共603KB&#xff0c;虽未提供具体文件明细&#xff…

作者头像 李华
网站建设 2026/9/25 1:18:30

行为树不是AI算法,而是游戏AI的工程化骨架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 1:18:24

LabWindows/CVI图像处理工程包解析与实战指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华