AviatorScript学习记录

是什么

AviatorScript 是一门寄生在 JVM  (Hosted on the JVM)上的语言,类似 clojure/scala/kotlin 等等,AviatorScript的基本过程是将表达式直接翻译成对应的java字节码执行,所以他的性能超越了大部分的解释性表达式引擎,而且除了依赖  commons-beanutils 这个库之外(用于做反射)不依赖任何第三方库

特点

  • 高性能
  • 轻量级
  • 支持运算符重载
  • 原生支持大整数和BigDecimal类型及运算,支持正则表达式以及匹配运算符
  • 。。。

使用场景

  1. 规则判断及规则引擎
  2. 公式计算
  3. 动态脚本控制
  4. 集合数据 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.addseq.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 方法,除了内置的函数库之外,你还可以通过下列方式来实现:

  1. 自定义函数
  2. 自动导入 java 类方法
  3. 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
 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值