Loading repository data…
Loading repository data…
alibaba / repository
QLExpress is a powerful, lightweight, dynamic language for the Java platform aimed at improving developers’ productivity in different business scenes.
A transparent discovery signal based on current public GitHub metadata.
This score does not audit code, security, maintainers, documentation quality, or suitability. Verify the repository and its current documentation before adoption.
:toc:
= QLExpress
image::images/logo.png[]
【中文版】| link:README-EN.adoc[[English]]
image::https://api.star-history.com/svg?repos=alibaba/QLExpress&type=Date[Star History Chart]
== 背景介绍
由阿里的电商业务规则演化而来的嵌入式Java动态脚本工具,在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。
在基本的表达式计算的基础上,还有以下特色:
QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,新增了大量特色功能,彻底拥抱函数式编程,在性能和表达能力上都进行了进一步增强。
如果项目还是使用旧版本的 QLExpress,可以跳转 link:https://github.com/alibaba/QLExpress/tree/branch_version_3.x.x[branch_version_3.x.x] 维护分支查看旧版文档。如需升级可以参考 link:#附录一-升级指南[附录一-升级指南]
场景举例:
......
== 新版特色
新版本并不是对旧版本的简单功能重构,而是我们基于对用户需求的洞察,对下一代规则表达式引擎的探索。拥有许多非常实用,但是在其他引擎中缺失的重要功能。
=== 表达式计算追踪
在业务人员完成规则脚本的配置后,很难对其线上执行情况进行感知。比如电商的促销规则,要求用户满足规则 isVip && 未登录10天以上。到底有多少线上用户是被 vip 条件拦截,又有多少用户是因为登录条件被拦截?这还是只是仅仅两个条件的简单规则,实际线上情况则更加复杂。
线上规则执行情况的追踪,不仅仅可以帮助业务人员了解线上的实际情况,排查和修复问题。其沉淀的数据也非常有价值,可以用于后续的规则优化和业务决策。以下是某个规则平台,基于 QLExpress4 的表达式追踪能力,对规则进行归因分析与附注的决策的产品简化图:
image::images/order_rules_cn.png[]
归因分析的原理在于利用 QLExpress4 的表达式追踪能力,获得表达式在计算过程中每个中间结果的值, 据此判断表达式最终运行结果产生的原因。
具体使用方法参考:link:#表达式计算追踪-1[表达式计算追踪]
=== 原生支持 JSON 语法
QLExpress4 原生支持 JSON 语法,可以快捷定义复杂的数据结构。
JSON 数组代表列表(List),而 JSON 对象代表映射(Map),也可以直接定义复杂对象。
产品上可以基于该特性实现 JSON 映射规则。让用户可以便捷地定义从一个模型向另一个模型的映射关系。以下是某个规则平台,基于该能力实现的模型映射产品简化图:
image::images/json_map.png[]
具体使用方法参考:link:#方便语法元素[方便语法元素]
=== 便捷字符串处理
QLExpress4 对字符串处理能力进行针对性的增强,在字符串中可以直接通过 $\{expression} 嵌入表达式计算结果。
具体使用方法参考:link:#动态字符串[动态字符串]
=== 附件透传
正常情况下,脚本执行需要的全部信息都在 context 中。context 中的 key 可以在脚本中作为变量引用,最终传递给自定义函数或者操作符。
但是出于安全,或者方便使用等因素考虑。有些信息并不希望用户通过变量引用到,比如租户名,密码等等。
此时可以通过附件(attachments)将这部分信息传递给自定义函数或者操作符使用。
具体使用方法参考:link:#添加自定义函数与操作符[添加自定义函数与操作符] 其中 hello 自定义函数根据附件中租户不同,返回不同的欢迎信息的示例。
=== 函数式编程
函数被提升为 QLExpress4 中的第一等公民,可以作为变量使用,也可以作为函数的返回值。并且可以很容易地和 Java 中常见的函数式 API(比如 Stream) 结合使用。
以下是一个简单的 QLExpress 示例脚本:
更多使用方法参考:
=== 分号简化
QLExpress4 支持省略分号,让表达式更加简洁。具体参考 link:#分号[分号]
== API 快速入门
=== 引入依赖
version 建议使用最新的稳定版本
4.1.2
环境要求:
=== 第一个 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);
更多的表达式执行方式见文档 link:docs/execute.adoc[表达式执行]
=== 添加自定义函数与操作符
最简单的方式是通过 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;
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);
QLExpress4还支持通过QLExpress脚本添加自定义函数。需要注意的是,在函数外定义的变量(如示例中的defineTime)在函数定义时就已初始化完成,后续调用函数时不会重新计算该变量的值。
Express4Runner express4Runner =
new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build());
BatchAddFunctionResult addResult = express4Runner.addFunctionsDefinedInScript(
"function myAdd(a,b) {\n" + " return a+b;" + "}\n" + "\n" + "function getCurrentTime() {\n"
+ " return System.currentTimeMillis();\n" + "}" + "\n" + "defineTime=System.currentTimeMillis();\n"
+ "function defineTime() {\n" + " return defineTime;" + "}\n",
ExpressContext.EMPTY_CONTEXT,
QLOptions.DEFAULT_OPTIONS);
assertEquals(3, addResult.getSucc().size());
QLResult result = express4Runner.execute("myAdd(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
assertEquals(3, result.getResult());
QLResult resultCurTime1 =
express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
Thread.sleep(1000);
QLResult resultCurTime2 =
express4Runner.execute("getCurrentTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
assertNotSame(resultCurTime1.getResult(), resultCurTime2.getResult());
/*
* The defineTime variable is defined outside the function and is initialized when the function is defined;
* it is not recalculated afterward, so the value returned is always the time at which the function was defined.
*/
QLResult resultDefineTime1 =
express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
Thread.sleep(1000);
QLResult resultDefineTime2 =
express4Runner.execute("defineTime()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS);
assertSame(resultDefineTime1.getResult(), resultDefineTime2.getResult());
建议尽可能使用Java方式定义自定义函数,这样可以获得更好的性能和稳定性。
==== 延迟参数求值函数(LazyArgCustomFunction)
默认情况下,QLExpress 在调用函数前会先对所有参数完成求值。如果某个参数的计算会产生副作用(如除零异常),即使函数逻辑上不需要该参数,也会在调用前触发错误。
实现 LazyArgCustomFunction 接口的函数可以控制参数的求值时机。编译器默认会将每个参数包装为 QLambda,也可以通过 isLazyArg(int argIndex) 指定部分参数,函数内部通过调用 QLambda.get() 手动触发求值,从而实现短路求值语义。
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
express4Runner.addFunction("IF", new LazyArgCustomFunction() {
private static final int PARAM_LENGTH = 3;
@Override
public boolean isLazyArg(int argIndex) {
return 1 == argIndex || 2 == argIndex;
}
@Override
public Object call(QContext qContext, Parameters parameters) {
if (parameters == null || parameters.size() != PARAM_LENGTH) {
throw new IllegalArgumentException("Invalid number of arguments");
}
Object v1 = call(parameters.getValue(0));
if (!(v1 instanceof Boolean)) {
throw new IllegalArgumentException("Argument 1 must be a boolean");
}
if ((Boolean)v1) {
return call(parameters.getValue(1));
}
return call(parameters.getValue(2));
}
private Object call(Object obj) {
if (obj instanceof QLambda) {
return ((QLambda)obj).get();
}
return obj;
}
});
上例中,当 b == 0 条件成立时,第三个参数 a / b 不会被求值,因此不会触发除零异常。
更多自定义语法元素的方式见文档 link:docs/custom-item.adoc[自定义语法元素]
=== 校验语法正确性
在不执行脚本的情况下,单纯校验语法的正确性,其中包含了操作符的限制校验,调用 check 并且捕获异常,如果捕获到 QLSyntaxException,则说明存在语法错误
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
try {
express4Runner.check("a+b;\n(a+b");
fail();
}
catch (QLSyntaxException e) {
assertEquals(2, e.getLineNo());
assertEquals(5, e.getColNo());
assertEquals("SYNTAX_ERROR", e.getErrorCode());
// <EOF> represents the end of script
assertEquals(
"[Error SYNTAX_ERROR: mismatched input '<EOF>' expecting ')']\n" + "[Near: a+b; (a+b<EOF>]\n"
+ " ^^^^^\n" + "[Line: 2, Column: 5]",
e.getMessage());
}
你可以使用 CheckOptions 配置更精细的语法校验规则,主要支持以下两个选项:
operatorCheckStrategy: 操作符校验策略,用于限制脚本中可以使用的操作符disableFunctionCalls: 是否禁用函数调用,默认为 false示例1:使用操作符校验策略(白名单)
// Create a whitelist of allowed operators
Set<String> allowedOps = new HashSet<>(Arrays.asList("+", "*"));
// Configure check options with operator whitelist
CheckOptions checkOptions =
CheckOptions.builder().operatorCheckStrategy(OperatorCheckStrategy.whitelist(allowedOps)).build();
// Create runner and check script with custom options
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
runner.check("a + b * c", checkOptions); // This will pass as + and * are allowed
示例2:禁用函数调用
// Create a runner
Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
// Create options with function calls disabled
CheckOptions options = CheckOptions.builder().disableFunctionCalls(true).build();
// Script with function call
String scriptWithFunctionCall = "Math.max(1, 2)";
// Use custom options to check script
try {
runner.check(scriptWithFunctionCall, options);
}
catch (QLSyntaxException e) {
// Will throw exception as function calls are disabled
}
=== 解析脚本所需外部变量
脚本中使用的变量有的是脚本内生,有的是需要从外部通过 context 传入的。
QLExpress4 提供了一个方法,可以解析出脚本中所有需要从外部传入的变量:
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS);
Set<String> outVar