☰
Aviator预编译模式:Java表达式引擎性能提升的关键
2026/10/9 16:21:05 网站建设 项目流程

开头:别小看那几毫秒的表达式执行

做Java后端的朋友,八成都在项目里用过表达式引擎。规则校验、动态计算、灰度开关、报表指标……这类需求要是硬编码,改一次发一次版,运维同学迟早要找你谈心。所以引入一个轻量的表达式引擎成了常规操作,而Aviator正是很多团队的首选——它够轻、够快、语法够直白。

但Aviator隐藏着一个关键分水岭:同样的表达式,以默认模式执行,和以预编译模式执行,性能差距能达到一个数量级。我见过不少项目,规则引擎接进来了,表达式也跑通了,但就是没做预编译这一步。结果上线后接口平均耗时涨了好几倍,排查半天发现罪魁祸首是那个“看起来差不多”的表达式求值。

这篇文章就围绕Aviator的表达式执行模式来聊。我会把默认的解释执行、预编译模式、底层差异、适用场景、实际代码,以及我踩过的坑,一次性讲透。无论你是刚准备引入Aviator的新手,还是已经在线上运行但想优化性能的开发者,下面这些内容都值得看完。

1. 表达式引擎执行模式全景

1.1 Aviator到底是什么

Aviator是一个轻量级、高性能的Java表达式求值引擎。它不是完整的脚本语言,也不像Groovy那样背上整个动态语言运行时,它只专注做一件事:把字符串形式的表达式算出来。比如"a + b * 2",传进去变量a=3, b=4,它返回11。就这么简单。

但简单背后藏着不少设计取舍。Aviator的核心定位是高性能,所以它在编译和求值环节做了大量优化。理解它的执行模式,是榨干性能的前提。

1.2 标题里的“默认模式”到底是什么

用过Aviator的朋友都知道,最常见的调用方式是:

Object result = AviatorEvaluator.execute("a + b * 2", params);

这一行代码背后,Aviator会在每次调用时完成“字符串解析 -> 词法分析 -> 语法分析 -> 生成AST -> 求值”的完整链路。这意味着,同一个表达式如果你调用10000次,前面这些解析步骤就要重复10000次。这就是所谓的“默认解释执行模式”。

1.3 预编译模式:把解析工作提前做完

预编译模式的核心思路很朴素:既然表达式字符串在运行期是不变的,那第一次调用时把解析和编译结果缓存下来,后面直接用,省掉重复的语法分析开销。

Aviator提供了非常直白的方式:

Expression compiledExpr = AviatorEvaluator.compile("a + b * 2"); Object result = compiledExpr.execute(params);

compile方法返回一个Expression对象,这个对象内部已经完成了表达式解析和代码生成(或AST的构建)。之后每次调用execute,直接基于编译产物求值,绕开了“字符串 -> AST”这条慢路径。

1.4 两种模式的差异,一句话说清

用生活化类比解释:默认模式好比每次用菜谱做菜,都要先把菜谱从头读到尾、理解步骤、再下锅;预编译模式好比一个老师傅把菜谱熟记于心,客人点菜直接开炒。菜谱不变的时候,显然后者效率碾压。

不过真实场景没那么绝对,默认模式也不是一无是处。它适合表达式动态变化、调用频率极低的场景,因为省掉了缓存管理的复杂度。预编译模式则适合表达式相对固定、调用频率高的场景。怎么选,后面我会专门分析。

2. 默认解释执行的底层机制与性能真相

2.1 每次execute究竟经历了什么

要理解性能差异,必须先拆解AviatorEvaluator.execute(String)每次调用时内部做了什么。这里我以Aviator 5.x版本的实现逻辑展开(老版本4.x原理类似,只是内部实现细节有差异)。

