AviatorScript学习记录
是什么
AviatorScript
是一门寄生在 JVM (Hosted on the JVM)
上的语言,类似 clojure/scala/kotlin
等等,AviatorScript
的基本过程是将表达式直接翻译成对应的java字节码执行,所以他的性能超越了大部分的解释性表达式引擎,而且除了依赖 commons-beanutils
这个库之外(用于做反射)不依赖任何第三方库
特点
- 高性能
- 轻量级
- 支持运算符重载
- 原生支持大整数和
BigDecimal
类型及运算,支持正则表达式以及匹配运算符 - 。。。
使用场景
- 规则判断及规则引擎
- 公式计算
- 动态脚本控制
- 集合数据 ELT 等 ……
为什么要用
- 当我们只需要做一些布尔表达式判定、数据集合处理等等,我们不想引入一堆依赖,并且期待有一定的性能保证。AviatorScript 提供了大量的定制选项,甚至各种语法特性都是可以开关的。
- 我们的表达式或者 script 是用户输入的,我无法保证他们的安全性,我希望控制用户能使用的 API,提供一个相对安全的运行沙箱
- 。。。
怎么用
他有三种运行方式,一种是以java的形式运行,需要引入maven依赖
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>{version}</version>
</dependency>
我们的项目中是用的5.3.3的版本,还一种是命令行工具,直接执行脚本,需要安装下载 aviator 文件,保存在path的路径下修改为可执行文件,我们主要是用到第一种在java中执行的方式
⚠从 5.3 版本开始, AviatorScript 还支持了解释执行模式,这种模式下,将生成 AviatorScript 自身设计的指令并解释执行,这样就不依赖 asm,也不会生成字节码,在 Android 等非标准 Java 平台上就可以运行。
运行一个简单的AviaatorScript脚本
创建文件丢在resources文件夹的examples目录下
## examples/hello.av
println("hello, AviatorScript!");
编写测试类运行
public class RunScriptExample {
public static void main(final String[] args) throws Exception {
Expression exp = AviatorEvaluator.getInstance().compileScript("examples/hello.av");
exp.execute();
}
}
其他例子
public class RunScriptExample {
public static void main(final String[] args) throws Exception {
// 1 -> 基于脚本
Expression exp = AviatorEvaluator.getInstance().compileScript("examples/hello.av");
exp.execute();
// 2 -> 基于变量
String expression = "a+b+(c-1)";
Expression compiledExp = AviatorEvaluator.compile(expression);
Long result =
(Long)compiledExp.execute(compiledExp.newEnv("a", 2, "b",2, "c", 2));
System.out.println(result);
// 3 -> 基于java函数
AviatorEvaluator.setFunctionMissing(JavaMethodReflectionFunctionMissing.getInstance());
System.out.println(AviatorEvaluator.execute("max(4,8)"));
}
}
// -> 输出结果
// hello, AviatorScript!
// 5
// 8
基本类型和语法
AviatorScript
支持常见的几种类型,如数字,布尔值,字符串,也支持大整数,BigDecimal
,正则
数字
AviatorScript
中并没有 byte/short/int
等类型,统一整数类型都为 long,支持的范围也跟 java 语言一样,整数也可以使用十六进制表示,以0X
或者0x
开头的数字,比如0xFF(255),整数可以参与所有的算术运算,比如加减乘除和取模
// 整数除整数也是一个整数 遵循 java 的整数运算规则,运算符优先级也和java一样
let a = 99;
let b = 0xFF;
let c = -99;
println(a + b);
println(a / b);
println(a- b + c);
println(a + b * c);
println(a- (b - c));
println(a/b * b + a % b);
大整数
超过long类型的整数,对应java.math.BigInteger
,超过long范围的变量会自动提升,或者用数字N
结尾
⚠默认的 long 类型在计算后如果超过范围溢出后,不会自动提升为 BigInt,但是 BigInt 和 long 一起参与算术运算的时候,结果为 BigInt 类型
浮点数
浮点数只支持double类型,双精度64位,常用的表示方式有两种一种是带小数点的数字比如1.0012,或者是科学计数法1e-2
高精度计算(Decimal)
一般涉及到精确运算的场景,比如我们项目中用到的薪资计算一般可以采用Decimal来实现,对应java里面的BigDecimal
类型,只要浮点数以为M结尾就会自动识别为decimal类型,比如1.3M
⚠除了 double 以外的数字类型和 decimal 一起运算,结果为 decimal。任何有 double 参与的运算,结果都为 double。
默认运算精度是MathContext.DECIMAL128
,你可以通过修改引擎配置项 Options.MATH_CONTEXT
来改变,如果觉的为浮点数添加 M
后缀比较麻烦,希望所有浮点数都解析为 decimal ,可以开启 Options.ALWAYS_PARSE_FLOATING_POINT_NUMBER_INTO_DECIMAL
选项。
数字类型转换
- 单一类型的运算,结果不变
- 多种类型参与的运算,按照下列顺序:
long -> bigint -> decimal -> double
自动提升,比如 long 和 bigint 运算结果为 bigint, long 和 decimal 运算结果为 decimal,任何类型和 double 一起运算结果为 double
字符串
对应java里面是String字符串类型,单引号或者双引号括起来的文本串,如’helloworld’,变量如果传入的是String或者Character也将转为String类型。
nil类型
nil类型
:常量nil,类似java中的null,但是nil比较特殊,nil不仅可以参与==、!=的比较,也可以参与>、>=、<、<=的比较,Aviator规定任何类型 都n大于nil除了nil本身,nil==nil返回true。用户传入的变量值如果为null,那么也将作为nil处理,nil打印为null。
数组
tuple
函数可以创建一个固定大小的数组,等价 java 的类型为 Object []
let t = tuple(1, 2, "hello", 3.14);
println("type of t: " + type(t));
println("count of t: "+ count(t));
// count 可以获取数组长度, `t[x]` 可以访问索引位置 x 的元素,同样也可以赋值特定位置的元素:
tuple可以丢入任何类型的元素,如果需要创建特定类型的就需要用到seq.array(type, ..args)
,比如创建一个int类型的数组
let a = seq.array(int, 1, 2, 3, 4);
创建空数组使用seq.array_of(type, len)
集合 List, Map 和 Set
// 创建一个list集合
let list = seq.list(1, 2, 3);
// 创建一个全部是x的list个数为10
let list = repeat(10, "a");
// 创建一个map ,`seq.map` 接受偶数个参数或者 0 个参数,不传入任何参数就是一个空的 map,可以通
//过 `seq.put` 来增加元素,对于 map ,你可以用 `m.{key}` 的方式来访问
seq.map(k1, v1, k2, v2 ...)
// 创建一个set
let s = seq.set(1, 2, 2, "hello", 3.3, "hello");
你有一个函数,可以产生元素,你想重复调用 n 次来产生一个集合,可以用 repeatedly(n, fn)
集合的添加和访问使用seq.add
和seq.get
,删除元素使用seq.remove
,
转义
同样,和其他语言类似,遇到特殊字符,AviatorScript 中的字符串也支持转义字符,和 java 语言一样,通过 \
来转义一个字符,比如我们想表示的字符串中有单引号,如果我们继续使用单引号来表示字符串,这时候就需要用到转义符,比如
println('Dennis\'s car');
特殊字符,比如 \r
、 \n
、 \t
等也是同样支持
字符串拼接可以使用加法,或者字符串插值
let name = "aviator";
let a = 1;
let b = 2;
let s = "hello, #{name}, #{a} + #{b} = #{a + b}";
布尔类型和逻辑运算
布尔类型用于表示真和假,它只有两个值 true
和 false
分别表示真值和假值。
x && y
表示并且的关系,x 为真,并且 y 为真的情况下,结果为 true,否则 false。x || y
表示或者的关系, x 为真,或者 y 为真,结果就为 true,两者都为假值的时候结果为 false。!x
否定运算符,如果 x 为 true,则结果为 false,反之则为 true。
三元表达式
和java里面的三元表达式差不多,但是唯一不一样的就是他支持两返回的结果可以不一样
正则表达式
AviatorScript
中正则表达式也是一等公民,作为基本类型来支持, / 括起来的正则表达式就是一个 java.util.Pattern
实例,例如 /\d+/
表示 1 个或者多个数字,正则表达式语法和 java 完全相同,但是对于需要转义的字符不需要连续的反斜杠 \\
,只要一个 \
即可,比如我们要匹配 .av 为结尾的文件,正则可以写成 /^.*\.av$/
,这里的 \.
来转义后缀里的 .
符号。
条件语句
和java差不多,他的值就是实际执行的分支语句的结果
// 代码块都必须用大括号包起来,哪怕是单行语句,这跟 java 是不一样的
if(true) {
println("in if body");
}
let a = if (true) {
1
};
p("a is :" + type(a) +", " + a);
// 输出a is :long, 1
循环语句
AviatorScript 支持 for
和 while
两种循环语句,循环语句的结果是最后一次迭代过程中返回的值
## examples/for_range1.av
for i in range(0, 10) {
println(i);
}
流程控制语句也支持continue/break/return,其中在执行代码块中途跳过剩余代码,继续下个迭代,可以用 continue
,中途跳出迭代,可以用 break,return 有类似 break
的效果,也可以从循环中跳出,但是它会将整个脚本(或者函数)中断执行并返回,而不仅仅是跳出循环
while 语句
也和java差不多
let sum = 1;
while sum < 1000 {
sum = sum + sum;
}
println(sum);
除了条件语句和循环语句还有一种大括号起的块叫Block,这个值就是这个块里最后一个执行的表达式的值
let c = {
let a = 1;
let b = 2;
a + b
};
p("c is :" + type(c) +", " + c);
// c is :long, 3
运算符
支持幂运算,Aviator支持所有的Java位运算符,包括&" “|” “^” “~” “>>” “<<” “>>>
和运算符别名
注释
仅支持 ##
单行注释
异常处理
AviatorScript 完整支持了 java 的异常处理机制,只是做了一些简化:
try {
throw "an exception";
} catch(e) { //AviatorScript 允许不指定异常类型,等价于 `catch(Throwable e)` 。
// 打印异常堆栈 e.printStackTrace()
pst(e);
} finally { // AviatorScript 中同样支持 `finally` 语句,这跟 Java 保持一致
p("finally");
}
从 5.1.0 开始, catch 语句支持单个语句同时捕捉多个异常,当其中一个匹配的时候,就执行分支代码,例如:
try {
throw new java.io.IOException("test");
} catch(IllegalArgumentException | IllegalStateException | java.io.IOException e) {
pst(e);
}
函数
函数可以通过fn
来定义命名一个函数,下面的代码表示我们定义了一个叫add的函数,这个函数接受x,y两个参数,返回两者相加的结果,因为AviatorScript是动态类型系统所以不需要定义参数类型和返回值类型,函数可以通过return语句直接返回,如果没有声明return默认返回表达式的最后一个值(不能加;号)
fn add(x, y) {
return x + y;
}
three = add(1, 2);
println(three);
从 5.2 开始,aviatorscript 支持参数个数的函数重载
fn join(s1) {
"#{s1}"
}
fn join(s1, s2) {
"#{s1}#{s2}"
}
fn join(s1, s2, s3) {
"#{s1}#{s2}#{s3}"
}
p(join("hello"));
p(join("hello", " world"));
p(join("hello", " world", ", aviator"));
从 5.2 版本开始,aviatorscript 也支持了不定参数个数的函数定义,跟 java 的要求类似,也要求可变参数只能出现在参数列表的最后一个位置,并且用 &
作为前缀
fn join(sep, &args) {
let s = "";
let is_first = true;
for arg in args {
if is_first {
s = s + arg;
is_first = false;
}else {
s = s + sep + arg;
}
}
return s;
}
p(join(" ", "a", "b", "c"));
p(join(",", "a", "b", "c", "d"));
p(join(",", "a"));
参数解包
fn add(a, b) {
a + b
}
let list = seq.list(1, 2);
p(add(*list));
匿名函数
匿名函数的基本定义形式是
lambda (参数1,参数2...) -> 参数体表达式 end
自定义函数和调用 Java 方法
如果你想在 AviatorScript 中调用 Java 方法,除了内置的函数库之外,你还可以通过下列方式来实现:
- 自定义函数
- 自动导入 java 类方法
- FunctionMissing 机制
自定义函数
可以通过java代码注入自定义函数
@SneakyThrows
@PostMapping("/system-function")
@ApiOperation(value = "系统内置函数")
public Object calculateBySystemFunction(@RequestBody
@Validated RuleEngineFunctionDTO ruleEngineFunctionDTO) {
return ruleEngineService.calculateBySystemFunction(ruleEngineFunctionDTO);
}
/**
* 系统内置函数
*
* @param ruleEngineFunctionDTO {@link RuleEngineFunctionDTO}
* @return 结果
* @throws IOException
*/
Object calculateBySystemFunction(RuleEngineFunctionDTO ruleEngineFunctionDTO) throws IOException;
@Override
@SneakyThrows
public Object calculateBySystemFunction(RuleEngineFunctionDTO ruleEngineFunctionDTO) {
String functionClassPath = ruleEngineFunctionDTO.getFunctionClassPath();
Class<AbstractFunction> clazz = (Class<AbstractFunction>) Class.forName(functionClassPath);
AviatorEvaluator.addFunction(clazz.getDeclaredConstructor(AbstractFunction.class).newInstance());
return AviatorEvaluator.execute(ruleEngineFunctionDTO.getExpression());
}
@Data
@ApiModel
@NoArgsConstructor
public class RuleEngineFunctionDTO {
@ApiModelProperty(value = "自定义函数路径")
@NotNull(message = "自定义函数不能为空")
String functionClassPath;
@ApiModelProperty(value = "计算表达式")
@NotBlank(message = "计算表达式不能为空")
String expression;
}
@NoArgsConstructor
public class AddFunction extends AbstractVariadicFunction {
@Override
public AviatorObject variadicCall(Map<String, Object> env, AviatorObject... args) {
long res = 0L;
for (AviatorObject arg : args) {
res += Long.parseLong(String.valueOf(arg.getValue(env)));
}
return AviatorRuntimeJavaType.valueOf(res);
}
@Override
public String getName() {
return "add";
}
}
上述例子是实现了自定义可变参数函数的例子,如果固定的参数可以实现AviatorFunction
接口
lambda 自定义函数
除了用 java 代码实现自定义函数之外,你也可以使用 lambda 来定义
AviatorEvaluator.defineFunction("add", "lambda (x,y) -> x + y end");
AviatorEvaluator.execute("add(1,2)"); // 结果为 3
调用 Java 实例方法(基于反射)
JavaMethodReflectionFunctionMissing
是一个特殊的 function missing 实现,它基于反射,自动调用传入的第一个参数的实例方法,将 method(instance, args...)
调用转化为 instance.method(args...)
调用
// 启用基于反射的方法查找和调用
AviatorEvaluator.setFunctionMissing(JavaMethodReflectionFunctionMissing.getInstance());
// 调用 String#indexOf
System.out.println(AviatorEvaluator.execute("indexOf('hello world', 'w')"));
// 调用 Long#floatValue
System.out.println(AviatorEvaluator.execute("floatValue(3)"));
// 调用 BigDecimal#add
System.out.println(AviatorEvaluator.execute("add(3M, 4M)"));
这个方式提供了最大的方法调用灵活性,只要将调用的对象作为第一个参数传入,就会自动查找该对象是否拥有对应的 public 实例方法,如果有,就转为反射调用进行。
当然也存在缺陷:
- 和导入 java 方法类似,性能相比自定义函数较差,接近 3 倍的差距,原因也是反射。
- 无法调用静态方法,静态方法调用仍然需要采用其他两种方式。
- 如果第一个参数为 null,无法找出方法,因为没有对象 class 信息。
当然还有很多种函数定义方式,比如从spring容器中加载自定义函数,从classpath下面加载自定义函数
函数和Runnable,Callable
Aviator 中的函数都实现了 Java 中的 Runnable
和 Callable
接口,只要这个函数是无参的,就可以直接作为 Runnable
和 Callable
的实现使用,比如传给 Thread
构造函数,作为线程任务执行
let r = lambda() ->
p("run in thread");
end;
let t = new Thread(r);
start(t);
join(t);
使用java来自定义模块
除了支持exports 和模块,利用exports.sort = qsort;
将模块暴露出去并且命名qsort,在另外的文件中可以利用let q = require('examples/qsort.av')来进行引入
@Import(ns = "str")
public static class StringModule {
public static boolean isBlank(final String s) {
return s == null || s.trim().length() == 0;
}
}
AviatorEvaluator.getInstance().addModule(StringModule.class);
String script = "let str = require('str'); str.isBlank(s) ";
System.out.println(AviatorEvaluator.execute(script, AviatorEvaluator.newEnv("s", "hello")));
System.out.println(AviatorEvaluator.execute(script, AviatorEvaluator.newEnv("s", " ")));
System.out.println(AviatorEvaluator.execute(script, AviatorEvaluator.newEnv("s", null)));
函数库列表
扩展阅读
转载自:https://juejin.cn/post/7197965654104014906