咱们好,我是老三,之前同事用了一款轻量级的规矩引擎脚本AviatorScript,老三也跟着用了起来,真的挺香,能少写许多代码。这期就给咱们介绍一下这款规矩引擎。

简介

AviatorScript 是一门高功用、轻量级寄宿于 JVM (包含 Android 渠道)之上的脚本语言。

它起源于2010年,作者对其时已有的一些产品不是很满意,所以自己撸了一个,它是Groovy的一个定制化的子集。

这款轻量级规则引擎,真香!

相比较一些传统的规矩引擎,比方DroolsJessJRules,它更加轻量级,而且功用更好,一起才能敞开,扩展很便利。

咱们来看(吹)看(吹)AviatorScript的特点:

  1. 它支撑数字、字符串、正则表达式、布尔值等根本类型,而且能够运用一切 Java 运算符进行运算。
  2. 还有一个内置的东西叫做 bigintdecimal,能够处理超大整数和高精度运算。而且咱们还能够经过运算符重载让它们运用普通的算术运算符 +-*/
  3. 语法非常完全,能够用它来写多行数据、条件句子、循环句子,还能处理词法作用域和反常处理等等。
  4. 假如咱们喜欢函数式编程,还有一个叫做 Sequence 笼统的东西,能够让你更便利地处理调集。
  5. 还有一个轻量化的模块系统,便利咱们组织代码。
  6. 假如咱们需求调用 Java 办法,也没问题,能够用多种方法便利地调用 Java 办法,还有一个完好的脚本 API能够让你从 Java 调用脚本。
  7. 功用也是超出想象的好,假如运用 ASM 模式,它会直接将脚本翻译成 JVM 字节码,解释模式还能够在 Android 等非标准 Java 渠道上运转。

AviatorScript能够用在各种场景,比方规矩判别和规矩引擎、公式核算、动态脚本操控,乃至调集数据 ELT 等等。能够说适当全能了。

快速开端

AviatorScript 是一门寄生在 JVM (Hosted on the JVM)上的语言,相似 clojure/scala/kotlin 等等,咱们从写个Hello World开端。

  • 创立一个SpringBoot项目,引入依赖,这儿挑选的是最新版别
        <dependency>
            <groupId>com.googlecode.aviator</groupId>
            <artifactId>aviator</artifactId>
            <version>5.3.3</version>
        </dependency>

PS:能够看到aviator的groupId有一个googlecode,可是它和Google可没什么关系,这是因为早期aviator保管在Google的一个开源项目保管渠道Google Code。

  • 在项目的resource目录下创立一个目录script,在script目录下创立脚本hello.av
println("Hello, AviatorScript!");
  • 编写一个单元测试,运转脚本
    @Test
    void testHello() throws Exception {
        //获取路径
        ClassPathResource resource = new ClassPathResource("script/hello.av");
        String scriptPath = resource.getPath();
        //编译
        Expression exp = AviatorEvaluator.getInstance().compileScript(scriptPath);
        //履行
        exp.execute();
    }

终究履行一下,就能够看到输出:

Hello, AviatorScript!
  • 咱们也能够直接把脚本界说成字符串,用compile()来进行编译
    @Test
    void testHelloStr() throws Exception {
        //界说脚本
        String script="println(\"Hello, AviatorScript!\");";
        //编译
        Expression exp = AviatorEvaluator.getInstance().compile(script);
        //履行
        exp.execute();
    }

AviatorScript有一个Idea插件,支撑直接编译运转Aviator脚本,比较便利。

这款轻量级规则引擎,真香!

但不足之处,这个插件现已不怎么保护了,只兼容到了Idea2021版别。

这款轻量级规则引擎,真香!

AviatorScript脚本的运转,分为两步,编译履行

这款轻量级规则引擎,真香!

编译支撑编译脚本文件和脚本文本,分别运用compileScriptcompile办法。

编译发生的 Expression 目标,终究都是调用 execute() 办法履行。

