Skip to content

Latest commit

 

History

History
845 lines (655 loc) · 26.2 KB

README.adoc

File metadata and controls

845 lines (655 loc) · 26.2 KB

QLExpress

背景介绍

由阿里的电商业务规则演化而来的嵌入式Java动态脚本工具,在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。

在基本的表达式计算的基础上,增加以下特色功能:

  • 灵活的自定义能力,通过 Java API 自定义函数和操作符,可以快速实现业务规则的 DSL

  • 兼容Java语法,最新的QLExpress4可以兼容Java8语法,方便Java程序员快速熟悉

  • 友好的报错提示,无论是编译还是运行时错误,都能精确友好地提示错误位置

  • 默认安全,脚本默认不允许和应用代码进行交互,如果需要交互,也可以自行定义安全的交互方式

  • 解释执行,不占用 JVM 元空间,可以开启缓存提升解释性能

  • 代码精简,依赖最小,适合所有java的运行环境

QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,彻底拥抱Java8和函数式编程,在性能和表达能力上都进行了进一步增强。 场景举例:

  • 电商优惠券规则配置:通过 QLExpress 自定义函数和操作符快速实现优惠规则 DSL,供运营人员根据需求自行动态配置

  • 表单搭建控件关联规则配置:表单搭建平台允许用户拖拽控件搭建自定义的表单,利用 QLExpress 脚本配置不同控件间的关联关系

  • 流程引擎条件规则配置

  • 广告系统计费规则配置

......

API 快速入门

引入依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>qlexpress4</artifactId>
    <version>4.0.0-beta.5</version>
</dependency>

第一个 QLExpress 程序

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
Map<String, Object> context = new HashMap<>();
context.put("a", 1);
context.put("b", 2);
context.put("c", 3);
Object result = express4Runner.execute("a + b * c", context, QLOptions.DEFAULT_OPTIONS).getResult();
assertEquals(7, result);

添加自定义函数与操作符

最简单的方式是通过 Java Lambda 表达式快速定义函数/操作符的逻辑:

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
// custom function
express4Runner.addVarArgsFunction("join", params ->
        Arrays.stream(params).map(Object::toString).collect(Collectors.joining(",")));
Object resultFunction = express4Runner.execute("join(1,2,3)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult();
assertEquals("1,2,3", resultFunction);

// custom operator
express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right);
Object resultOperator = express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult();
assertEquals("1,2,3", resultOperator);

如果自定义函数的逻辑比较复杂,或者需要获得脚本的上下文信息,也可以通过继承 CustomFunction 的方式实现。

比如下面的 hello 自定义函数,根据租户不同,返回不同的欢迎信息:

package com.alibaba.qlexpress4.test.function;

import com.alibaba.qlexpress4.runtime.Parameters;
import com.alibaba.qlexpress4.runtime.QContext;
import com.alibaba.qlexpress4.runtime.function.CustomFunction;

public class HelloFunction implements CustomFunction {
    @Override
    public Object call(QContext qContext, Parameters parameters) throws Throwable {
        String tenant = (String) qContext.attachment().get("tenant");
        return "hello," + tenant;
    }
}
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
express4Runner.addFunction("hello", new HelloFunction());
String resultJack = (String) express4Runner.execute("hello()", Collections.emptyMap(),
        // Additional information(tenant for example) can be brought into the custom function from outside via attachments
        QLOptions.builder()
                .attachments(Collections.singletonMap("tenant", "jack"))
                .build()).getResult();
assertEquals("hello,jack", resultJack);
String resultLucy = (String) express4Runner.execute("hello()", Collections.emptyMap(),
        QLOptions.builder()
                .attachments(Collections.singletonMap("tenant", "lucy"))
                .build()).getResult();
assertEquals("hello,lucy", resultLucy);

校验语法正确性

