Java使用Aviator表达式 学习记录(五)

4,988 阅读6分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

变量

定义和赋值

和其他语言类似, AviatorScript 也允许定义变量,变量有特定的类型和“指向”的值。不过 AviatorScript 中变量的定义和赋值是不可分割的,定义一个变量的同时需要给他赋值:

## examples/type.av

a = 1;
println(type(a));

s = "Hello AviatorScript";
println(type(s));

let p = /(?i)hello\s+(.*)/;
println(type(p));

if s =~ p {
  println($1);
}
``

我们将变量 a 定义并赋值成了整数 1,将变量 s 定义并赋值为字符串 `Hello AviatorScript` ,将变量 p 赋值为一个正式表达式,接下来你就可以正常使用这些变量来参与各种运算,比如上面的例子做了正则匹配,提取 `hello` 后面的字符串。

在这个例子中,我们还使用 `type(x)` 函数来获取 `x` 的类型,它会返回代表具体类型的字符串,上述代码执行将打印:

long string pattern AviatorScript

`type` 函数会将参数的类型以字符串的形式返回,包括:

-   long 整数
-   double 浮点数

<!---->

-   decimal 高精度数字类型
-   bigint 大整数

<!---->

-   boolean 布尔类型
-   string 字符串类型

<!---->

-   pattern 正则表达式
-   range 用于循环语句的 `Range` 类型

<!---->

-   function 函数,参见第七章。
-   nil 特殊变量 nil,下文解释。

## 动态类型

AviatorScript 是**动态类型语言,变量的类型随着赋值而改变**:

examples/type.av

a = 1; println(type(a));

s = "Hello AviatorScript"; println(type(s));

let p = /(?i)hello\s+(.*)/; println(type(p));

if s =~ p { println($1); }

s = 99; println(type(s));

println(a + s);

s 原来是一个字符串,我们通过复制 `s = 99` ,他的类型变为了数字,就可以参与算术运算。


## nil

当我们想表示一个变量还没有赋值的时候,需要用到 `nil`  这个特殊类型,它和 java 中的 `null`  作用一样,表示当前变量还没有赋值。nil 最常见的场景就是用于判断变量是否为 null:

examples/nil.av

a = nil; b = 99;

if a == nil { println("a is " + type(x)); }

a = 3;

if a !=nil { println(a + b); } ``

输出:

a is nil
102

在 Java 中, null 只能参与等于和不等于的比较运算,而在 AviatorScript 中, nil 可以参与所有的比较运算符,只是规定任何类型都比nil大除了nil本身:

## examples/nil.av
a = 3;

