GitHub - alibaba/QLExpress: QLExpress is a powerful, lightweight, dynamic language for the Java platform aimed at improving developers’ productivity in different business scenes.
引入依赖
<dependency> <groupId>com.alibaba</groupId> <artifactId>qlexpress4</artifactId> <version>4.1.0</version> </dependency>
环境要求:
-
JDK 8 或更高版本
第一个 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);
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方式定义自定义函数,这样可以获得更好的性能和稳定性。
校验语法正确性
在不执行脚本的情况下,单纯校验语法的正确性,其中包含了操作符的限制校验,调用 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(4, 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: 4]", 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> outVarNames = express4Runner.getOutVarNames("int a = 1, b = 10;\n" + "c = 11\n" + "e = a + b + c + d\n" + "f+e"); Set<String> expectSet = new HashSet<>(); expectSet.add("d"); expectSet.add("f"); assertEquals(expectSet, outVarNames);
更多脚本依赖解析工具:
-
getOutFunctions: 解析所有需要从外部定义的函数 -
getOutVarAttrs:解析所有需要从外部传入变量及其涉及的属性,getOutVarNames的增强版本
高精度计算
QLExpress 内部会用 BigDecimal 表示所有无法用 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());
安全策略
QLExpress4 默认采用隔离安全策略,不允许脚本访问 Java 对象的字段和方法,这确保了脚本执行的安全性。如果需要访问 Java 对象,可以通过不同的安全策略进行配置。
假设应用中有如下的 Java 类:
package com.alibaba.qlexpress4.inport; /** * Author: DQinYuan */ public class MyDesk { private String book1; private String book2; public String getBook1() { return book1; } public void setBook1(String book1) { this.book1 = book1; } public String getBook2() { return book2; } public void setBook2(String book2) { this.book2 = book2; } }
脚本执行的上下文设置如下:
MyDesk desk = new MyDesk(); desk.setBook1("Thinking in Java"); desk.setBook2("Effective Java"); Map<String, Object> context = Collections.singletonMap("desk", desk);
QLExpress4 提供了四种安全策略:
1. 隔离策略(默认)
默认情况下,QLExpress4 采用隔离策略,不允许访问任何字段和方法:
// default isolation strategy, no field or method can be found Express4Runner express4RunnerIsolation = new Express4Runner(InitOptions.DEFAULT_OPTIONS); assertErrorCode(express4RunnerIsolation, context, "desk.book1", "FIELD_NOT_FOUND"); assertErrorCode(express4RunnerIsolation, context, "desk.getBook2()", "METHOD_NOT_FOUND");
2. 黑名单策略
通过黑名单策略,可以禁止访问特定的字段或方法,其他字段和方法可以正常访问:
// black list security strategy Set<Member> memberList = new HashSet<>(); memberList.add(MyDesk.class.getMethod("getBook2")); Express4Runner express4RunnerBlackList = new Express4Runner( InitOptions.builder().securityStrategy(QLSecurityStrategy.blackList(memberList)).build()); assertErrorCode(express4RunnerBlackList, context, "desk.book2", "FIELD_NOT_FOUND"); Object resultBlack = express4RunnerBlackList.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult(); Assert.assertEquals("Thinking in Java", resultBlack);
3. 白名单策略
通过白名单策略,只允许访问指定的字段或方法,其他字段和方法都会被禁止:
// white list security strategy Express4Runner express4RunnerWhiteList = new Express4Runner( InitOptions.builder().securityStrategy(QLSecurityStrategy.whiteList(memberList)).build()); Object resultWhite = express4RunnerWhiteList.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult(); Assert.assertEquals("Effective Java", resultWhite); assertErrorCode(express4RunnerWhiteList, context, "desk.getBook1()", "METHOD_NOT_FOUND");
4. 开放策略
开放策略允许访问所有字段和方法,类似于 QLExpress3 的行为,但需要注意安全风险:
// open security strategy Express4Runner express4RunnerOpen = new Express4Runner(InitOptions.builder().securityStrategy(QLSecurityStrategy.open()).build()); Assert.assertEquals("Thinking in Java", express4RunnerOpen.execute("desk.book1", context, QLOptions.DEFAULT_OPTIONS).getResult()); Assert.assertEquals("Effective Java", express4RunnerOpen.execute("desk.getBook2()", context, QLOptions.DEFAULT_OPTIONS).getResult());
注意:开放策略虽然提供了最大的灵活性,但也带来了安全风险。建议只在受信任的环境中使用,不建议用于处理终端用户输入的脚本。
策略建议
建议直接采用默认策略,在脚本中不要直接调用 Java 对象的字段和方法。而是通过自定义函数和操作符的方式(参考 添加自定义函数与操作符),对嵌入式脚本提供系统功能。这样能同时保证脚本的安全性和灵活性,用户体验还更好。
如果确实需要调用 Java 对象的字段和方法,至少应该使用白名单策略,只提供脚本有限的访问权限。
至于黑名单和开放策略,不建议在外部输入脚本的场景使用,除非确保每个脚本都会经过审核。
调用应用中的 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导入给定类路径里的所有内部类 -
ImportManager.importClsAlias为类指定别名,特别适用于代码混淆场景。该方法直接使用传入的 Class 对象,而不是通过类名加载,可以避免类名编译后被混淆
InitOptions initOptions = InitOptions.builder() .securityStrategy(QLSecurityStrategy.open()) .addDefaultImport(Arrays.asList(ImportManager.importClsAlias(Aa.class, "User"), ImportManager.importClsAlias(Bb.class, "Order"))) .build(); Express4Runner express4Runner = new Express4Runner(initOptions); QLResult result = express4Runner .execute("user = new User(); user.name = 'jack'; " + "order = new Order(); order.amount = 100; " + "user.name + ':' + order.amount", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); assertEquals("jack:100", result.getResult());
自定义 ClassLoader
QLExpress4 支持通过自定义 ClassSupplier 来指定类加载器,这在插件化架构、模块化应用等场景中非常有用。通过自定义类加载器,可以让 QLExpress 脚本访问特定 ClassLoader 中的类。
下面的示例展示了如何与 PF4J 插件框架集成,让 QLExpress 脚本能够访问插件中的类:
// Specify plugin directory (test-plugins directory under test resources) Path pluginsDir = new File("src/test/resources/test-plugins").toPath(); PluginManager pluginManager = new DefaultPluginManager(pluginsDir); pluginManager.loadPlugins(); pluginManager.startPlugins(); // Get the PluginClassLoader of the first plugin PluginWrapper plugin = pluginManager.getPlugins().get(0); ClassLoader pluginClassLoader = plugin.getPluginClassLoader(); // Custom class supplier using plugin ClassLoader ClassSupplier pluginClassSupplier = clsName -> { try { return Class.forName(clsName, true, pluginClassLoader); } catch (ClassNotFoundException | NoClassDefFoundError e) { return null; } }; InitOptions options = InitOptions.builder() .securityStrategy(QLSecurityStrategy.open()) .classSupplier(pluginClassSupplier) .build(); Express4Runner runner = new Express4Runner(options); String script = "import com.alibaba.qlexpress4.pf4j.TestPluginInterface; TestPluginInterface.TEST_CONSTANT"; Object result = runner.execute(script, Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); Assert.assertEquals("Hello from PF4J Plugin!", result.toString());
自定义 ClassSupplier 的典型应用场景:
-
插件化架构:让脚本能够访问插件中定义的类和接口
-
模块化应用:在 OSGi 等模块化框架中,让脚本访问特定模块的类
-
动态类加载:从远程仓库或动态生成的字节码中加载类
-
类隔离:使用不同的 ClassLoader 来实现类的隔离
表达式缓存
通过 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");
注意该缓存的大小是无限的,业务上注意控制大小,可以调用 clearCompileCache 方法定期清空编译缓存。
清除 DFA 缓存
QLExpress 使用 ANTLR4 作为解析引擎,ANTLR4 在运行时会构建 DFA (确定有限状态自动机) 缓存来加速后续的语法解析。这个缓存会占用一定的内存空间。
在某些内存敏感的场景下,可以通过调用 clearDFACache 方法来清除 DFA 缓存,释放内存。
重要警告: 清除 DFA 缓存会导致编译性能大幅下降,正常情况下不建议使用此方法。
最佳实践
在解析并缓存表达式后立即调用此方法,并确保后续所有执行都打开缓存选项以避免重新编译。示例代码如下:
/* * When the expression changes, parse it and add it to the expression cache; * after parsing is complete, call clearDFACache. */ Express4Runner runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); runner.parseToDefinitionWithCache(exampleExpress); runner.clearDFACache(); /* * All subsequent runs of this script must enable the cache option to ensure that re-compilation does not occur. */ for (int i = 0; i < 3; i++) { runner.execute(exampleExpress, ExpressContext.EMPTY_CONTEXT, QLOptions.builder().cache(true).build()); }
通过这种方式,可以在保证性能的同时,最大限度地降低内存占用。
设置超时时间
可以给脚本设置一个超时时间,防止其中存在死循环或者其他原因导致应用资源被过量消耗。
下面的示例代码给脚本给脚本设置了一个 10ms 的超时时间:
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); try { express4Runner.execute("while (true) {\n" + " 1+1\n" + "}", Collections.emptyMap(), QLOptions.builder().timeoutMillis(10L).build()); fail("should timeout"); } catch (QLTimeoutException e) { assertEquals(QLErrorCodes.SCRIPT_TIME_OUT.name(), e.getErrorCode()); }
注意,出于系统性能的考虑,QLExpress 对于超时时间的检测是不准确的。特别是在回调Java代码中(比如自定义函数或者操作符)出现的超时,不会立刻被检测到。只有在执行完,回到 QLExpress 运行时后才会被检测到并中断执行。
扩展函数
利用 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) { String originStr = (String)obj; return "Hello," + originStr; } }; Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); express4Runner.addExtendFunction(helloFunction); Object result = express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS).getResult(); assertEquals("Hello,jack", result); // simpler way to define extension function express4Runner.addExtendFunction("add", Number.class, params -> ((Number)params[0]).intValue() + ((Number)params[1]).intValue()); QLResult resultAdd = express4Runner.execute("1.add(2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); assertEquals(3, resultAdd.getResult());
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中一个强大的代码复用机制,它允许用户定义一段可重用的脚本片段,并在需要时进行调用。与简单的文本替换不同,QLExpress的宏是基于指令回放的机制实现的,具有更好的性能和语义准确性。
宏特别适用于以下场景:
-
代码复用:将常用的脚本片段封装成宏,避免重复编写相同的逻辑
-
业务规则模板:定义标准的业务规则模板,如价格计算、权限检查等
-
流程控制:封装复杂的控制流程,如条件判断、循环逻辑等
-
DSL构建:作为构建领域特定语言的基础组件
宏可以通过两种方式定义:
1. 在脚本中使用 macro 关键字定义
macro add { c = a + b; } a = 1; b = 2; add; assert(c == 3);
2. 通过Java API添加
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); express4Runner.addMacro("rename", "name='haha-'+name"); Map<String, Object> context = Collections.singletonMap("name", "wuli"); Object result = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); assertEquals("haha-wuli", result); // replace macro express4Runner.addOrReplaceMacro("rename", "name='huhu-'+name"); Object result1 = express4Runner.execute("rename", context, QLOptions.DEFAULT_OPTIONS).getResult(); assertEquals("huhu-wuli", result1);
宏与函数的区别:
特性 |
宏 |
函数 |
参数传递 |
无参数,依赖上下文变量 |
支持参数传递 |
性能 |
指令直接插入,无调用开销 |
有函数调用开销 |
作用域 |
共享调用者作用域 |
独立的作用域 |
适用场景 |
代码片段复用 |
逻辑封装和参数化 |
宏特别适合那些不需要参数传递、主要依赖上下文变量的代码片段复用场景,而函数更适合需要参数化和独立作用域的场景。
QLExpress4 相比 3 版本,宏特性的变化:
-
4 的宏实现更加接近通常编程语言中宏的定义,相当于将预定义的代码片段插入到宏所在的位置,与调用点位于同一作用域,宏中的
return,contine和break等可以影响调用方的控制流。但是 3 中的实现其实更加接近无参函数调用。 -
4 的宏无法作为变量使用,只有单独作为一行语句时才能被宏替换。因为宏可以是任意脚本,不一定是有返回值的表达式,作为变量时会存在语义问题。3 的宏本质是一个无参函数调用,所以常常被作为变量使用
如果想兼容 3 中的宏特性,建议使用 动态变量
动态变量
常规的 “静态变量”,是 context 中和 key 关联的固定的值。而动态变量可以是一个表达式,由另外一些变量计算而得。动态变量支持嵌套,即动态变量可以依赖另一个动态变量计算得到。
示例如下:
Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); Map<String, Object> staticContext = new HashMap<>(); staticContext.put("语文", 88); staticContext.put("数学", 99); staticContext.put("英语", 95); QLOptions defaultOptions = QLOptions.DEFAULT_OPTIONS; DynamicVariableContext dynamicContext = new DynamicVariableContext(express4Runner, staticContext, defaultOptions); dynamicContext.put("平均成绩", "(语文+数学+英语)/3.0"); dynamicContext.put("是否优秀", "平均成绩>90"); // dynamic var assertTrue((Boolean)express4Runner.execute("是否优秀", dynamicContext, defaultOptions).getResult()); assertEquals(94, ((Number)express4Runner.execute("平均成绩", dynamicContext, defaultOptions).getResult()).intValue()); // static var assertEquals(187, ((Number)express4Runner.execute("语文+数学", dynamicContext, defaultOptions).getResult()).intValue());
表达式计算追踪
如果打开相关选项,QLExpress4 就会在返回规则脚本计算结果的同时,返回一颗表达式追踪树。表达式追踪树的结构类似语法树,不同之处在于,它会在每个节点上记录本次执行的中间结果。
比如对于表达式 !true || myTest(a, 1),表达式追踪树的结构大概如下:
|| true
/ \
! false myTest
/ / \
true a 10 1可应用于多种场景:
-
方便业务人员对规则的计算结果进行分析排查
-
对线上判断为 false 的规则进行采样归类
-
AI 自动诊断和修复规则
节点计算结果会被放置到 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.builder().traceExpression(true).build()); 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 && true) && (!myTest(11) || false)", context, QLOptions.builder().traceExpression(true).build()); Assert.assertFalse((Boolean)resultShortCircuit.getResult()); ExpressionTrace expressionTraceShortCircuit = resultShortCircuit.getExpressionTraces().get(0); Assert.assertEquals( "OPERATOR && false\n" + " | OPERATOR && false\n" + " | VARIABLE a false\n" + " | VALUE true \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.builder().traceExpression(true).build()); 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时将InitOptions.traceExpression选项设置为 true,同时在执行脚本时将QLOptions.traceExpression设置为 true,该功能才能生效。
也可以在不执行脚本的情况下获得所有表达式追踪点:
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 |
列表 |
列表元素 |
MAP |
字段 |
无 |
IF |
条件分支 |
condition表达式,then逻辑块和else逻辑块 |
SWITCH |
switch分支 |
switch表达式,各个case分支条件和代码块,以及default代码块 |
RETURN |
返回语句 |
返回表达式 |
VARIABLE |
变量 |
无 |
VALUE |
字面值 |
无 |
DEFINE_FUNCTION |
定义函数 |
无 |
DEFINE_MACRO |
定义宏 |
无 |
PRIMARY |
暂时未继续下钻的其他复合值(比如字典,if等等) |
无 |
STATEMENT |
暂未继续下钻的其他复合语句(比如 while, for 等等) |
无 |
与 Spring 集成
QLExpress 并不需要专门与 Spring 集成,只需要一个 Express4Runner 单例,即可使用。
这里提供的 “集成” 示例,可以在 QLExpress 脚本中直接引用任意 Spring Bean。
这种方式虽然很方便,但是脚本权限过大,自由度太高。不再推荐使用,还是建议在 context 只放入允许用户访问的对象。
核心集成组件:
-
QLSpringContext: 实现了
ExpressContext接口,提供了对 Spring 容器的访问能力。它会优先从传入的 context 中查找变量,如果找不到则尝试从 Spring 容器中获取同名的 Bean。 -
QLExecuteService: 封装了 QLExpress 的执行逻辑,集成了 Spring 容器,方便在 Spring 应用中使用。
假设存在一个 Spring Bean, 名为 helloService:
package com.alibaba.qlexpress4.spring; import org.springframework.stereotype.Service; /** * Spring Bean example service class */ @Service public class HelloService { /** * Hello method that returns a greeting string * @return greeting string */ public String hello(String name) { return "Hello, " + name + "!"; } }
在脚本中调用该 Bean:
package com.alibaba.qlexpress4.spring; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import java.util.HashMap; import java.util.Map; /** * HelloService unit test class */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = SpringTestConfig.class) public class SpringDemoTest { @Autowired private QLExecuteService qlExecuteService; @Test public void qlExecuteWithSpringContextTest() { Map<String, Object> context = new HashMap<>(); context.put("name", "Wang"); String result = (String)qlExecuteService.execute("helloService.hello(name)", context); Assert.assertEquals("Hello, Wang!", result); } }