表达式引擎Aviator实战

概述

Aviator是一门高性能、轻量级的Java语言实现的表达式动态求值引擎。其设计目标是轻量级和高性能,相对于Groovy、JRuby的笨重,Aviator非常小,不过Aviator的语法受限,它并不是一门完整的语言,只是语言的一小部分集合。定位是介于Groovy这样重量级脚本语言和IKExpression这样轻量级表达式引擎之间。

Aviator的实现思路与其它轻量级的求值器不同,其它求值器是通过解释的方式运行,而Aviator是直接将表达式编译成Java字节码,交给JVM去执行。

功能

  • 支持大部分运算操作符:算数运算符、关系运算符、逻辑操作符、正则表达式匹配操作符、三元表达式,支持操作符的优先级及括号的强制优先级
  • 支持函数调用和自定义函数
  • 自动类型转换,当执行操作时,会自动判断操作数类型并做相应的转换,无法转换就抛异常
  • 支持传入变量,支持类似a.b.c的嵌套变量访问

限制:没有if else、do while等语句,没有赋值语句,没有位运算符;

使用场景

  • 公式计算
  • 动态脚本控制
  • 规则判断以及规则引擎

实战

pom文件引入:

<dependency>
	<groupId>com.googlecode.aviator</groupId>
	<artifactId>aviator</artifactId>
</dependency>

入门

Aviator的数值类型只支持Long和Double,任何整数都将会转换成Long,任何浮点数都会转换成Double,包括用户传入的变量数值。不支持科学计数法,仅支持十进制。

nil对象
nil是Aviator内置的常量,类似Java中的null,空值。nil跟null不同在于,Java中null只能使用在==!=的比较运算符,而nil还可以使用>、>=、<、<=等比较运算符。Aviator规定,任何对象都比nil大,除nil本身。用户传入的变量如果为null,将自动以nil替代。nil与String相加时,跟Java一样显示为null。

Aviator并不支持日期类型,如果要比较日期,你需要将日期写字符串的形式,并且要求是形如yyyy-MM-dd HH:mm:ss:SS的字符串,否则都将报错。字符串跟java.util.Date比较时将自动转换为Date对象进行比较。

要访问变量a中的某个属性b,可通过a.b访问到,即Aviator支持变量的嵌套访问。

Aviator在表达式级别支持正则表达式,通过//括起来的字符序列构成一个正则表达式,可用于匹配(作为=~操作符的右操作数)、比较大小,匹配仅能与字符串进行匹配。匹配成功后,Aviator会自动将匹配成功的分组放入$num的变量中,$0指代整个匹配的字符串,$1表示第一个分组,以此类推。正则表达式规则跟Java完全一样,内部使用java.util.regex.Pattern编译。

类型转换规则

  • Java的byte,short,int,long都转化为Long类型,Java的float,double都将转化为Double类型。Java的char String都将转化为String。Java的null都将转为nil
  • 当两个操作符都是Double或Long时,各自按照Double或Long的类型执行
  • 当两个操作符中某一个是Double时,另一个操作数也将转换成Double,按照Double类型执行
  • 任何类型与String相加,结果为String
  • 任何类型都比nil大,除了nil本身
  • nil在打印或与字符串相加时,显示为null
  • 形如yyyy-MM-dd HH:mm:ss:SS的字符串,在与java.util.Date做比较时将尝试转换成java.util.Date对象比较
  • 没有规定的类型转换操作,除了未知的变量类型之间,都将抛出异常

访问数组和集合
可以通过中括号去访问数组和java.util.List对象,可以通过map.key访问java.util.Map中key对应的value。

两种模式

  • 默认AviatorEvaluator以编译速度优先:
    AviatorEvaluator.setOptimize(AviatorEvaluator.COMPILE);
  • 修改为运行速度优先,这会做更多的编译优化:
    AviatorEvaluator.setOptimize(AviatorEvaluator.EVAL);