println("a > nil: " + (a > nil));
println("nil >= a: " + (nil >= a));
println(""" >= nil: "+ ("" >= nil));
println("nil > "": " + (nil > ""));
println("0.0 > nil: " + (0.0 > nil));
println("nil >= 0.0: " + (nil >= 0.0));
println("/\s/ > nil: " + (/\s/ > nil));
println("nil <= /\s/: " + (nil <= /\s/));
``

输出:

a > nil: true nil >= a: false "" >= nil: true nil > "": false 0.0 > nil: true nil >= 0.0: false /\s/ > nil: true nil <= /\s/: true


除了比较运算符之外, nil 不能参与其他运算,比如你不能将 nil 和一个整数相加,这将报错

c = nil; c + 3;


报错信息: Could not add <JavaType, c, null, null> with <Long, 3>

Exception in thread "main" com.googlecode.aviator.exception.ExpressionRuntimeException: Could not add <JavaType, c, null, null> with <Long, 3> at com.googlecode.aviator.runtime.type.AviatorObject.add(AviatorObject.java:97) at com.googlecode.aviator.runtime.type.AviatorJavaType.add(AviatorJavaType.java:638) ``

传入变量

AviatorScript 一开始是一个表达式引擎,因此是允许执行的时候传入变量,在 2.1 编译和执行我们已经看到一个例子:

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

我们通过 execute(env) 中的 env 来为表达式注入变量,传入的变量的作用域都是默认的脚本内的全局作用域

如果脚本中用到的变量没有传入,并且没有定义,那么默认值将是 nil ****

  String expression = "name != nil ? ('hello, ' + name):'who are u?'";
  Expression compiledExp = AviatorEvaluator.compile(expression);
  // we don't inject variable name
  String s = (String) compiledExp.execute();
  System.out.println(s);

  // inject name
  s = (String) compiledExp.execute(compiledExp.newEnv("name", "dennis"));
  System.out.println(s);

表达式里用三元运算符判断变量 name 是否为 null,并返回不同的值,例子中我们先直接执行,不传入 name,然后再将 name 绑定为 dennis 重新执行:

who are u?
hello, dennis

变量的语法糖

Aviator 有个方便用户使用变量的语法糖, 当你要访问变量a中的某个属性b, 那么你可以通过a.b访问到, 更进一步, a.b.c将访问变量ab属性中的c属性值, 推广开来也就是说 Aviator 可以将变量声明为嵌套访问的形式。

TestAviator类符合JavaBean规范, 并且是 public 的,我们就可以使用语法糖:

package com.googlecode.aviator.example;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.googlecode.aviator.AviatorEvaluator;

public class TestAviator {
  int i;
  float f;
  Date date;

  public TestAviator(final int i, final float f, final Date date) {
    this.i = i;
    this.f = f;
    this.date = date;
  }

  public int getI() {
    return this.i;
  }

  public void setI(final int i) {
    this.i = i;
  }

  public float getF() {
    return this.f;
  }

  public void setF(final float f) {
    this.f = f;
  }

  public Date getDate() {
    return this.date;
  }

  public void setDate(final Date date) {
    this.date = date;
  }

  public static void main(final String[] args) {
    TestAviator foo = new TestAviator(100, 3.14f, new Date());
    Map<String, Object> env = new HashMap<String, Object>();
    env.put("foo", foo);
    System.out.println(AviatorEvaluator.execute("'foo.i = '+foo.i", env));
    System.out.println(AviatorEvaluator.execute("'foo.f = '+foo.f", env));
    System.out.println(AviatorEvaluator.execute("'foo.date.year = '+ (foo.date.year+1990)", env));
  }
}

这个实现的基础是 commons-beanutils,基于反射实现。但是 AviatorScript 也做了优化,如果访问路径上的每一级变量都是 Map,会直接调用 Map#get(key) 访问,避免反射调用

引用变量

对于深度嵌套并且同时有数组的变量访问,例如 foo.bars[1].name,从 3.1.0 版本开始, aviator 通过引用变量来支持(quote variable):

AviatorEvaluator.execute("'hello,' + #foo.bars[1].name", env)
``

引用变量要求以 `#` 符号开始,变量如果包含特殊字符串,需要使用两个 ` 符号来包围,并且变量名中不能包含其他变量,也就是并不支持 `#foo.bars[i].name` 这样的访问,如果有此类特殊需求,请通过[自定义函数](https://www.yuque.com/boyan-avfmj/aviatorscript/xbdgg2)实现。

aviatorscript 内部支持了 3 种特殊语法(基于 `PropertyUtilsBean` 实现):

-   索引 `name[index]` ,name 是数组或者链表可以通过索引位置 index 访问,但是 index 不能是变量或者表达式
-   映射 `name(key)` ,如果 name 是 map 类型,并且 key 的类型为 String,可以通过此表达式获取 value。

<!---->

-   融合的,比如 `name1[index].name2(key)` 。


请注意,因为 `name(key)` 可能被解析为方法调用,因此这里需要上面提到的特殊的语法访问 #`name(key)`


对于一些深度嵌套的 List 或者数组的访问, commons-beanutils 还支持类似 `#map.array.[0].name`这样的访问语法,如果不满足JavaBean规范的,请尝试使用这样的语法做嵌套访问。


此外,对于嵌套变量也支持赋值,只要它有相应的 setter 方法即可:

System.out.println(AviatorEvaluator.execute("foo.i = 100; foo.f + foo.i", env));


这里的例子我们定义的都是脚本范围内的全局变量,局部变量和作用域参见下一节《[3.5 作用域](https://www.yuque.com/boyan-avfmj/aviatorscript/lhht36)》

## 访问 Java 静态变量

从 5.2 开始,你可以直接访问 Java 类的静态变量,比如 `Math.PI` :

examples/static_vars.av

p("Math.PI is: " + Math.PI);

use com.googlecode.aviator.AviatorEvaluator;

p("AviatorEvaluator.VERSION is: " + AviatorEvaluator.VERSION); p("AviatorEvaluator.COMPILE is: " + AviatorEvaluator.COMPILE);

尝试执行将输出:

Math.PI is: 3.141592653589793 AviatorEvaluator.VERSION is: 5.1.5-SNAPSHOT AviatorEvaluator.COMPILE is: 0

对于 `java.lang` 下面的类可以直接访问,比如 `Math` 等,但是对于其他包下面的类,都要求先用 `use` 语法导入该类到当前上下文,然后通过 `Class.Var` 的方式访问,比如上面例子中的 `AviatorEvaluator.VERSION` ,不能通过完整类名的方式来访问,这将报错:


p(com.googlecode.aviator.AviatorEvaluator.VERSION);

Exception in thread "main" com.googlecode.aviator.exception.ExpressionRuntimeException: Could not find variable com.googlecode.aviator.AviatorEvaluator.VERSION at com.googlecode.aviator.runtime.type.AviatorJavaType.getProperty(AviatorJavaType.java:442) at com.googlecode.aviator.runtime.type.AviatorJavaType.getValueFromEnv(AviatorJavaType.java:329) at com.googlecode.aviator.runtime.type.AviatorJavaType.getValue(AviatorJavaType.java:317) at com.googlecode.aviator.runtime.function.system.PrintlnFunction.call(PrintlnFunction.java:54) at Script_1605786165591_47/1644443712.execute0(static_vars.av:11) at com.googlecode.aviator.ClassExpression.executeDirectly(ClassExpression.java:65) at com.googlecode.aviator.BaseExpression.execute(BaseExpression.java:136) at com.googlecode.aviator.Main.main(Main.java:45) Caused by: java.lang.NullPointerException: com at com.googlecode.aviator.runtime.type.AviatorJavaType.fastGetProperty(AviatorJavaType.java:542) at com.googlecode.aviator.runtime.type.AviatorJavaType.getProperty(AviatorJavaType.java:433) ... 7 more


## 特殊变量


AviatorScript 内置了部分特殊变量(启用了 `Feature.InternalVars` 才可以访问):

-   `__exp__` (注意是双下划线),当前表达式的 `Expression` 对象。
-   `__env__` 当前上下文环境 env

<!---->

-   `__instance__` 当前执行引擎 `AviatorEvaluatorInstance` 实例
-   `__args__` 当前函数调用的参数列表。

我们可以简单打印看下 `__env__` 看下:

AviatorEvaluator.execute("let a = 1; p(env)");


输出:

com.googlecode.aviator.utils.Env@7cca494b{ instance=com.googlecode.aviator.AviatorEvaluatorInstance@7ba4f24f, exp=Script_1596184736312_0/1149319664@3b9a45b3, env=, a=1 }


可以看到所有的变量包括内部特殊变量,都可以在 `__env__` 里找到。


## 外部变量(未初始化全局变量)


从编译后的 `Expression` 对象,可以获取脚本中未初始化的全局变量列表,通过 `getVariableNames` 方法:

Expression expression = AviatorEvaluator.compile("b + a", true); List vars = expression.getVariableNames();


`vars` 列表将是 `b` 和 `a` ,更复杂一点的例子:


Expression exp = AviatorEvaluator .compile("b=2; if(a > 1) { a + b } elsif( a > 10) { return a + c; } else { return 10; }"); List vars = exp.getVariableNames();


上面这个 `vars` 列表将是 `[a, c]` 。该方法可以识别出所有脚本中(哪怕是嵌套循环或者闭包)里的未初始化全局变量。


如果你的全局变量名称可能是 `a.b.c` 这样的命名,那么可以使用 `getVariableFullNames()` 来获取完整命名,它和 `getVariableNames` 区别就是会包括了含有 `.` 的变量名。