这儿有个重要才能,execute 办法能够承受一个变量列表组成的 map,来注入履行的上下文:

        String expression = "a-(b-c) > 100";
        Expression compiledExp = AviatorEvaluator.compile(expression);
        //上下文
        double a=100.3,b=45,c= -199.100;
        Map<String, Object> context=new HashMap<>();
        context.put("a",a);
        context.put("b",b);
        context.put("c",c);
        //经过注入的上下文履行
        Boolean result = (Boolean) compiledExp.execute(context);
        System.out.println(result);

咱们完成一些规矩的判别便是基于这个才能,把一些参数下上下文传进去,然后进行逻辑判别。

根本语法

咱们在来看看AviatorScript的根本语法,它的语法适当简洁,比较接近于数学表达式的方法。

根本类型及运算

AviatorScript 支撑常见的类型,如数字、布尔值、字符串等等,一起将大整数、BigDecimal、正则表达式也作为一种根本类型来支撑。

数字

AviatorScript 支撑数字类型,包含整数和浮点数,以及高精度核算(BigDecimal)。数字类型能够进行各种算术运算。

整数和算术运算

整数类型,对应Java中的long类型,能够表明范围为 -9223372036854774808 ~ 9223372036854774807 的整数。整数能够运用十进制或十六进制表明。

let a = 99;
let b = 0xFF;
let c = -99;
println(a + b); // 270
println(a / b); // 0
println(a - b + c); // -156
println(a + b * c); // -9801
println(a - (b - c)); // 198
println(a / b * b + a % b); // 99

整数能够进行加减乘除和取模运算。需求留意的是,整数相除的成果依然是整数,遵循整数运算规矩。能够运用括号来指定运算的优先级。

浮点数

浮点数类型对应Java中的double类型,表明双精度 64 位浮点数。浮点数能够运用十进制或科学计数法表明。

let a = 1.34159265;
let b = 0.33333;
let c = 1e-2;
println(a + b); // 1.67492265
println(a - b); // 1.00826265
println(a * b); // 0.4471865500145
println(a / b); // 4.0257402772554
println(a + c); // 1.35159265

浮点数能够进行加减乘除运算,成果依然为浮点数。

高精度核算(Decimal)

高精度核算运用 BigDecimal 类型,能够进行精确的数值核算,适用于货币运算或许物理公式运算的场景。能够经过在数字后边增加 “M” 后缀来表明 BigDecimal 类型。

let a = 1.34M;
let b = 0.333M;
let c = 2e-3M;
println(a + b); // 1.673M
println(a - b); // 1.007M
println(a * b); // 0.44622M
println(a / b); // 4.022022022M
println(a + c); // 1.342M

BigDecimal 类型能够进行加减乘除运算,成果依然为 BigDecimal 类型。默认的运算精度是 MathContext.DECIMAL128,能够经过修正引擎装备项 Options.MATH_CONTEXT 来改动。

数字类型转化

数字类型在运算时会主动进行类型转化:

  • 单一类型参加的运算,成果依然为该类型。
  • 多种类型参加的运算,依照 long -> bigint -> decimal -> double 的次序主动提高,成果为提高后的类型。

能够运用 long(x) 函数将数字强制转化为 long 类型,运用 double(x) 函数将数字强制转化为 double 类型。

let a = 1;
let b = 2;
println("a/b is " + a/b); // 0
println("a/double(b) is " + a/double(b)); // 0.5

a 和 b 都是 long 类型,它们相除的成果依然是整数。运用 double(b) 将 b 转化为 double 类型后,相除的成果为浮点数。

字符串

字符串类型由单引号或双引号括起来的接连字符组成。能够运用 println 函数来打印字符串。

let a = "hello world";
println(a); // hello world

字符串的长度能够经过 string.length 函数获取。

let a = "hello world";
println(string.length(a)); // 11

字符串能够经过 + 运算符进行拼接。

let a = "hello world";
let b = "AviatorScript";
println(a + ", " + b + "!" + 5); // hello world, AviatorScript!5

字符串还包含其他函数,如截取字符串 substring,都在 string 这个 namespace 下,具体见函数库列表。

布尔类型和逻辑运算

布尔类型用于表明真和假,它只要两个值 truefalse 分别表明真值和假值。

比较运算如大于、小于能够发生布尔值:

