由阿里的电商业务规则演化而来的嵌入式Java动态脚本工具,在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。
在基本的表达式计算的基础上,增加以下特色功能:
-
灵活的自定义能力,通过 Java API 自定义函数和操作符,可以快速实现业务规则的 DSL
-
兼容Java语法,最新的QLExpress4可以兼容Java8语法,方便Java程序员快速熟悉
-
友好的报错提示,无论是编译还是运行时错误,都能精确友好地提示错误位置
-
默认安全,脚本默认不允许和应用代码进行交互,如果需要交互,也可以自行定义安全的交互方式
-
解释执行,不占用 JVM 元空间,可以开启缓存提升解释性能
-
代码精简,依赖最小,适合所有java的运行环境
QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,彻底拥抱Java8和函数式编程,在性能和表达能力上都进行了进一步增强。 场景举例:
-
电商优惠券规则配置:通过 QLExpress 自定义函数和操作符快速实现优惠规则 DSL,供运营人员根据需求自行动态配置
-
表单搭建控件关联规则配置:表单搭建平台允许用户拖拽控件搭建自定义的表单,利用 QLExpress 脚本配置不同控件间的关联关系
-
流程引擎条件规则配置
-
广告系统计费规则配置
......
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>qlexpress4</artifactId>
<version>4.0.0-beta.5</version>
</dependency>
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 类(com.alibaba.qlexpress4.QLImportTester
):
package com.alibaba.qlexpress4;
public class QLImportTester {
public static int add(int a, int b) {
return a + b;
}
}
在 QLExpress 中有如下两种调用方式。
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);
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);
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
是测试框架往引擎中注入的断言方法,会确保其参数为 true
。
assertErrCode
会确保其 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)
除了完全兼容 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)
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"])
QLExpress 可以兼容 Java8 的常见语法。
比如 for each循环, 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能比3有接近10倍性能提升;有编译缓存,也有一倍性能提升。
-
Email:
-
WeChat:
-
xuanheng: dqy932087612
-
binggou: pymbupt
-
linxiang: tkk33362
-
-
DingTalk Support Group