整个过程大致分为四步:

  1. 词法分析:读取表达式字符串,拆分出标识符、数字、运算符、括号等token。比如"a + b * 2"会被拆成a、+、b、*、2。
  2. 语法分析:根据文法规则,把token序列组织成抽象语法树(AST),比如识别出*的优先级高于+,生成以+为根节点、a和b * 2为子节点的树结构。
  3. 优化与代码生成:Aviator在语法分析后还有一些优化步骤,比如对常量表达式做折叠、短路逻辑处理等。在某些模式下,它还会生成Java字节码(这是Aviator性能优秀的核心原因之一)。
  4. 求值:遍历AST(或执行生成的字节码),结合运行时传入的变量环境,计算出最终结果。

注意,在高版本Aviator中,execute(String)内部实际上也会尝试做缓存。这个细节很多文档没提,但我实际看过源码,AviatorEvaluator内部有一个表达式缓存机制,并不是每次execute(String)都完全从零解析。那为什么还要显式预编译?因为默认缓存只在特定条件下生效,而且有大小和策略限制。具体后面我会讲。

2.2 解释执行的性能数据:别被表象骗了

有人可能会说,既然内部有缓存,那每次 execute(String) 是不是也没那么慢?要分场景。

我做过一个简单的基准测试,环境为 JDK8、Aviator 5.2.4、表达式"a + b * c - d / e",循环执行100万次,对比两种方式。

执行方式100万次耗时(大约值)单次平均耗时
execute(String) 不预热约2300ms约2300ns
compile后execute约120ms约120ns

上面是不预热的数据。如果先预热(把表达式分别跑10次)再测试:

执行方式100万次耗时(预热后)单次平均耗时
execute(String)约880ms约880ns
compile后execute约115ms约115ns

可以看到,即便预热后,默认模式仍然比预编译慢7倍左右。如果表达式更复杂、嵌套更深,差距还会拉大,达到10倍以上一点都不夸张。

2.3 默认缓存策略的坑

Aviator的execute(String)其实维护了一个内部缓存,但有两个问题:

一是默认缓存容量有限。它内部有一个LRU缓存,默认容量我记得是几个数值(具体看版本,比如某些版本默认100),如果表达式数量超过了容量,老表达式会被淘汰,下次又要重新解析。

二是缓存key匹配是全字符串精确匹配。也就是说,表达式字符串有任何细微差别——哪怕多个空格——都会产生新的缓存条目。如果你在代码里动态拼接表达式,每次都生成带随机后缀的字符串,缓存就会形同虚设。

所以默认模式适合“表达式总量少且调用不频繁”的场景,一旦表达式动态性高、调用量又大,缓存就容易失效,性能就会失控。

2.4 什么时候放心用默认模式

默认模式不是坏东西,它在下面这些场景反而更合适:

  • 表达式完全由外部传入,每次都不同,比如用户自定义的计算公式。
  • 调用频率极低,比如一分钟才执行一次的管理后台校验。
  • 表达式数量极少,比如只有两三个固定的简单表达式,且调用总量不大。
  • 快速原型验证,还没到优化阶段。

在这些场景下,强行引入预编译和缓存管理,反而增加代码复杂度。性能优化要讲性价比,没必要为了省0.1ms把架构搞复杂。

3. 预编译模式:编译原理与性能跃迁的关键

3.1 compile之后到底生成了什么

Aviator的预编译核心在于compile(String)返回的Expression对象。根据版本和配置不同,这个Expression背后的实现可能是:

  • AST解释器模式:表达式被解析成AST后存储,execute时遍历求值。
  • 字节码模式:Aviator会把表达式编译成Java字节码,动态生成一个Class,然后通过反射调用。这种方式最接近手写Java的性能。

自Aviator 3.0开始,字节码模式就已经成为主要卖点。Aviator号称是“唯一一个支持直接装载脚本表达式并生成字节码的轻量级表达式引擎”。在5.x版本里,编译机制更加成熟,生成的代码更接近手写最优Java代码。