println("3 > 1 is " + (3 > 1));  // 3 > 1 is true
println("3 >= 1 is " + (3 >= 1));  // 3 >= 1 is true
println("3 >= 3 is " + (3 >= 3));  // 3 >= 3 is true
println("3 < 1 is " + (3 < 1));  // 3 < 1 is false
println("3 <= 1 is " + (3 <= 1));  // 3 <= 1 is false
println("3 <= 3 is " + (3 <= 3));  // 3 <= 3 is true
println("3 == 1 is " + (3 == 1));  // 3 == 1 is false
println("3 != 1 is " + (3 != 1));  // 3 != 1 is true

上面演示了一切的逻辑运算符:

  • > 大于
  • >= 大于等于
  • < 小于
  • <= 小于等于
  • == 等于
  • != 不等于

根本语法

AviatorScript也支撑条件句子和循环句子。

条件句子

AviatorScript 中的条件句子和其他语言没有太大区别:

  • if
if(true) {
   println("in if body");
}
  • ifelse
if(false){
   println("in if body");
} else {
   println("in else body");
}
  • ifelsifelse
let a = rand(1100);
if(a > 1000) {
  println("a is greater than 1000.");
} elsif (a > 100) {
  println("a is greater than 100.");
} elsif (a > 10) {
   println("a is greater than 10.");
} else {
   println("a is less than 10 ");
}

循环句子

AviatorScript提供了两种循环句子:forwhile

for循环:遍历调集

for ... in 句子通常用于遍历一个调集,例如下面是遍历 0 到 9 的数字

for i in range(0, 10) {
  println(i);
}

在这儿,range(start, end) 函数用于创立一个整数调集,包含起始值 start,但不包含完毕值 end。在循环迭代过程中,变量 i 绑定到调集中的每个元素,并履行大括号 {...} 中的代码块。

range 函数还能够承受第三个参数,表明递增的步长巨细(默认步长为 1)。例如,咱们能够打印出0到9之间的偶数:

for i in range(0, 10, 2) {
  println(i);
}

for .. in 能够用于任何调集结构,比方数组java.util.Listjava.util.Map 等等。

while循环

while 循环本质上是将条件句子与循环结合在一起。当条件为真时,不断履行一段代码块,直到条件变为假。

例如,下面的示例中,变量 sum 从 1 开端,不断累加自身,直到超过 1000 才中止,然后进行打印输出:

let sum = 1;
while sum < 1000 {
  sum = sum + sum;
}
println(sum);

循环能够用这三个关键字完毕——continue/break/return

  • continue用于越过当前迭代,持续下一次迭代。
  • break用于跳出整个循环。
  • return用于中断整个脚本(或函数)的履行并回来。

函数

咱们再来看看AviatorScript一个非常重要的特性——函数。

函数

函数界说和调用

AviatorScript中运用fn语法来界说函数:

fn add(x, y) {
  return x + y;
}
three = add(1, 2);
println(three);  // 输出:3
s = add('hello', ' world');
println(s);  // 输出:hello world

咱们这儿经过fn关键字来界说了一个函数,函数名为add,它承受两个参数xy,并回来它们的和。

需求留意的是,AviatorScript是动态类型系统,不需求界说参数和回来值的类型,它会依据实践传入和回来的值进行主动类型转化。因此,咱们能够运用字符串来调用add函数。

函数的回来值能够经过return句子来指定,也能够省略不写。在函数体内,假如没有清晰的return句子,终究一个表达式的值将被作为回来值。

自界说函数

再来给咱们介绍一个AviatorScript里非常好的特性,支撑自界说函数,这给AviatorScript带来了非常强的扩展性。

能够经过 java 代码完成并往引擎中注入自界说函数,在 AviatorScript 中就能够运用,事实上一切的内置函数也是经过相同的方法完成的:

public class TestAviator {
    public static void main(String[] args) {
        //通通创立一个AviatorEvaluator的实例
        AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
        //注册函数
        instance.addFunction(new AddFunction());
        //履行ab脚本,脚本里调用自界说函数
        Double result= (Double) instance.execute("add(1, 2)");
        //输出成果
        System.out.println(result);
    }
}
/**
 * 完成AbstractFunction接口,就能够自界说函数
 */
