AviatorScript 引擎

AviatorScript 编译和执行的入口是 AviatorEvaluatorInstance 类,该类的一个实例就是一个编译和执行的单元,这个单元我们称为一个 AviatorScript 引擎,你可以多个引擎,每个引擎可以设置不同的编译和运行选项,后面我们将详细介绍。

AviatorEvaluator.getInstance() 返回一个全局共享的 AviatorScript 引擎,没有定制化的需求,该默认引擎已足够我们讲解后续的章节。

编译脚本文件

AviatorEvaluatorInstance 有诸多方法,在上一节中,我们用 compileScript(path) 方法编译一个脚本文件,这个方法对文件路径查找按照下列顺序:

  • path 指定的文件系统绝对或者相对路径
  • user.dir 项目的根目录开始的相对路径
  • classpath 下的绝对和相对路径

找到就尝试读取脚本并动态实时地编译成 JVM 字节码,最终的结果是一个 Expression 对象(为什么叫 Expression 而不是 Script ,这是历史遗留问题,因为 aviator 一开始只是一个表达式引擎)。所有的脚本最终编译的结果都是一个 Expression 对象,它经过:

  • Lexer 文法分析
  • Parser 语法解析
  • 一趟优化:常量折叠、常量池化等简单优化。
  • 第二趟生成 JVM 字节码,并最终动态生成一个匿名 Class
  • 实例化 Class,最终的 Expression 对象。

每次调用 compileScript(path) 都生成一个新的匿名类和对象,因此如果频繁调用会占满 JVM 的 metaspace,可能导致 full gc 或者 OOM(关于这一点我们将在最佳实践里更详细的解释),因此还有一个方法 compileScript(path, cached) 可以通过第二个布尔值参数决定是否缓存该编译结果。

  1. Expression exp = AviatorEvaluator.getInstance().compileScript("examples/hello.av", true);
  2. exp.execute();

后面再次调用 compileScript("examples/hello.av", true) 将返回同一个 Expression 对象,哪怕脚本文件发生了修改。

AviatorScript 脚本源码文件约定以 **.av** 作为文件后缀。

编译脚本文本

假设你的脚本存储在其他地方,比如数据库的文本字段,或者远程文件,获取后是一个 String 字符串对象,你可以通过 AviatorEvaluatorInstance#compile(script) 方法来编译,同样结果是一个 Expression 对象。事实上 compileScript 方法最终调用的也是 compile 方法。

  1. // Compile a script
  2. Expression script = AviatorEvaluator.getInstance().compile("println('Hello, AviatorScript!');");
  3. script.execute();

运行结果同样是打印了 Hello, AviatorScript! 。注意,这里字符串 Hello, AviatorScript! 是用单引号括起来的,在 AviatorScript 中字符串可以是双引号括起来,也可以是单引号括起来,作为字面量表达就省去了转义的麻烦。

compile 方法默认不缓存编译结果,同样有缓存的重载版本方法 compile(final String expression, final boolean cached) ,如果第二个参数为 true 将以 script 文本为 key 来缓存编译结果,但是如果你的脚本特别长,用来做缓存 key 会占用比较多的内存,这时候你可以指定缓存 key ,只要调用 compile(final String cacheKey, final String expression, final boolean cached) 方法即可。

执行

编译产生的 Expression 对象,最终都是调用 execute() 方法执行,得到结果。但是 execute 方法还可以接受一个变量列表组成的 map,来注入执行的上下文,我们来一个例子:

  1. String expression = "a-(b-c) > 100";
  2. Expression compiledExp = AviatorEvaluator.compile(expression);
  3. // Execute with injected variables.
  4. Boolean result =
  5. (Boolean) compiledExp.execute(compiledExp.newEnv("a", 100.3, "b", 45, "c", -199.100));
  6. System.out.println(result);

AviatorEvaluator.compileAviatorEvaluator.getInstance().compile 的等价方法。

我们编译了一段脚本 a-(b-c) > 100 ,这是一个简单的数字计算和比较,最终返回一个布尔值。a, b, c 是三个变量(后面我们将详解变量),它们的值都是未知,没有在脚本里明确赋值,那么可以通过外部传参的方式,将这些变量的值注入进去,同时求得结果,比如例子是通过 Expression#newEnv 方法创建了一个 Map<String, Object 的上下文 map,将 a 设置为 100.3,将 b 设置为 45,将 c 设置为 -199.100,最终代入的执行过程如下:

  1. a-(b-c) > 100
  2. => 100.3 - (45 - -199.100) > 100
  3. => 100.3 - 244.1 > 100
  4. => -143.8 > 100
  5. => false

因此返回的 result 就是 false。

这是一个很典型的动态表达式求值的例子,通过复用 Expression 对象,结合不同的上下文 map,你可以对一个表达式反复求值。

同样, compile 方法也有一个缓存模式 compile(script, cached) 用于决定是否缓存编译结果,避免重复生成类和对象。

如果你想获得脚本中没有初始化的变量,可以在执行前做强制校验或者传入默认值等,可以通过 Expression#getVariableNames方法,参见《外部变量》一节。

语法校验

如果只是想简单地校验语法是否合法,可以调用 5.1.0 版本引入的 validate 方法:

  1. AviatorEvaluator.validate("1 +* 2");

抛出异常:

  1. Exception in thread "main" com.googlecode.aviator.exception.ExpressionSyntaxErrorException: Syntax error: invalid token at 3, lineNumber: 1, token : [type='Char',lexeme='*',index=3],
  2. while parsing expression: `
  3. 1 +* 2^^^
  4. `
  5. at com.googlecode.aviator.AviatorEvaluatorInstance.validate(AviatorEvaluatorInstance.java:1373)
  6. at com.googlecode.aviator.AviatorEvaluator.validate(AviatorEvaluator.java:488)
  7. at com.googlecode.aviator.example.SimpleExample.main(SimpleExample.java:8)

引擎模式

默认情况下, AviatorScript 的运行模式是运行期优化优先,会在编译阶段做更多优化,你可以通过选项 Options.OPTIMIZE_LEVEL 来修改,默认是 AviatorEvaluator.EVAL

  1. AviatorEvaluator.getInstance()
  2. .setOption(Options.OPTIMIZE_LEVEL, AviatorEvaluator.COMPILE);

这样就修改为编译优先模式。

Options 拥有大量选项,我们将在后续章节中介绍,你可以参考完整选项说明

IDEA 插件

感谢社区的朋友严长友贡献的 IDEA 插件 https://github.com/yanchangyou/aviatorscript-ideaplugin 有语法高亮、执行功能。使用 idea 的朋友可以尝试。

Next: 2.2 解释运行