@Test
public void testAviator() {
    log.info(AviatorEvaluator.execute("1 + 2 + 3") + "");
    log.info(AviatorEvaluator.execute("3 > 1 && 2 != 4 || true") + "");
    log.info((String) AviatorEvaluator.execute("3 > 0 ? 'yes': no"));// 三元表达式对于两个分支的结果类型并不要求一致,可以是任何类型

    // 以下5个皆为true
    AviatorEvaluator.execute("nil == nil");
    AviatorEvaluator.execute("3 > nil");
    AviatorEvaluator.execute("true != nil");
    AviatorEvaluator.execute("' ' > nil");
    AviatorEvaluator.execute("a == nil");

    // 内置函数
    log.info("string.length('hello') = " + AviatorEvaluator.execute("string.length('hello')"));
    log.info("string.contains('hello', 'h') = " + AviatorEvaluator.execute("string.contains('hello', 'h')"));
    log.info("math.pow(-3, 2) = " + AviatorEvaluator.execute("math.pow(-3, 2)"));
    log.info("math.sqrt(9.0) = " + AviatorEvaluator.execute("math.sqrt(9.0)"));

    final List<String> list = new ArrayList<String>();
    list.add("hello");
    list.add(" world");

    final int[] array = new int[3];
    array[0] = 0;
    array[1] = 1;
    array[2] = 3;

    final Map<String, Date> map = new HashMap<>();
    map.put("date", new Date());


    Map<String, Object> env = new HashMap<>();
    env.put("name", "johnny");
    String str = "'hello ' + name";
    log.info((String) AviatorEvaluator.execute(str, env));
    // 对比, 被@Deprecated的方法
    log.info((String) AviatorEvaluator.exec(str, "johnny"));
    env.put("date", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SS").format(new Date()));
    env.put("foo", new Foo(100, new Date()));
    env.put("email", "killmesoftly2022@gmail.com");
    env.put("list", list);
    env.put("array", array);
    env.put("map", map);
    // 正则
    log.info((String) AviatorEvaluator.execute("email=~/\\w[-\\w.+]*@([A-Za-z0-9][-A-Za-z0-9]+\\.)+[A-Za-z]{2,14}/ ? $0:'unknow'", env));
    // 嵌套引用
    log.info((String) AviatorEvaluator.execute("'[foo i='+ foo.i + ' year='+(foo.date.year+1900)+ ' month='+foo.date.month +']' ", env));
    // 日期对比
    log.info(AviatorEvaluator.execute("date > '2009-12-20 00:00:00:00' ", env) + "");
    // 数组和列表
    log.info(AviatorEvaluator.execute("list[0]+list[1]+'\nsum of array='+(array[0]+array[1]+array[2]) +'\ntoday is '+map.date ", env) + "");

}

依赖的实体Bean类为:

@Data
@AllArgsConstructor
private static class Foo { 
    int i;
    Date date;
}

运算符

  • 算术运算符:包括+ - * / %五个二元运算符,和一元运算符-。其中- * / %和一元的-仅能作用于Number类型。"+"不仅能用于Number类型,还可以用于String的相加,或字符串与其他对象的相加。任何类型与String相加,结果为String。
  • 逻辑运算符:一元否定运算符!,逻辑与&&,逻辑或||。逻辑运算符的操作数只能为Boolean。&&||都执行短路规则
  • 关系运算符:也叫比较运算符,包括<,<=,>,>=,==,!=,不仅可以用于数值,也可以用于Number、String、Pattern、Boolean等,甚至是用户传入的任何两个都实现java.lang.Comparable接口的对象之间。变量之间以及其他类型与nil之间的关系比较,不同类型除了nil之外不能相互比较。任何对象都比nil大,除nil之外。
  • 匹配运算符:匹配运算符"=~"用于String和Pattern的匹配,它的左操作数必须为String,右操作数必须为Pattern。匹配成功后,Pattern的分组将存于变量$num,num为分组索引。

内置函数

函数名称说明
sysdate()返回当前日期对象java.util.Date
rand()返回一个介于0-1的随机数,double类型
print([out],obj)打印对象,如果指定out,向out打印,否则输出到控制台
now()返回System.currentTimeMillis
string.substring(s,begin[,end])截取字符串s,从begin到end,end如果忽略的话,将从begin到结尾,与java.util.String.substring一样。
math.log(d)求d的自然对数
math.log10(d)求d以10为底的对数
map(seq,fun)将函数fun作用到集合seq每个元素上,返回新元素组成的集合
filter(seq,predicate)将谓词predicate作用在集合的每个元素上,返回谓词为true的元素组成的集合
include(seq,element)判断element是否在集合seq中,返回boolean值
sort(seq)排序集合,仅对数组和List有效,返回排序后的新集合
reduce(seq,fun,init)fun接收两个参数,集合元素,累积init,用于将fun作用在集合每个元素和初始值上面,返回最终的init值
seq.eq(value)返回一个谓词,用来判断传入的参数是否跟value相等,一般用于filter函数,如filter(seq,seq.eq(3)) 过滤返回等于3的元素组成的集合
seq.exists()返回判断存在,即不为nil的谓词

自定义函数

实现com.googlecode.aviator.runtime.function.AbstractFunction接口,并注册到AviatorEvaluator即可使用

public class UserDefinedFunction extends AbstractFunction {
    /**
     * 自定义函数名称
     */
    @Override
    public String getName() {
        return "add";
    }

    @Override
    public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
        Number num1 = FunctionUtils.getNumberValue(arg1, env);
        Number num2 = FunctionUtils.getNumberValue(arg2, env);
        return new AviatorDouble(num1.doubleValue() + num2.doubleValue());
    }
}