在不执行脚本的情况下,单纯校验语法的正确性: 调用 parseToSyntaxTree 并且捕获异常,如果捕获到 QLSyntaxException,则说明存在语法错误

try {
    Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
    express4Runner.parseToSyntaxTree("a+b;\n(a+b");
    fail();
} catch (QLSyntaxException e) {
    assertEquals(2, e.getLineNo());
    assertEquals(4, e.getColNo());
    assertEquals("SYNTAX_ERROR", e.getErrorCode());
    // <EOF> represents the end of script
    assertEquals("[Error SYNTAX_ERROR: invalid primaryNoFix]\n" +
            "[Near: a+b; (a+b<EOF>]\n" +
            "                ^^^^^\n" +
            "[Line: 2, Column: 4]", e.getMessage());
}

高精度计算

QLExpress 内部会用 BigDecimal 表示所有无法用 double 精确表示数字,来尽可能地表示计算精度:

举例:0.1 在 double 中无法精确表示

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
Object result = express4Runner.execute("0.1", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult();
assertTrue(result instanceof BigDecimal);

通过这种方式能够解决一些计算精度问题:

比如 0.1+0.2 因为精度问题,在 Java 中是不等于 0.3 的。 而 QLExpress 能够自动识别出 0.1 和 0.2 无法用双精度精确表示,改成用 BigDecimal 表示,确保其结果等于0.3

assertNotEquals(0.3, 0.1 + 0.2, 0.0);
assertTrue((Boolean) express4Runner.execute("0.3==0.1+0.2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult());

除了默认的精度保证外,还提供了 precise 开关,打开后所有的计算都使用BigDecimal,防止外部传入的低精度数字导致的问题:

Map<String, Object> context = new HashMap<>();
context.put("a", 0.1);
context.put("b", 0.2);
assertFalse((Boolean) express4Runner.execute("0.3==a+b", context, QLOptions.DEFAULT_OPTIONS).getResult());
// open precise switch
assertTrue((Boolean) express4Runner.execute("0.3==a+b", context, QLOptions.builder().precise(true).build()).getResult());

调用应用中的 Java 类

需要放开安全策略,不建议用于终端用户输入

假设应用中有如下的 Java 类(com.alibaba.qlexpress4.QLImportTester):

package com.alibaba.qlexpress4;

public class QLImportTester {

    public static int add(int a, int b) {
        return a + b;
    }

}

在 QLExpress 中有如下两种调用方式。

1. 在脚本中使用 import 语句导入类并且使用

Express4Runner express4Runner = new Express4Runner(InitOptions.builder()
        // open security strategy, which allows access to all Java classes within the application.
        .securityStrategy(QLSecurityStrategy.open())
        .build()
);
// Import Java classes using the import statement.
Map<String, Object> params = new HashMap<>();
params.put("a", 1);
params.put("b", 2);
Object result = express4Runner.execute("import com.alibaba.qlexpress4.QLImportTester;" +
        "QLImportTester.add(a,b)", params, QLOptions.DEFAULT_OPTIONS).getResult();
Assert.assertEquals(3, result);

2. 在创建 Express4Runner 时默认导入该类,此时脚本中就不需要额外的 import 语句

Express4Runner express4Runner = new Express4Runner(InitOptions.builder()
        .addDefaultImport(
                Collections.singletonList(ImportManager.importCls("com.alibaba.qlexpress4.QLImportTester"))
        )
        .securityStrategy(QLSecurityStrategy.open())
        .build()
);
Object result = express4Runner.execute("QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult();
Assert.assertEquals(3, result);

除了用 ImportManager.importCls 导入单个类外,还有其他更方便的导入方式:

  • ImportManager.importPack 直接导入包路径下的所有类,比如 ImportManager.importPack("java.util") 会导入 java.util 包下的所有类,QLExpress 默认就会导入下面的包

    • ImportManager.importPack("java.lang")

    • ImportManager.importPack("java.util")

    • ImportManager.importPack("java.math")

    • ImportManager.importPack("java.util.stream")

    • ImportManager.importPack("java.util.function")

  • ImportManager.importInnerCls 导入给定类路径里的所有内部类

表达式缓存

通过 cache 选项可以开启表达式缓存,这样相同的表达式就不会重新编译,能够大大提升性能。

注意该缓存没有限制大小,只适合在表达式为有限数量的情况下使用:

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
// open cache switch
express4Runner.execute("1+2", new HashMap<>(), QLOptions.builder()
        .cache(true).build());

但是当脚本首次执行时,因为没有缓存,依旧会比较慢。

可以通过下面的方法在首次执行前就将脚本缓存起来,保证首次执行的速度:

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
express4Runner.parseToDefinitionWithCache("a+b");

扩展函数

利用 QLExpress 提供的扩展函数能力,可以给Java类中添加额外的成员方法。

扩展函数是基于 QLExpress 运行时实现的,因此仅仅在 QLExpress 脚本中有效。

下面的示例代码给 String 类添加了一个 hello() 扩展函数:

ExtensionFunction helloFunction = new ExtensionFunction() {
    @Override
    public Class<?>[] getParameterTypes() {
        return new Class[0];
    }

    @Override
    public String getName() {
        return "hello";
    }

    @Override
    public Class<?> getDeclaringClass() {
        return String.class;
    }

    @Override
    public Object invoke(Object obj, Object[] args) throws InvocationTargetException, IllegalAccessException {
        String originStr = (String) obj;
        return "Hello," + originStr;
    }
};
Express4Runner express4Runner = new Express4Runner(InitOptions.builder()
        .addExtensionFunctions(Collections.singletonList(helloFunction))
        .build());
Object result = express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult();
assertEquals("Hello,jack", result);

Java类的对象,字段和方法别名

QLExpress 支持通过 QLAlias 注解给对象,字段或者方法定义一个或多个别名,方便非技术人员使用表达式定义规则。

下面的例子中,根据用户是否 vip 计算订单最终金额。

用户类定义:

package com.alibaba.qlexpress4.test.qlalias;

import com.alibaba.qlexpress4.annotation.QLAlias;

@QLAlias("用户")
public class User {

    @QLAlias("是vip")
    private boolean vip;

    @QLAlias("用户名")
    private String name;

    public boolean isVip() {
        return vip;
    }

    public void setVip(boolean vip) {
        this.vip = vip;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

订单类定义:

package com.alibaba.qlexpress4.test.qlalias;

import com.alibaba.qlexpress4.annotation.QLAlias;

@QLAlias("订单")
public class Order {

    @QLAlias("订单号")
    private String orderNum;

    @QLAlias("金额")
    private int amount;

    public String getOrderNum() {
        return orderNum;
    }

    public void setOrderNum(String orderNum) {
        this.orderNum = orderNum;
    }

    public int getAmount() {
        return amount;
    }

    public void setAmount(int amount) {
        this.amount = amount;
    }
}

通过 QLExpress 脚本规则计算最终订单金额:

Order order = new Order();
order.setOrderNum("OR123455");
order.setAmount(100);

User user = new User();
user.setName("jack");
user.setVip(true);

// Calculate the Final Order Amount
Express4Runner express4Runner = new Express4Runner(InitOptions.builder()
        .securityStrategy(QLSecurityStrategy.open()).build());
Number result = (Number) express4Runner.executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额",
        QLOptions.DEFAULT_OPTIONS, order, user).getResult();
assertEquals(80, result.intValue());

关键字,操作符和函数别名

为了进一步方面非技术人员编写规则,QLExpress 提供 addAlias 给原始关键字,操作符和函数增加别名。让整个脚本的表述更加贴近自然语言。

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
// add custom function zero
express4Runner.addFunction("zero", (String ignore) -> 0);

// keyword alias
assertTrue(express4Runner.addAlias("如果", "if"));
assertTrue(express4Runner.addAlias("则", "then"));
assertTrue(express4Runner.addAlias("否则", "else"));
assertTrue(express4Runner.addAlias("返回", "return"));
// operator alias
assertTrue(express4Runner.addAlias("大于", ">"));
// function alias
assertTrue(express4Runner.addAlias("零", "zero"));

Map<String, Object> context = new HashMap<>();
context.put("语文", 90);
context.put("数学", 90);
context.put("英语", 90);

Object result = express4Runner.execute(
        "如果 (语文 + 数学 + 英语 大于 270) 则 {返回 1;} 否则 {返回 零();}",
        context, QLOptions.DEFAULT_OPTIONS
).getResult();
assertEquals(0, result);

支持设置别名的关键字有:

  • if

  • then

  • else

  • for

  • while

  • break

  • continue

  • return

  • function

  • macro

  • new

  • null

  • true

  • false

注意:部分大家熟悉的用法其实是操作符,而不是关键字,比如 in 操作符。而所有的操作符和函数默认就是支持别名的

表达式计算跟踪

跟踪表达式在中间节点计算的值,应用可基于该功能实现表达式的归因分析。实现更加强大的提示和分析功能。

目前该功能之会追踪脚本中最顶层的表达式,即单独作为语句存在的表达式。可以满足所有将 QLExpress 当成表达式使用的场景。

节点计算结果会被放置到 ExpressionTrace 对象的 value 字段中。如果中间发生短路导致部分表达式未被计算,则 ExpressionTrace 对象的 evaluated 字段会被设置为 false。代码示例如下:

Express4Runner express4Runner = new Express4Runner(InitOptions.builder().traceExpression(true).build());
express4Runner.addFunction("myTest", (Predicate<Integer>) i -> i > 10);

Map<String, Object> context = new HashMap<>();
context.put("a", true);
QLResult result = express4Runner.execute("a && (!myTest(11) || false)", context, QLOptions.DEFAULT_OPTIONS);
Assert.assertFalse((Boolean) result.getResult());

List<ExpressionTrace> expressionTraces = result.getExpressionTraces();
Assert.assertEquals(1, expressionTraces.size());
ExpressionTrace expressionTrace = expressionTraces.get(0);
Assert.assertEquals("OPERATOR && false\n" +
        "  | VARIABLE a true\n" +
        "  | OPERATOR || false\n" +
        "      | OPERATOR ! false\n" +
        "          | FUNCTION myTest true\n" +
        "              | VALUE 11 11\n" +
        "      | VALUE false false\n", expressionTrace.toPrettyString(0));

// short circuit
context.put("a", false);
QLResult resultShortCircuit = express4Runner.execute("a && (!myTest(11) || false)", context, QLOptions.DEFAULT_OPTIONS);
Assert.assertFalse((Boolean) resultShortCircuit.getResult());
ExpressionTrace expressionTraceShortCircuit = resultShortCircuit.getExpressionTraces().get(0);
Assert.assertEquals("OPERATOR && \n" +
        "  | VARIABLE a false\n" +
        "  | OPERATOR || \n" +
        "      | OPERATOR ! \n" +
        "          | FUNCTION myTest \n" +
        "              | VALUE 11 \n" +
        "      | VALUE false \n", expressionTraceShortCircuit.toPrettyString(0));
Assert.assertTrue(expressionTraceShortCircuit.getChildren().get(0).isEvaluated());
Assert.assertFalse(expressionTraceShortCircuit.getChildren().get(1).isEvaluated());

// in
QLResult resultIn= express4Runner.execute("'ab' in ['cc', 'dd', 'ff']", context, QLOptions.DEFAULT_OPTIONS);
Assert.assertFalse((Boolean) resultIn.getResult());
ExpressionTrace expressionTraceIn = resultIn.getExpressionTraces().get(0);
Assert.assertEquals("OPERATOR in false\n" +
        "  | VALUE 'ab' ab\n" +
        "  | LIST [ [cc, dd, ff]\n" +
        "      | VALUE 'cc' cc\n" +
        "      | VALUE 'dd' dd\n" +
        "      | VALUE 'ff' ff\n", expressionTraceIn.toPrettyString(0));

也可以在不执行脚本的情况下获得所有表达式追踪点:

Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
TracePointTree tracePointTree = express4Runner.getExpressionTracePoints("1+3+5*ab+9").get(0);
Assert.assertEquals("OPERATOR +\n" +
        "  | OPERATOR +\n" +
        "      | OPERATOR +\n" +
        "          | VALUE 1\n" +
        "          | VALUE 3\n" +
        "      | OPERATOR *\n" +
        "          | VALUE 5\n" +
        "          | VARIABLE ab\n" +
        "  | VALUE 9\n", tracePointTree.toPrettyString(0));

支持的表达式追踪点类型以及对应子节点的含义如下:

节点类型

节点含义

子节点含义

OPERATOR

操作符

两侧操作数

FUNCTION

函数

函数参数

METHOD

方法

方法参数

FIELD

字段

取字段的目标对象

LIST

列表

列表元素

VARIABLE

变量

VALUE

字面值

PRIMARY

暂时未继续下钻的其他复合值(比如字典,if等等)

语法入门

QLExpress4 兼容 Java8 语法的同时,也提供了很多更加灵活宽松的语法模式,帮助用户更快捷地编写表达式。

基于表达式优先的语法设计,复杂的条件判断语句也可以直接当作表达式使用。

在本章节中出现的代码片段都是 qlexpress 脚本, assert 是测试框架往引擎中注入的断言方法,会确保其参数为 trueassertErrCode 会确保其 lambda 参数表达式的执行一定会抛出含第二个参数 error code 的 QLException。

变量声明

同时支持静态类型和动态类型:

  • 变量声明时不写类型,则变量是动态类型,也同时是一个赋值表达式

  • 变量声明如果写类型,则是静态类型,此时是一个变量声明语句

// Dynamic Typeing
a = 1;
a = "1";
// Static Typing
int b = 2;
// throw QLException with error code INCOMPATIBLE_ASSIGNMENT_TYPE when assign with incompatible type String
assertErrorCode(() -> b = "1", "INCOMPATIBLE_ASSIGNMENT_TYPE")

方便语法元素

列表(List),映射(Map)等常用语法元素在 QLExpress 中都有非常方便的构造语法糖:

// list
l = [1,2,3]
assert(l[0]==1)
// Underlying data type of list is ArrayList in Java
assert(l instanceof ArrayList)
// map
m = {
  "aa": 10,
  "bb": {
    "cc": "cc1",
    "dd": "dd1"
  }
}
assert(m['aa']==10)
// Underlying data type of map is ArrayList in Java
assert(m instanceof LinkedHashMap)

数字

对于未声明类型的数字, QLExpress会根据其所属范围自动从 int, long, BigInteger, double, BigDecimal 等数据类型中选择一个最合适的:

assert(2147483647 instanceof Integer);
assert(9223372036854775807 instanceof Long);
assert(18446744073709552000 instanceof BigInteger);
// 0.25 can be precisely presented with double
assert(0.25 instanceof Double);
assert(2.7976931348623157E308 instanceof BigDecimal);

因此在自定义函数或者操作符时,建议使用 Number 类型进行接收,因为数字类型是无法事先确定的。

动态字符串

QLExpress 支持 ${expression} 的格式在字符串中插入表达式,更方便地进行动态字符串组装:

a = 123
assert("hello,${a-1}" == "hello,122")

b = "test"
assert("m xx ${
  if (b like 't%') {
      'YYY'
  }
}" == "m xx YYY")

分号

表达式语句可以省略结尾的分号,整个脚本的返回值就是最后一个表达式的计算结果。

以下脚本的返回值为 2:

a = 1
b = 2
// last express
1+1

等价于以下写法:

a = 1
b = 2
// return statment
return 1+1;

表达式

QLExpress 采用表达式优先的设计,其中 除了 import, return 和循环等结构外,几乎都是表达式。

if 语句也是一个表达式:

assert(if (11 == 11) {
  10
} else {
  20 + 2
} + 1 == 11)

try catch 结构也是一个表达式:

assert(1 + try {
    100 + 1/0
} catch(e) {
    // Throw a zero-division exception
    11
} == 12)

控制结构

if 分支

除了完全兼容 Java 中的 if 写法,还支持类似规则引擎的 if …​ then …​ else …​ 的写法,其中 then 可以当成一个可以省略的关键字:

a = 11;
// if ... else ...
assert(if (a >= 0 && a < 5) {
  true
} else if (a >= 5 && a < 10) {
  false
} else if (a >= 10 && a < 15) {
  true
} == true)

// if ... then ... else ...
r = if (a == 11) then true else false
assert(r == true)

while 循环

i = 0;
while (i < 5) {
  if (++i == 2) {
    break;
  }
}
assert(i==2)

for 循环

l = [];
for (int i = 3; i < 6; i++) {
  l.add(i);
}
assert(l==[3,4,5])

for-each 循环

sum = 0;
for (i: [0,1,2,3,4]) {
  if (i == 2) {
    continue;
  }
  sum += i;
}
assert(sum==8)

try-catch

assert(try {
    100 + 1/0
} catch(e) {
    // Throw a zero-division exception
    11
} == 11)

函数定义

function sub(a, b) {
    return a-b;
}
assert(sub(3,1)==2)

Lambda 表达式

QLExpress4 中,Lambda 表达式作为一等公民,可以作为变量进行传递或者返回。

add = (a, b) -> {
  return a + b;
}
assert(add(1,2)==3)

列表过滤和映射

支持通过 filter, map 方法直接对列表类型进行函数式过滤和映射。

底层通过在列表类型添加 扩展函数 实现,注意和 Stream API 中同名方法区分。

相比 Stream Api,它可以直接对列表进行操作,返回值也直接就是列表,更加方便。

l = ["a-111", "a-222", "b-333", "c-888"]
assert(l.filter(i -> i.startsWith("a-"))
        .map(i -> i.split("-")[1]) == ["111", "222"])

兼容 Java8 语法

QLExpress 可以兼容 Java8 的常见语法。

比如 for each循环, Stream API, 函数式接口等等。

Stream API

可以直接使用 Java 集合中的 stream api 对集合进行操作。

因为此时的 stream api 都是来自 Java 中的方法,参考 调用应用中的Java类 打开安全选项,以下脚本才能正常执行。

l = ["a-111", "a-222", "b-333", "c-888"]

l2 = l.stream()
      .filter(i -> i.startsWith("a-"))
      .map(i -> i.split("-")[1])
      .collect(Collectors.toList());
assert(l2 == ["111", "222"]);
函数式接口

Java8 中引入了 Function, Consumer, Predicate 等函数式接口,QLExpress 中的 Lambda表达式 可以赋值给这些接口,或者作为接收这些接口的方法参数:

Runnable r = () -> a = 8;
r.run();
assert(a == 8);

Supplier s = () -> "test";
assert(s.get() == 'test');

Consumer c = (a) -> b = a + "-te";
c.accept("ccc");
assert(b == 'ccc-te');

Function f = a -> a + 3;
assert(f.apply(1) == 4);

Function f1 = (a, b) -> a + b;
assert(f1.apply("test-") == "test-null");

附录一 QLExpress4性能提升

总结:常见场景下,无编译缓存时,QLExpress4能比3有接近10倍性能提升;有编译缓存,也有一倍性能提升。

附录二 开发者联系方式

qlexpress support group qr