简单说,预编译之后,表达式的求值不再有“理解语义”的开销,只有“执行计算”的开销。这就是性能跃迁的根本原因。

3.2 Expression接口的正确使用姿势

看一段标准代码:

import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.Expression; import java.util.HashMap; import java.util.Map; public class AviatorCompileDemo { // 表达式固定,编译一次,全局复用 private static final Expression EXPR = AviatorEvaluator.compile("a + b * 2"); public static void main(String[] args) { Map<String, Object> params = new HashMap<>(); params.put("a", 3); params.put("b", 4); for (int i = 0; i < 100; i++) { Object result = EXPR.execute(params); System.out.println(result); } } }

这里有几个关键点:

  • Expression是线程安全的,这是官方明确保证的。编译后的对象可以被多个线程并发调用execute,不会产生状态冲突。但要注意,execute传入的Map不能被同时修改(这属于调用方责任)。
  • compile只需要在应用启动或第一次使用时执行一次,然后复用同一个实例。如果你在每次调用时都compile一次,那和默认模式的区别就只剩缓存命中的概率了。
  • 变量名写在表达式里,运行期通过Map传入。Aviator支持Map和AviatorObject两种方式,日常用Map足够。

3.3 带参数的execute:让性能再进一步

除了基本execute(Map),Expression还提供了一些重载方法,可以减少Map构造开销:

Object result = EXPR.execute(3, 4); // 对应 a, b 的顺序 Object result2 = EXPR.execute(3, 4, 5); // 对应 a, b, c

前提是在编译时指定变量顺序,使用compile(String, boolean)的变体或者通过AviatorEvaluator.compile(script, true)开启“变量缓序优化”?这里需要说明一下,Aviator有一个enableUnsafe相关的能力,但更常见的用法是:

Expression expr = AviatorEvaluator.compile("a + b * 2", true);

第二个参数cached表示是否缓存表达式内部变量列表。如果为true,execute时可以直接按位置参数传入,省掉Map的hash查找开销。

我实测下来,按位置传参比Map传参大约能再快10%~20%。如果你的表达式参数结构固定,这个优化值得做。但必须注意,位置传参的顺序必须与表达式内部变量首次出现的顺序一致,否则会得到错误结果。

3.4 编译模式与解释模式的混合使用

Aviator本身允许你通过配置来控制默认行为。例如:

// 设置表达式缓存容量 AviatorEvaluator.setExpressionCacheCapacity(10000); // 开启/关闭自动缓存(不同版本API略有差异) AviatorEvaluator.setCachedExpireEnabled(true);

但这里我不建议依赖全局配置去替代显式compile。原因很简单:显式编译让你在代码层面明确知道哪些表达式是常驻的,出了性能问题容易排查;全局缓存只是黑盒优化,表达式的生命周期不受你控制,反而容易踩到缓存淘汰的坑。

3.5 预编译模式带来的额外收益:静态校验

预编译的另一个好处是,表达式语法错误可以在compile阶段就暴露。默认模式下,表达式语法错误要等到第一次execute才会抛异常,如果那次调用恰好发生在线上流量高峰期,可能就影响了实时请求。而预编译可以在应用启动阶段做一次表达式合法性自检,把问题挡在发布之前。

我在不少项目里都做了一个启动自检模块:把所有规则表达式在初始化阶段统一compile一遍,有任何语法错误直接fail-fast,让应用启动失败而不是带病上线。这个习惯帮我挡掉过很多次低级错误——比如少写一个括号、拼错一个函数名。

4. 实操:从接入到优化的完整过程

4.1 环境准备与依赖引入

以Maven项目为例,引入Aviator最新稳定版:

<dependency> <groupId>com.googlecode.aviator</groupId> <artifactId>aviator</artifactId> <version>5.4.3</version> </dependency>

如果项目用的是JDK8,建议用5.x版本(支持JDK6+,但高版本可能需要JDK8)。JDK11及以上没有任何问题。Aviator不依赖第三方包,引入后即用。

4.2 场景1:固定规则的高频计算

比如订单金额计算,规则是"折扣价 * 数量 + 运费 - 优惠券抵扣"。这个表达式在系统里是固定的,但每天要被调用几十万次。

优化方案:

import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.Expression; import java.util.HashMap; import java.util.Map; public class OrderCalculator { private static final Expression ORDER_AMOUNT_EXPR = AviatorEvaluator.compile("discount_price * quantity + freight - coupon_deduction"); public static BigDecimal calculate(BigDecimal discountPrice, BigDecimal quantity, BigDecimal freight, BigDecimal couponDeduction) { Map<String, Object> env = new HashMap<>(8); env.put("discount_price", discountPrice); env.put("quantity", quantity); env.put("freight", freight); env.put("coupon_deduction", couponDeduction); return (BigDecimal) ORDER_AMOUNT_EXPR.execute(env); } }

这里要注意的是,Expression可以做成static final常量,在类加载时编译一次。如果规则可能动态更新(比如运营改规则),可以把Expression放到一个支持刷新的缓存Map里,结合配置中心更新。比如:

private volatile Map<String, Expression> expressionCache = new ConcurrentHashMap<>(); public void refreshExpression(String ruleId, String expression) { expressionCache.put(ruleId, AviatorEvaluator.compile(expression)); } public Object eval(String ruleId, Map<String, Object> env) { Expression expr = expressionCache.get(ruleId); if (expr == null) { throw new IllegalArgumentException("rule not found: " + ruleId); } return expr.execute(env); }

4.3 场景2:大量表达式的批量求值

如果你有一个列表,每行数据需要用不同的表达式计算,或者相同的表达式对大量数据逐行计算,差别的本质还是表达式是否复用。

批量处理时,一个常见错误是在循环内部使用execute(String):

// 错误示范:100万次execute(String) = 重复解析 + 缓存命中损耗 for (DataItem item : list) { Object result = AviatorEvaluator.execute("a + b > threshold ? 'Y' : 'N'", buildEnv(item)); }

正确做法:

Expression expr = AviatorEvaluator.compile("a + b > threshold ? 'Y' : 'N'"); for (DataItem item : list) { Object result = expr.execute(buildEnv(item)); }

这两个循环的性能差距有多大?用前面那段基准测试数据估算,100万次场景下,前者可能耗时900ms,后者约120ms,差距接近8倍。数据量越大,差距越明显。

4.4 场景3:结合Spring管理表达式对象

在Spring项目中,建议把常用表达式做成@Component里的单例:

@Component public class RiskRuleEngine { private static final String DEFAULT_RISK_RULE = "score >= riskScore && amount <= maxAmount && !blacklisted"; private final Expression riskExpr; public RiskRuleEngine() { this.riskExpr = AviatorEvaluator.compile(DEFAULT_RISK_RULE); } public boolean isRisk(ScoreBO score, BigDecimal amount, String userId) { Map<String, Object> env = new HashMap<>(8); env.put("score", score.getValue()); env.put("riskScore", score.getRiskThreshold()); env.put("amount", amount); env.put("maxAmount", score.getMaxAmount()); env.put("blacklisted", blacklistService.contains(userId)); return (Boolean) riskExpr.execute(env); } }

这样表达式编译发生在Bean初始化阶段,不会污染请求链路。如果规则需要动态更新,可以使用@RefreshScope或手动刷新,但要注意并发切换时的原子性——用volatile引用即可。

4.5 解决一个关键疑问:编译后的Expression是否线程安全

这里单独拎出来说,是因为我在很多群里看到过争论。Aviator官方文档明确说明Expression是线程安全的,其内部不持有可变的、与单次执行绑定状态。

但下面几种操作需要自行加锁:

  • 同一个Map实例被多个线程同时放进execute执行,且这个Map在外部还被修改。
  • 编译后动态改变表达式内部变量(不存在的操作,除非重新compile)。
  • 使用了Aviator的静态全局配置(如AviatorEvaluator.setXX),这些配置是全局的,并发执行时若频繁修改全局配置,可能影响其他线程。

简单说,Expression本身可以放心中复用,但调用方要保证传入的环境Map是线程隔离的。最常见的做法是每次执行时新建Map,或者使用ThreadLocal缓存Map。

4.6 性能测试:怎么测才靠谱

如果要在自己的项目里验证预编译的收益,不要只跑一次就下结论。这里给一个规范的测试思路:

  1. 准备一组有代表性的表达式,包含算术、逻辑、字符串处理、三元运算。
  2. 准备足够大的循环次数,建议至少10万次,避免JIT未热身导致数据失真。
  3. 测试前先做预热,把代码跑个几千次,让JIT充分编译。
  4. 分别测试默认模式和预编译模式,记录耗时。
  5. 建议用JMH做基准测试,或者至少循环多次取平均值。

下面是JMH的一个简化思路(伪代码示意):

@Benchmark public void testExecuteString() { AviatorEvaluator.execute("a + b * c - d / e", env); } @Benchmark public void testCompiledExecute() { compiledExpr.execute(env); }

实测时记得把env定义为@State变量,避免Map构建影响结果对比。

5. 常见问题与排查技巧实录

5.1 症状:预编译后首次调用仍然很慢

这个现象很常见,原因有两个:

  • ClassLoader加载和字节码生成:Aviator在编译时会动态生成Class,首次执行需要触发类加载、链接、初始化,这个过程需要几毫秒到几十毫秒不等。解决方案是应用启动时做一次预热,主动调用一次execute。
  • JIT编译:Aviator生成的Java方法调用链也需要经过JIT的“解释->编译”过程,预热几万次后性能才会稳定。

靠谱的预热方式:

@Component public class AviatorPreheatRunner implements ApplicationRunner { @Override public void run(ApplicationArguments args) { Map<String, Object> env = new HashMap<>(); env.put("a", 1); env.put("b", 2); // 预编译并预热所有核心表达式 for (Expression expr : coreExpressions) { expr.execute(env); } } }

5.2 症状:缓存命中但速度依然上不去

如果你用了默认execute(String),且确认表达式没有变化,但仍然很慢,检查一下是否在循环内创建了大量表达式字符串拼接。比如:

for (int i = 0; i < n; i++) { String expr = "a + b + " + i; // 每次生成新表达式 AviatorEvaluator.execute(expr, env); }

这种场景下,每次都是全新的字符串,缓存永远命中不了。正确的设计是把i作为变量传入表达式,而不是拼进字符串。表达式写成"a + b + i",通过环境Map或位置参数传i。

5.3 症状:使用位置参数时结果不对

使用execute(Object... args)按位置传参时,Aviator要求参数顺序与表达式变量出现的顺序保持一致。这个顺序并不总是直观,特别是表达式中有重复变量时。

举例:

Expression expr = AviatorEvaluator.compile("a + b + a", true); Object result = expr.execute(2, 3); // 结果应该是 2 + 3 + 2 = 7

如果你写成了expr.execute(3, 2),结果就错了。所以在用位置参数前,建议打印一下表达式的变量列表:

// 通过 expression.getVariableNames() 查看变量顺序 List<String> vars = expr.getVariableNames(); System.out.println(vars); // [a, b]

5.4 症状:内存占用偏高或OOM

Aviator编译表达式会产生Class对象(尤其在字节码模式下),如果动态编译了海量不同的表达式,会占用Metaspace。默认模式虽然不生成Class,但AST对象同样占用堆内存。

排查建议:

  • 确认表达式总数是否存在无界增长。典型场景是execute(String)传入了动态拼接的表达式,导致内部缓存持续膨胀。
  • 显式调用AviatorEvaluator.clearExpressionCache()或在低峰期清除缓存。
  • 对Expression使用new WeakReference或自定义容器管理,避免长期持有。

Aviator本身有缓存容量上限,不是无限增长,但如果你故意在代码里塞几百万个表达式,就另当别论了。务必保证表达式集合是有限的、受控的。

5.5 症状:Aviator解析器未开启括号优化

在旧版本中,表达式中有大量括号、嵌套复杂函数时,默认模式下的解析性能会剧烈下降。5.x版本已经优化了许多。如果你还在用3.x/4.x,强烈建议升级到5.x。升级可能带来一些API不兼容,但整体收益非常大。我在一个老项目里从4.x升到5.x,同一个表达式集合的批量求值耗时下降了约40%。

5.6 症状:与其他表达式引擎对比,Aviator优势在哪

经常有人问,Aviator和SpEL、OGNL、Groovy相比怎么选。说几点实际感受:

  • 与SpEL比,Aviator更轻,不依赖Spring容器,性能通常更优,但若项目已经强依赖Spring,SpEL集成更方便。
  • 与OGNL比,Aviator语法更规范,类型支持更好,没有Struts的包袱。
  • 与Groovy比,Aviator不追求脚本语言的全量特性,执行性能高得多,适合纯计算场景。

Aviator不支持动态定义函数、类等重量级特性,它做的是“把表达式算出来”,并且算得飞快。选型时不要拿它当脚本语言用。

6. 执行模式的选型建议与全局配置心得

6.1 一张表判断你该用哪种模式

场景特征适合模式理由
表达式数量极少(<10),调用频率高预编译 + 常量缓存一次编译,长期复用,性能最优
表达式动态变化,每次内容不同默认模式预编译无法命中,缓存没意义
表达式固定,但数量极多(万级)预编译 + LRU缓存控制总表达式数,避免缓存无界增长
调用频率低,不在乎单次耗时默认模式简单直接,无需额外维护
表达式来自不可信外部输入默认模式 + 严格校验编译阶段同样会执行恶意代码?Aviator本身不支持调用任意Java方法,安全性有保障,但还是要限制长度

6.2 全局配置的合理使用

AviatorEvaluator提供了一些静态配置,我建议只改这几项:

// 调大表达式缓存容量(默认值因版本而异,5.x默认是100?我习惯显式设置) AviatorEvaluator.setExpressionCacheCapacity(2000); // 设置缓存过期策略(如果版本支持) AviatorEvaluator.setCachedExpireEnabled(true); // 开启严格模式(遇到未知变量直接抛异常,而不是返回null) AviatorEvaluator.setFeature(AviatorEvaluator.FEATURE_STRICT, true);

FEATURE_STRICT是我强烈建议开启的。默认情况下,Aviator遇到表达式里引用了未传入的变量,会把它当作null处理,这很容易掩盖数据缺失问题。开启严格模式后,遇到缺失变量直接抛异常,可以在开发期就发现问题。

6.3 结合配置中心动态更新规则

预编译不等于不能更新。如果规则引擎需求频繁调整表达式,你可以把表达式字符串放在配置中心(如Apollo、Nacos),当配置变更时重新编译并替换内存中的Expression引用。推荐写法:

public class DynamicRuleManager { private volatile Map<String, Expression> rules = new ConcurrentHashMap<>(); public void updateRule(String key, String expression) { Expression newExpr = AviatorEvaluator.compile(expression); rules.put(key, newExpr); } public Object execute(String key, Map<String, Object> env) { Expression expr = rules.get(key); if (expr == null) { throw new IllegalStateException("rule not exists: " + key); } return expr.execute(env); } }

volatile保证多线程下的可见性,ConcurrentHashMap保证不同key之间的并发安全。更新规则时只影响后续请求,正在执行的请求不受影响,因为execute用的是对象引用的快照。

6.4 关于启动自检的经验

前面提过,我在项目里会加一个自检机制启动时编译所有表达式。具体做法:

@Component public class ExpressionValidator { @PostConstruct public void validate() { List<String> expressions = ruleConfig.getAllExpressions(); for (String expr : expressions) { try { AviatorEvaluator.compile(expr); } catch (Exception e) { throw new IllegalStateException("表达式非法: " + expr, e); } } } }

一次发布前,运维从配置中心拉下来的规则里有个表达式少了一个等号,如果不在启动阶段拦截,等用户触发那条规则时才会炸出异常。自检机制上线后,这类低级错误基本被消灭在了发布阶段。

7. 再聊几个容易被忽略的细节

7.1 数值类型与精度

Aviator对数字的默认处理值得注意。它会把整数解析为Long,带小数点的解析为Double,如果涉及BigDecimal需要显式声明类型。比如计算金额,建议表达式使用decimal类型转换函数:

Expression expr = AviatorEvaluator.compile("decimal(price) * count");

如果直接用Double算金额,精度丢失的坑迟早会来。我在订单金额场景中吃过亏,所以只要和钱相关,一律显式用decimal()包装。

7.2 表达式中的序列与集合操作

Aviator支持map、seq、filter、reduce等函数,可以在表达式层面做集合处理。这些函数在字节码模式下性能不错,但如果集合很大,建议把集合处理放在Java代码里,只把最终标量传入表达式,降低表达式复杂度、提高可读性,也便于调优。

7.3 编译模式下的调试手段

Aviator可以生成并保存编译后的字节码,便于在本地调试。方式是通过系统属性:

// 开发环境临时开启 System.setProperty("aviator.debug", "true");

开启后Aviator会把生成的字节码输出到指定目录。我遇到过表达式执行结果不符预期的情况,就是靠看生成的字节码定位到类型转换问题。当然,生产环境不要开启,会产生额外IO开销。

7.4 性能优化不止在编译模式

预编译是最大的优化点,但还有几个容易叠加的优化手段:

  • 复用环境Map:如果每次execute都需要构建大量参数的Map,可以用ThreadLocal缓存Map实例,每次执行前clear再put。
  • 使用位置参数:前面提到过,可省去一部分hash查找开销。
  • 减少表达式复杂度:能拆分成两个简单表达式的,不要硬合成一个复杂表达式。复杂表达式无论是在编译期还是求值期开销都更大。

实测中,这几个优化叠加起来,相比最朴素的execute(String)能达到10倍以上的提升。强烈建议追求极致性能的团队把这几个手段都用上。

最后分享一点我的个人体会

Aviator用了这么些年,我最深刻的感受是:它的上限很高,但很多人只用了它的下限。默认的execute(String)足够方便,但生产环境里,只要表达式是相对固定的,花两行代码改成compile+ 复用,收益立竿见影。不要为了“省事”一直停留在默认模式,尤其是当你的接口被调用频率逐渐上去之后。

我踩过最贵的坑,是在一个日调用量千万级的接口里用了默认模式。当时接口RT从50ms一路涨到300ms,GC频率飙升,最后定位到AviatorEvaluator.execute(String)的解析开销和缓存竞争上。改成预编译后,RT降到80ms左右。那一次优化让我意识到,表达式引擎的“简单用法”和“正确用法”之间,隔着一道需要主动跨过的门槛。

如果你接下来要接Aviator,我的建议很简单:凡是你能预知的固定表达式,全部在启动阶段编译成Expression静态常量;凡是从配置中心来的动态规则,用并发容器挂载编译结果;启动时做一次全量编译自检。做到这三点,基本就能避开我在文章里提到的所有大坑。

预编译的收益不仅仅是那几毫秒,它带来的是可预期的性能边界和更早期的错误暴露。工具是好工具,剩下的就看你怎么用了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询