使用自定义函数之前需要通过addFunction()方式注册,否则报错com.googlecode.aviator.exception.FunctionNotFoundException: Function not found: add

// 注册函数
AviatorEvaluator.addFunction(new UserDefinedFunction());
log.info("add(1, 1) = " + AviatorEvaluator.execute("add(1, 1)"));
AviatorEvaluator.removeFunction("add");
// log.info("add(1, 1) = " + AviatorEvaluator.execute("add(1, 1)"));

编译表达式

每次执行Aviator.execute()时,背后都经过编译和执行的操作。那可以先编译表达式,拿到编译后结果,然后传入不同的env来重复使用编译结果,提高性能。

/**
 * 编译表达式和未编译表达式性能测试
 */
@Test
public void testCompile() {
    String expression = "a * (b - c)";
    Map<String, Object> env = new HashMap<>();
    env.put("a", 3.3);
    env.put("b", 2.2);
    env.put("c", 1.1);
    int num = 10000;
    // 编译表达式
	Expression compliedExp = AviatorEvaluator.compile(expression);
    Stopwatch watch = Stopwatch.createStarted();
    for (int i = 0; i < num; i++) {
        compliedExp.execute(env);
    }
    log.info(String.format("预编译耗时为%dms", watch.elapsed(TimeUnit.MILLISECONDS)));
    watch.stop().start();
    for (int i = 0; i < num; i++) {
        AviatorEvaluator.execute(expression, env);
    }
    log.info(String.format("无编译耗时为%dms", watch.elapsed(TimeUnit.MILLISECONDS)));
}

输出:

ExpressionTest [testCompile:65] 预编译耗时为13ms
ExpressionTest [testCompile:70] 无编译耗时为2435ms

通过complie方法可以将表达式编译成Expression的中间对象,当要执行表达式时传入Map对象直接调用Expression的execute方法即可。

编译后结果可以自己缓存,也可以交给Aviator缓存,AviatorEvaluator内部维护有一个全局缓存池:

/**
 * 预编译缓存举例
 */
@Test
public void testCompileCache() {
    String expression1 = "a + b + c";
	Expression exp1 = AviatorEvaluator.compile(expression1, true);
    Expression exp2 = AviatorEvaluator.compile(expression1, true);
    Expression exp3 = AviatorEvaluator.compile(expression1, false);
    log.info("exp1 == exp2 : " + (exp1 == exp2));
    log.info("exp1 == exp3 : " + (exp1 == exp3));
}

输出:

ExpressionTest [testCompileCache:79] exp1 == exp2: true
ExpressionTest [testCompileCache:80] exp1 == exp3: false

将cached设置为true时,那下次编译同一个表达式时将直接返回上一次编译结果。使缓存失效:public static void invalidateCache(String expression)

规则引擎

没有规则引擎时,有些逻辑复杂的业务代码,只能通过不断的增添if - else来满足复杂的业务场景,当if - else过多时,会导致代码极难阅读,虽然能通过策略模式来优化if - else,但依旧存在开发周期缓慢、发布上线的问题。

所以需要规则引擎,尤其是风控系统。

规则引擎的优势:

  • 降低开发成本,提高规则变更调整优化的效率和规则上线的速度
  • 业务人员独立配置业务规则,开发人员无需理解,以往需要业务人员告诉开发人员,开发人员需要理解才能开发,并且还需要大量的测试来确定是否正确,而且开发结果还容易和提出的业务有偏差,种种都导致开发成本上升
  • 增加业务的透明度,业务人员配置之后其它人业务人员也能看到

Aviator也可用于规则引擎
将业务人员配置的规则转换成一个规则字符串,然后将该规则字符串保存进数据库中,当使用该规则时,只传递该规则所需要的参数,便可以直接计算出结果,开发人员无需再为这些规则编写任何代码。

public class AviatorRuleEngine {
	// 规则可以保存在MySQL或Redis中
    Map<Integer, String> ruleMap = new HashMap<>();
	public AviatorRuleEngine() {
		// 判断是不是资深顾客
        ruleMap.put(1, "age >= 18 && sumConsume > 2000 && vip");
		// 资深顾客要求修改
        ruleMap.put(2, "age > 10 && sumConsume >= 8000 && vip && avgYearConsume >= 1000");
    }
	
	public static void main(String[] args) {
        AviatorRuleEngine aviator = new AviatorRuleEngine();
		// 选择规则,传入规则所需要的参数
        log.info("公式1:" + aviator.getResult(1, 20, 3000, false));
        log.info("公式2:" + aviator.getResult(2, 23, 8000, true, 2000));
    }
	
	public Object getResult(int ruleId, Object... args) {
        String rule = ruleMap.get(ruleId);
		return AviatorEvaluator.exec(rule, args);
    }
}

输出:

公式1:false
公式2:true

参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

johnny233

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值