class AddFunction extends AbstractFunction {
    /**
     * 函数调用
     * @param env 当前履行的上下文
     * @param arg1 第一个参数
     * @param arg2 第二个参数
     * @return 函数回来值
     */
    @Override
    public AviatorObject call(Map<String, Object> env,
                              AviatorObject arg1, AviatorObject arg2) {
        Number left = FunctionUtils.getNumberValue(arg1, env);
        Number right = FunctionUtils.getNumberValue(arg2, env);
        //将两个参数进行相加
        return new AviatorDouble(left.doubleValue() + right.doubleValue());
    }
    /**
     * 函数的称号
     * @return 函数名
     */
    public String getName() {
        return "add";
    }
}

咱们看到:

  • 承继AbstractFunction类,就能够自界说一个函数
  • 重写call办法,就能够界说函数的逻辑,能够经过FunctionUtils获取脚本传递的参数
  • 经过getName能够设置函数的称号
  • 经过addFunction增加一个自界说函数类的实例,就能够注册函数
  • 终究就能够在Aviator的脚本里编译履行咱们自界说的函数

好了,关于AviatorScript的语法咱们就不过多介绍了,咱们能够直接查看官方文档[1],可读性适当不错。

接下来咱们就来看看AviatorScript的实践运用,看看它到底怎么提高项目的灵活性。

实战案例

标题带了规矩引擎,在咱们的项目里也主要是拿AviatorScript作为规矩引擎运用——咱们能够把AviatorScript的脚本保护在装备中心或许数据库,进行动态地保护,这样一来,一些规矩的修正,就不必大动干戈地去修正代码,这样就更加便利和灵活了。

这款轻量级规则引擎,真香!

客户端版别操控

在日常的开发中,咱们许多时分或许面临这样的状况,兼容客户端的版别,尤其是Android和iPhone,有些功用是低版别不支撑的,或许说有些功用到了高版别就抛弃掉,这时分假如硬编码去兼容就很麻烦,那么就能够考虑运用规矩脚本的方法。

  • 自界说版别比较函数:AviatorScript没有内置版别比较函数,可是能够利用它的自界说函数特性,自己界说一个版别比较函数
class VersionFunction extends AbstractFunction {
        @Override
        public String getName() {
            return "compareVersion";
        }
        @Override
        public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
            // 获取版别
            String version1 = FunctionUtils.getStringValue(arg1, env);
            String version2 = FunctionUtils.getStringValue(arg2, env);
            int n = version1.length(), m = version2.length();
            int i = 0, j = 0;
            while (i < n || j < m) {
                int x = 0;
                for (; i < n && version1.charAt(i) != '.'; ++i) {
                    x = x * 10 + version1.charAt(i) - '0';
                }
                ++i; // 越过点号
                int y = 0;
                for (; j < m && version2.charAt(j) != '.'; ++j) {
                    y = y * 10 + version2.charAt(j) - '0';
                }
                ++j; // 越过点号
                if (x != y) {
                    return x > y ? new AviatorBigInt(1) : new AviatorBigInt(-1);
                }
            }
            return new AviatorBigInt(0);
        }
    }
  • 注册自界说函数:为了便利运用各种自界说函数,咱们一般界说一个单例的AviatorEvaluatorInstance,把它注册成Bean
    @Bean
    public AviatorEvaluatorInstance aviatorEvaluatorInstance() {
        AviatorEvaluatorInstance instance = AviatorEvaluator.getInstance();
        // 默认开启缓存
        instance.setCachedExpressionByDefault(true);
        // 运用LRU缓存,最大值为100个。
        instance.useLRUExpressionCache(100);
        // 注册内置函数,版别比较函数。
        instance.addFunction(new VersionFunction());
    }    
  • 在代码里传递上下文:接下来,就能够在事务代码里将一些参数放进履行上下文,然然后进行编译履行,留意编译的时分最好要开启缓存,这样功率会高许多
    /**
     * 
     * @param device 设备
     * @param version 版别
     * @param rule 规矩脚本
     * @return
     */
    public boolean filter(String device,String version,String rule){
        // 履行参数
        Map<String, Object> env = new HashMap<>();
        env.put("device", device);
        //编译脚本
        Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
        //履行脚本
        boolean isMatch = (boolean) expression.execute(env);
        return isMatch;
    }
  • 编写脚本:接下来就能够编写和保护对应的规矩脚本,这些规矩脚本通常放在在装备中心或许数据库,便利进行动态变更
if(device==bil){
    return false;
}
## 操控android的版别
if (device=="Android" && compareVersion(version,"1.38.1")<0){
    return false;
}
return true;

这样一来,假如某天,客户端Bug或许产品原因,需求修正客户端和客户端的版别操控,直接修正脚本就好了。

乃至咱们能够在env里放进更多参数,比方uid,能够完成简单的黑白名单。

咱们的自界说函数除了这种简单的比较版别,咱们还能够放一些杂乱的逻辑,比方判别是否新用户等等。

营销活动规矩

假如现在咱们的运营期望进行一场营销活动,对用户进行一定的付出优惠,最开端的一版活动规矩:

  • 满1000减200,满500减100

这个好写,一顿if-else就完事了。

可是没过几天,又改了活动规矩:

  • 首单用户一致减20

好,啪啪改代码。

又曩昔几天,活动规矩又改了:

  • 随机优惠不同金额

为了一些多变的营销规矩,大动干戈,不停修正代码,耗时吃力,那么不如用规矩脚本完成:

  • 界说脚本
if (amount>=100){
    return 200;
}elsif(amount>=500){
    return 100;
}else{
    return 0;
}
  • 事务代码调用
    public BigDecimal getDiscount(BigDecimal amount,String rule){
        // 履行规矩并核算终究价格
        Map<String, Object> env = new HashMap<>();
        env.put("amount", amount);
        Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
        return  (BigDecimal) expression.execute();
    }

接下来,再发生营销规矩变更,就能够少数开发(自界说函数,比方判别首单用户),而且能够组件化地保护营销规矩。

订单风控规矩

Aviator我在订单风控里运用也很香,风控的规矩调整是适当频频的,比方一个电商网站,常常要依据买卖的争议率、买卖体现等等,来收紧和放松风控规矩,这就要求咱们能对一风控规矩进行快速地装备变更。

例如,依据订单金额、客户评级、收货地址等特点,主动判别是否有危险并触发相应的风控操作。

  • 规矩脚本
if (amount>=1000 || rating <= 2){
    return "High";
}elsif(amount >= 500 || rating<=4){
    return "Mid";
}else{
    return "Low";
}
  • 代码调用:这儿只是简单回来了一个风控等级,其实能够经过Map的方法回来多个参数。
    public String riskLevel(BigDecimal amount,String rating,String rule){
        // 履行规矩并核算终究价格
        Map<String, Object> env = new HashMap<>();
        env.put("amount", amount);
        env.put("rating", rating);
        Expression expression = aviatorEvaluatorInstance.compile(DigestUtils.md5DigestAsHex(rule.getBytes()), rule, true);
        return  (String) expression.execute();
    }

上面顺手列出了几个简单的例子,AviatorScript 还能够用在一些审批流程、事情处理、数据质量管理等等场景……

在一些轻量级的需求规矩引擎的场景下,AviatorScript 真的太香了,尤其是它的扩展性,支撑经过Java自界说函数,我乃至能够在脚本里查询数据库、查询Redis、调用外部接口……这样就能够像搭积木相同建立想要的功用。

总结

这一期给咱们分享了一款轻量级的规矩脚本语言AviatorScript,它的语法丰富,可是很轻量,而且支撑非常灵活的扩展,在项目中运用能够有效提高事务的灵活性,下降开发的工作量。

原本这期想浅浅盘点一下AviatorScript的设计完成,成果发现越盘越深,所以就独自拆出来,放在下一期了,敬请期待。


终究放个小彩蛋,作者在官方文档里挂了一个收款码,这个名字左看右看,觉得像是作者太太呢?——作者应该很爱(怕bushi)老婆吧!

这款轻量级规则引擎,真香!



参考:

[1].www.yuque.com/boyan-avfmj…

[2].github.com/killme2008/…