1110 lines
41 KiB
Plaintext
1110 lines
41 KiB
Plaintext
: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年开源。
|
||
|
||
在基本的表达式计算的基础上,还有以下特色:
|
||
|
||
* 灵活的自定义能力,通过 Java API 自定义函数和操作符,可以快速实现业务规则的 DSL
|
||
* 兼容最新的 Java 语法,方便 Java 程序员快速熟悉。熟悉类 C 语言的业务人员使用起来也会非常顺手
|
||
* 原生支持 JSON,快捷定义复杂数据结构
|
||
* 友好的报错提示,无论是编译还是运行时错误,都能精确友好地提示错误位置
|
||
* 独一无二的表达式追踪功能,可以追踪表达式在中间节点计算的值,方便业务人员或者 AI 对线上规则的计算结果进行归因分析
|
||
* 默认安全,脚本默认不允许和应用代码进行交互,如果需要交互,也可以自行定义安全的交互方式
|
||
* 解释执行,不占用 JVM 元空间,可以开启缓存提升解释性能
|
||
* 代码精简,依赖最小,适合所有java的运行环境
|
||
|
||
QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,新增了大量特色功能,彻底拥抱函数式编程,在性能和表达能力上都进行了进一步增强。
|
||
|
||
> 如果项目还是使用旧版本的 QLExpress,可以跳转 link:https://github.com/alibaba/QLExpress/tree/branch_version_3.x.x[branch_version_3.x.x] 维护分支查看旧版文档。如需升级可以参考 link:#附录一-升级指南[附录一-升级指南]
|
||
|
||
场景举例:
|
||
|
||
* 电商优惠券规则配置:通过 QLExpress 自定义函数和操作符快速实现优惠规则 DSL,供运营人员根据需求自行动态配置
|
||
* 表单搭建控件关联规则配置:表单搭建平台允许用户拖拽控件搭建自定义的表单,利用 QLExpress 脚本配置不同控件间的关联关系
|
||
* 流程引擎条件规则配置
|
||
* 广告系统计费规则配置
|
||
|
||
\...\...
|
||
|
||
== 新版特色
|
||
|
||
新版本并不是对旧版本的简单功能重构,而是我们基于对用户需求的洞察,对下一代规则表达式引擎的探索。拥有许多非常实用,但是在其他引擎中缺失的重要功能。
|
||
|
||
=== 表达式计算追踪
|
||
|
||
在业务人员完成规则脚本的配置后,很难对其线上执行情况进行感知。比如电商的促销规则,要求用户满足规则 `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 示例脚本:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/lambda/lambda_doc.ql[]
|
||
----
|
||
|
||
更多使用方法参考:
|
||
|
||
* link:#lambda-表达式[Lambda表达式]
|
||
* link:#列表过滤和映射[列表过滤和映射]
|
||
* link:#stream-api[Stream API]
|
||
* link:#函数式接口[函数式接口]
|
||
|
||
=== 分号简化
|
||
|
||
QLExpress4 支持省略分号,让表达式更加简洁。具体参考 link:#分号[分号]
|
||
|
||
== API 快速入门
|
||
|
||
=== 引入依赖
|
||
|
||
[source,xml]
|
||
----
|
||
<dependency>
|
||
<groupId>com.alibaba</groupId>
|
||
<artifactId>qlexpress4</artifactId>
|
||
<version>4.0.7</version>
|
||
</dependency>
|
||
----
|
||
|
||
环境要求:
|
||
|
||
* JDK 8 或更高版本
|
||
|
||
=== 第一个 QLExpress 程序
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl]
|
||
----
|
||
|
||
更多的表达式执行方式见文档 link:docs/execute.adoc[表达式执行]
|
||
|
||
=== 添加自定义函数与操作符
|
||
|
||
最简单的方式是通过 Java Lambda 表达式快速定义函数/操作符的逻辑:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionAndOperator]
|
||
----
|
||
|
||
如果自定义函数的逻辑比较复杂,或者需要获得脚本的上下文信息,也可以通过继承 `CustomFunction` 的方式实现。
|
||
|
||
比如下面的 `hello` 自定义函数,根据租户不同,返回不同的欢迎信息:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/test/function/HelloFunction.java[]
|
||
----
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customComplexFunction]
|
||
----
|
||
|
||
QLExpress4还支持通过QLExpress脚本添加自定义函数。需要注意的是,在函数外定义的变量(如示例中的defineTime)在函数定义时就已初始化完成,后续调用函数时不会重新计算该变量的值。
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addFunctionsDefinedInScript]
|
||
----
|
||
|
||
建议尽可能使用Java方式定义自定义函数,这样可以获得更好的性能和稳定性。
|
||
|
||
更多自定义语法元素的方式见文档 link:docs/custom-item.adoc[自定义语法元素]
|
||
|
||
=== 校验语法正确性
|
||
|
||
在不执行脚本的情况下,单纯校验语法的正确性,其中包含了操作符的限制校验,调用 `check` 并且捕获异常,如果捕获到 `QLSyntaxException`,则说明存在语法错误
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=checkSyntax]
|
||
----
|
||
|
||
你可以使用 `CheckOptions` 配置更精细的语法校验规则,主要支持以下两个选项:
|
||
|
||
1. `operatorCheckStrategy`: 操作符校验策略,用于限制脚本中可以使用的操作符
|
||
2. `disableFunctionCalls`: 是否禁用函数调用,默认为 false
|
||
|
||
示例1:使用操作符校验策略(白名单)
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/OperatorLimitTest.java[tag=operatorCheckStrategyExample]
|
||
----
|
||
|
||
示例2:禁用函数调用
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableFunctionCallsExample]
|
||
----
|
||
|
||
=== 解析脚本所需外部变量
|
||
|
||
脚本中使用的变量有的是脚本内生,有的是需要从外部通过 `context` 传入的。
|
||
|
||
QLExpress4 提供了一个方法,可以解析出脚本中所有需要从外部传入的变量:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=getOutVarNames]
|
||
----
|
||
|
||
更多脚本依赖解析工具:
|
||
|
||
* `getOutFunctions`: 解析所有需要从外部定义的函数
|
||
* `getOutVarAttrs`:解析所有需要从外部传入变量及其涉及的属性,`getOutVarNames` 的增强版本
|
||
|
||
=== 高精度计算
|
||
|
||
QLExpress 内部会用 BigDecimal 表示所有无法用 double 精确表示数字,来尽可能地表示计算精度:
|
||
|
||
> 举例:0.1 在 double 中无法精确表示
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=bigDecimalForPrecise]
|
||
----
|
||
|
||
通过这种方式能够解决一些计算精度问题:
|
||
|
||
比如 0.1+0.2 因为精度问题,在 Java 中是不等于 0.3 的。
|
||
而 QLExpress 能够自动识别出 0.1 和 0.2 无法用双精度精确表示,改成用 BigDecimal 表示,确保其结果等于0.3
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=preciseComparisonWithJava]
|
||
----
|
||
|
||
除了默认的精度保证外,还提供了 `precise` 开关,打开后所有的计算都使用BigDecimal,防止外部传入的低精度数字导致的问题:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=preciseSwitch]
|
||
----
|
||
|
||
=== 安全策略
|
||
|
||
QLExpress4 默认采用隔离安全策略,不允许脚本访问 Java 对象的字段和方法,这确保了脚本执行的安全性。如果需要访问 Java 对象,可以通过不同的安全策略进行配置。
|
||
|
||
假设应用中有如下的 Java 类:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/inport/MyDesk.java[]
|
||
----
|
||
|
||
脚本执行的上下文设置如下:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyContextSetup]
|
||
----
|
||
|
||
QLExpress4 提供了四种安全策略:
|
||
|
||
==== 1. 隔离策略(默认)
|
||
|
||
默认情况下,QLExpress4 采用隔离策略,不允许访问任何字段和方法:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyIsolation]
|
||
----
|
||
|
||
==== 2. 黑名单策略
|
||
|
||
通过黑名单策略,可以禁止访问特定的字段或方法,其他字段和方法可以正常访问:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyBlackList]
|
||
----
|
||
|
||
==== 3. 白名单策略
|
||
|
||
通过白名单策略,只允许访问指定的字段或方法,其他字段和方法都会被禁止:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyWhiteList]
|
||
----
|
||
|
||
==== 4. 开放策略
|
||
|
||
开放策略允许访问所有字段和方法,类似于 QLExpress3 的行为,但需要注意安全风险:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyOpen]
|
||
----
|
||
|
||
> 注意:开放策略虽然提供了最大的灵活性,但也带来了安全风险。建议只在受信任的环境中使用,不建议用于处理终端用户输入的脚本。
|
||
|
||
==== 策略建议
|
||
|
||
建议直接采用默认策略,在脚本中不要直接调用 Java 对象的字段和方法。而是通过自定义函数和操作符的方式(参考 link:#添加自定义函数与操作符[添加自定义函数与操作符]),对嵌入式脚本提供系统功能。这样能同时保证脚本的安全性和灵活性,用户体验还更好。
|
||
|
||
如果确实需要调用 Java 对象的字段和方法,至少应该使用白名单策略,只提供脚本有限的访问权限。
|
||
|
||
至于黑名单和开放策略,不建议在外部输入脚本的场景使用,除非确保每个脚本都会经过审核。
|
||
|
||
=== 调用应用中的 Java 类
|
||
|
||
> 需要放开安全策略,不建议用于终端用户输入
|
||
|
||
假设应用中有如下的 Java 类(`com.alibaba.qlexpress4.QLImportTester`):
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/QLImportTester.java[]
|
||
----
|
||
|
||
在 QLExpress 中有如下两种调用方式。
|
||
|
||
==== 1. 在脚本中使用 `import` 语句导入类并且使用
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=importJavaCls]
|
||
----
|
||
|
||
==== 2. 在创建 `Express4Runner` 时默认导入该类,此时脚本中就不需要额外的 `import` 语句
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=defaultImport]
|
||
----
|
||
|
||
除了用 `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` 导入给定类路径里的所有内部类
|
||
|
||
=== 自定义 ClassLoader
|
||
|
||
QLExpress4 支持通过自定义 `ClassSupplier` 来指定类加载器,这在插件化架构、模块化应用等场景中非常有用。通过自定义类加载器,可以让 QLExpress 脚本访问特定 ClassLoader 中的类。
|
||
|
||
下面的示例展示了如何与 link:https://pf4j.org/[PF4J] 插件框架集成,让 QLExpress 脚本能够访问插件中的类:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/pf4j/Pf4jClassSupplierTest.java[tag=pluginClassSupplier]
|
||
----
|
||
|
||
自定义 ClassSupplier 的典型应用场景:
|
||
|
||
* **插件化架构**:让脚本能够访问插件中定义的类和接口
|
||
* **模块化应用**:在 OSGi 等模块化框架中,让脚本访问特定模块的类
|
||
* **动态类加载**:从远程仓库或动态生成的字节码中加载类
|
||
* **类隔离**:使用不同的 ClassLoader 来实现类的隔离
|
||
|
||
=== 表达式缓存
|
||
|
||
通过 `cache` 选项可以开启表达式缓存,这样相同的表达式就不会重新编译,能够大大提升性能。
|
||
|
||
注意该缓存没有限制大小,只适合在表达式为有限数量的情况下使用:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=cacheSwitch]
|
||
----
|
||
|
||
但是当脚本首次执行时,因为没有缓存,依旧会比较慢。
|
||
|
||
可以通过下面的方法在首次执行前就将脚本缓存起来,保证首次执行的速度:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=parseToCache]
|
||
----
|
||
|
||
注意该缓存的大小是无限的,业务上注意控制大小,可以调用 `clearCompileCache` 方法定期清空编译缓存。
|
||
|
||
=== 清除 DFA 缓存
|
||
|
||
QLExpress 使用 ANTLR4 作为解析引擎,ANTLR4 在运行时会构建 DFA (确定有限状态自动机) 缓存来加速后续的语法解析。这个缓存会占用一定的内存空间。
|
||
|
||
在某些内存敏感的场景下,可以通过调用 `clearDFACache` 方法来清除 DFA 缓存,释放内存。
|
||
|
||
> **重要警告**: 清除 DFA 缓存会导致编译性能大幅下降,正常情况下不建议使用此方法。
|
||
|
||
==== 适用场景
|
||
|
||
* **内存敏感型应用**: 当内存使用是关键考虑因素,且可以容忍较慢的编译时间时
|
||
* **脚本不经常变更**: 当脚本相对稳定且不会频繁重新编译时
|
||
|
||
==== 最佳实践
|
||
|
||
在解析并缓存表达式后立即调用此方法,并确保后续所有执行都打开缓存选项以避免重新编译。示例代码如下:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/ClearDfaCacheTest.java[tag=clearDFACacheBestPractice]
|
||
----
|
||
|
||
通过这种方式,可以在保证性能的同时,最大限度地降低内存占用。
|
||
|
||
=== 设置超时时间
|
||
|
||
可以给脚本设置一个超时时间,防止其中存在死循环或者其他原因导致应用资源被过量消耗。
|
||
|
||
下面的示例代码给脚本给脚本设置了一个 10ms 的超时时间:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=scripTimeout]
|
||
----
|
||
|
||
> 注意,出于系统性能的考虑,QLExpress 对于超时时间的检测是不准确的。特别是在回调Java代码中(比如自定义函数或者操作符)出现的超时,不会立刻被检测到。只有在执行完,回到 QLExpress 运行时后才会被检测到并中断执行。
|
||
|
||
=== 扩展函数
|
||
|
||
利用 QLExpress 提供的扩展函数能力,可以给Java类中添加额外的成员方法。
|
||
|
||
扩展函数是基于 QLExpress 运行时实现的,因此仅仅在 QLExpress 脚本中有效。
|
||
|
||
下面的示例代码给 String 类添加了一个 `hello()` 扩展函数:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=extensionFunction]
|
||
----
|
||
|
||
=== Java类的对象,字段和方法别名
|
||
|
||
QLExpress 支持通过 `QLAlias` 注解给对象,字段或者方法定义一个或多个别名,方便非技术人员使用表达式定义规则。
|
||
|
||
下面的例子中,根据用户是否 vip 计算订单最终金额。
|
||
|
||
用户类定义:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/test/qlalias/User.java[]
|
||
----
|
||
|
||
订单类定义:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/test/qlalias/Order.java[]
|
||
----
|
||
|
||
通过 QLExpress 脚本规则计算最终订单金额:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=qlAlias]
|
||
----
|
||
|
||
=== 关键字,操作符和函数别名
|
||
|
||
为了进一步方面非技术人员编写规则,QLExpress 提供 `addAlias` 给原始关键字,操作符和函数增加别名。让整个脚本的表述更加贴近自然语言。
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addAlias]
|
||
----
|
||
|
||
支持设置别名的关键字有:
|
||
|
||
* if
|
||
* then
|
||
* else
|
||
* for
|
||
* while
|
||
* break
|
||
* continue
|
||
* return
|
||
* function
|
||
* macro
|
||
* new
|
||
* null
|
||
* true
|
||
* false
|
||
|
||
> 注意:部分大家熟悉的用法其实是操作符,而不是关键字,比如 `in` 操作符。而所有的操作符和函数默认就是支持别名的
|
||
|
||
=== 宏
|
||
|
||
宏是QLExpress中一个强大的代码复用机制,它允许用户定义一段可重用的脚本片段,并在需要时进行调用。与简单的文本替换不同,QLExpress的宏是基于指令回放的机制实现的,具有更好的性能和语义准确性。
|
||
|
||
宏特别适用于以下场景:
|
||
|
||
* **代码复用**:将常用的脚本片段封装成宏,避免重复编写相同的逻辑
|
||
* **业务规则模板**:定义标准的业务规则模板,如价格计算、权限检查等
|
||
* **流程控制**:封装复杂的控制流程,如条件判断、循环逻辑等
|
||
* **DSL构建**:作为构建领域特定语言的基础组件
|
||
|
||
宏可以通过两种方式定义:
|
||
|
||
**1. 在脚本中使用 `macro` 关键字定义**
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/macro/macro.ql[tag=addMacroInScript]
|
||
----
|
||
|
||
**2. 通过Java API添加**
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=addMacro]
|
||
----
|
||
|
||
宏与函数的区别:
|
||
|
||
[cols="1,1,1"]
|
||
|===
|
||
| 特性 | 宏 | 函数
|
||
| 参数传递 | 无参数,依赖上下文变量 | 支持参数传递
|
||
| 性能 | 指令直接插入,无调用开销 | 有函数调用开销
|
||
| 作用域 | 共享调用者作用域 | 独立的作用域
|
||
| 适用场景 | 代码片段复用 | 逻辑封装和参数化
|
||
|===
|
||
|
||
宏特别适合那些不需要参数传递、主要依赖上下文变量的代码片段复用场景,而函数更适合需要参数化和独立作用域的场景。
|
||
|
||
**QLExpress4 相比 3 版本,宏特性的变化**:
|
||
|
||
* 4 的宏实现更加接近通常编程语言中宏的定义,相当于将预定义的代码片段插入到宏所在的位置,与调用点位于同一作用域,宏中的 `return`, `contine` 和 `break` 等可以影响调用方的控制流。但是 3 中的实现其实更加接近无参函数调用。
|
||
* 4 的宏无法作为变量使用,只有单独作为一行语句时才能被宏替换。因为宏可以是任意脚本,不一定是有返回值的表达式,作为变量时会存在语义问题。3 的宏本质是一个无参函数调用,所以常常被作为变量使用
|
||
|
||
如果想兼容 3 中的宏特性,建议使用 link:#动态变量[动态变量]
|
||
|
||
=== 动态变量
|
||
|
||
常规的 “静态变量”,是 context 中和 key 关联的固定的值。而动态变量可以是一个表达式,由另外一些变量计算而得。动态变量支持嵌套,即动态变量可以依赖另一个动态变量计算得到。
|
||
|
||
示例如下:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=dynamicVar]
|
||
----
|
||
|
||
=== 表达式计算追踪
|
||
|
||
如果打开相关选项,QLExpress4 就会在返回规则脚本计算结果的同时,返回一颗表达式追踪树。表达式追踪树的结构类似语法树,不同之处在于,它会在每个节点上记录本次执行的中间结果。
|
||
|
||
比如对于表达式 `!true || myTest(a, 1)`,表达式追踪树的结构大概如下:
|
||
|
||
[source]
|
||
----
|
||
|| true
|
||
/ \
|
||
! false myTest
|
||
/ / \
|
||
true a 10 1
|
||
----
|
||
|
||
可应用于多种场景:
|
||
|
||
* 方便业务人员对规则的计算结果进行分析排查
|
||
* 对线上判断为 false 的规则进行采样归类
|
||
* AI 自动诊断和修复规则
|
||
|
||
节点计算结果会被放置到 `ExpressionTrace` 对象的 `value` 字段中。如果中间发生短路导致部分表达式未被计算,则 `ExpressionTrace` 对象的 `evaluated` 字段会被设置为 false。代码示例如下:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=expressionTrace]
|
||
----
|
||
|
||
> 注意,必须在新建 `Express4Runner` 时将 `InitOptions.traceExpression` 选项设置为 true,同时在执行脚本时将 `QLOptions.traceExpression` 设置为 true,该功能才能生效。
|
||
|
||
也可以在不执行脚本的情况下获得所有表达式追踪点:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=getExpressionTracePoints]
|
||
----
|
||
|
||
支持的表达式追踪点类型以及对应子节点的含义如下:
|
||
|
||
[cols="1,1,1"]
|
||
|===
|
||
| 节点类型 | 节点含义 | 子节点含义
|
||
| OPERATOR | 操作符 | 两侧操作数
|
||
| FUNCTION | 函数 | 函数参数
|
||
| METHOD | 方法 | 方法参数
|
||
| FIELD | 字段 | 取字段的目标对象
|
||
| LIST | 列表 | 列表元素
|
||
| MAP | 字段 | 无
|
||
| IF | 条件分支 | condition表达式,then逻辑块和else逻辑块
|
||
| RETURN | 返回语句 | 返回表达式
|
||
| VARIABLE | 变量 | 无
|
||
| VALUE | 字面值 | 无
|
||
| DEFINE_FUNCTION | 定义函数 | 无
|
||
| DEFINE_MACRO | 定义宏 | 无
|
||
| PRIMARY | 暂时未继续下钻的其他复合值(比如字典,if等等)| 无
|
||
| STATEMENT | 暂未继续下钻的其他复合语句(比如 while, for 等等)| 无
|
||
|===
|
||
|
||
=== 与 Spring 集成
|
||
|
||
QLExpress 并不需要专门与 Spring 集成,只需要一个 `Express4Runner` 单例,即可使用。
|
||
|
||
这里提供的 “集成” 示例,可以在 QLExpress 脚本中直接引用任意 Spring Bean。
|
||
|
||
这种方式虽然很方便,但是脚本权限过大,自由度太高。不再推荐使用,还是建议在 context 只放入允许用户访问的对象。
|
||
|
||
核心集成组件:
|
||
|
||
* link:src/test/java/com/alibaba/qlexpress4/spring/QLSpringContext.java[QLSpringContext]: 实现了 `ExpressContext` 接口,提供了对 Spring 容器的访问能力。它会优先从传入的 context 中查找变量,如果找不到则尝试从 Spring 容器中获取同名的 Bean。
|
||
* link:src/test/java/com/alibaba/qlexpress4/spring/QLExecuteService.java[QLExecuteService]: 封装了 QLExpress 的执行逻辑,集成了 Spring 容器,方便在 Spring 应用中使用。
|
||
|
||
假设存在一个 Spring Bean, 名为 `helloService`:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/spring/HelloService.java[]
|
||
----
|
||
|
||
在脚本中调用该 Bean:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/spring/SpringDemoTest.java[]
|
||
----
|
||
|
||
|
||
== 语法入门
|
||
|
||
QLExpress4 兼容 Java8 语法的同时,也提供了很多更加灵活宽松的语法模式,帮助用户更快捷地编写表达式。
|
||
|
||
基于表达式优先的语法设计,复杂的条件判断语句也可以直接当作表达式使用。
|
||
|
||
在本章节中出现的代码片段都是 qlexpress 脚本,
|
||
`assert` 是测试框架往引擎中注入的断言方法,会确保其参数为 `true`。
|
||
`assertErrCode` 会确保其 lambda 参数表达式的执行一定会抛出含第二个参数 error code 的 QLException。
|
||
|
||
=== 变量声明
|
||
|
||
同时支持静态类型和动态类型:
|
||
|
||
* 变量声明时不写类型,则变量是动态类型,也同时是一个赋值表达式
|
||
* 变量声明如果写类型,则是静态类型,此时是一个变量声明语句
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/dynamic_typing.ql[]
|
||
----
|
||
|
||
=== 方便语法元素
|
||
|
||
列表(List),映射(Map)等常用语法元素在 QLExpress 中都有非常方便的构造语法糖:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql[]
|
||
----
|
||
|
||
通过 `*.` 操作符,可以快捷地对列表和映射进行处理,比如对列表元素进行取属性,或者获得映射的 key 列表和 value 列表:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/spread/list_spread.ql[tag=spreadExample]
|
||
----
|
||
|
||
在映射中通过 `@class` key 指定类型的全限定名,就可以直接使用 JSON 创建复杂Java对象。比如下面的 MyHome , 是一个含有复杂嵌套类型 Java 类:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/inport/MyHome.java[]
|
||
----
|
||
|
||
可以通过下面的 QLExpress 脚本,便捷创建:
|
||
|
||
> 注意,该特性需要参考 link:#安全策略[安全策略] 打开安全选项,才能正常执行。
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/java/map/classified_json.ql[tag=classifiedJson]
|
||
----
|
||
|
||
=== 数字
|
||
|
||
对于未声明类型的数字,
|
||
QLExpress会根据其所属范围自动从 int, long, BigInteger, double, BigDecimal 等数据类型中选择一个最合适的:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/java/number/number_auto_type.ql[]
|
||
----
|
||
|
||
因此在自定义函数或者操作符时,建议使用 Number 类型进行接收,因为数字类型是无法事先确定的。
|
||
|
||
=== 动态字符串
|
||
|
||
动态字符串是 QLExpress 为了增强字符串处理能力,在 4 版本新引入的能力。
|
||
|
||
支持 `$\{expression}` 的格式在字符串中插入表达式计算:
|
||
|
||
> 如果想在字符串中原样保持 `$\{expression}`,可以使用 `\$` 对 `$` 进行转义
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/dynamic_string.ql[]
|
||
----
|
||
|
||
如果还想让 QLExpress4 的字符串和 3 保持兼容性,不对插值表达式进行处理,可以在新建 `Express4Runner` 时直接关闭该特性:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableInterpolation]
|
||
----
|
||
|
||
=== 模板语言
|
||
|
||
利用动态字符串能力,QLExpress4 也可以作为一个轻量级模板引擎使用。
|
||
|
||
无需在脚本中手动添加字符串引号,直接调用 `executeTemplate` 渲染模板字符串:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=templateEngine]
|
||
----
|
||
|
||
=== 占位符
|
||
|
||
占位符用于从 context 中提取任意 key 的值。
|
||
|
||
全局变量也可以从 context 中提取值,但是收到 QLExpress 关键词和语法的限制,能提取的 key 有限。
|
||
比如 context 中 "0" key 对应的值就无法通过变量提取,因为 0 不是 QLExpress 中的合法变量,而是一个数字常量。
|
||
此时可以用默认占位符 `$\{0}` 来提取。
|
||
|
||
> 注意和动态字符串中插值区分,占位符是写在字符串之外。动态字符串插值是 `$\{expression}`,其中默认写的是表达式,`"${0}"` 的运行结果是 `"0"`。而占位符是 `$\{placeholder}`,其中默认写的是 context 中的 key,`${0}` 的运行结果是 context 中 "0" key 对应的值。
|
||
|
||
QLExpress 默认使用 `$\{placeholder}` 格式的占位符,其中:
|
||
|
||
* `${` 是起始标记
|
||
* `}` 是结束标记
|
||
* `placeholder` 是占位符内容,对应 cotext 中的 key
|
||
|
||
除了默认的占位符外,QLExpress 还支持自定义占位符的起始和结束标记:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=customSelector]
|
||
----
|
||
|
||
自定义占位符并不是任意的,限制条件如下:
|
||
|
||
* **起始标记限制**:`selectorStart` 必须是以下四种格式之一:
|
||
** `${` (默认)
|
||
** `$[`
|
||
** `#{`
|
||
** `#[`
|
||
* **结束标记限制**:`selectorEnd` 必须是 1 个或更多字符的字符串
|
||
|
||
=== 分号
|
||
|
||
表达式语句可以省略结尾的分号,整个脚本的返回值就是最后一个表达式的计算结果。
|
||
|
||
以下脚本的返回值为 2:
|
||
|
||
[source,java]
|
||
----
|
||
a = 1
|
||
b = 2
|
||
// last express
|
||
1+1
|
||
----
|
||
|
||
等价于以下写法:
|
||
|
||
[source,java]
|
||
----
|
||
a = 1
|
||
b = 2
|
||
// return statment
|
||
return 1+1;
|
||
----
|
||
|
||
因为分号可以省略,QLExpress4 对于换行的处理相比 3 或者 Java 语言更加严格。如果想要将多行表达式拆成多行,建议将操作符保留在当前行,而将右操作数换到下一行。
|
||
|
||
以下多行表达式会报语法错误(反例):
|
||
|
||
[source,java]
|
||
----
|
||
// syntax error
|
||
a
|
||
+ b
|
||
----
|
||
|
||
以下是正确的换行示例(正例):
|
||
|
||
[source,java]
|
||
----
|
||
a +
|
||
b
|
||
----
|
||
|
||
其他的语法习惯保持和 Java 一致即可。
|
||
|
||
=== 表达式
|
||
|
||
QLExpress 采用表达式优先的设计,其中 除了 import, return 和循环等结构外,几乎都是表达式。
|
||
|
||
if 语句也是一个表达式:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/if_as_expr.ql[]
|
||
----
|
||
|
||
try catch 结构也是一个表达式:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql[]
|
||
----
|
||
|
||
=== 短路计算
|
||
|
||
和 Java 类似,`&&` 和 `||` 逻辑运算都是短路运算的。
|
||
|
||
比如表达式 `false && (1/0)` 不会发生除 0 错误,因为 `&&` 短路在了最开始的 `false` 处。
|
||
|
||
短路计算默认是开启的,引擎也提供了选项,可以在某次执行时将短路关闭:
|
||
|
||
> 关闭短路的一个场景是保证表达式的充分预热
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=disableShortCircuit]
|
||
----
|
||
|
||
|
||
=== 控制结构
|
||
|
||
==== if 分支
|
||
|
||
除了完全兼容 Java 中的 `if` 写法,还支持类似规则引擎的 `if ... then ... else ...` 的写法,其中 `then` 可以当成一个可以省略的关键字:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/if.ql[]
|
||
----
|
||
|
||
==== while 循环
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/while.ql[]
|
||
----
|
||
|
||
==== for 循环
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/for.ql[]
|
||
----
|
||
|
||
==== for-each 循环
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/for_each.ql[]
|
||
----
|
||
|
||
==== try-catch
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/try_catch.ql[]
|
||
----
|
||
|
||
=== 函数定义
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/function.ql[]
|
||
----
|
||
|
||
=== Lambda 表达式
|
||
|
||
QLExpress4 中,Lambda 表达式作为一等公民,可以作为变量进行传递或者返回。
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/lambda.ql[]
|
||
----
|
||
|
||
=== 列表过滤和映射
|
||
|
||
支持通过 filter, map 方法直接对列表类型进行函数式过滤和映射。
|
||
|
||
底层通过在列表类型添加 link:#扩展函数[扩展函数] 实现,注意和 Stream API 中同名方法区分。
|
||
|
||
相比 Stream Api,它可以直接对列表进行操作,返回值也直接就是列表,更加方便。
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/independent/doc/list_map_filter.ql[]
|
||
----
|
||
|
||
=== 兼容 Java8 语法
|
||
|
||
QLExpress 可以兼容 Java8 的常见语法。
|
||
|
||
比如 link:#for-each-循环[for each循环], Stream API, 函数式接口等等。
|
||
|
||
==== Stream API
|
||
|
||
可以直接使用 Java 集合中的 stream api 对集合进行操作。
|
||
|
||
因为此时的 stream api 都是来自 Java 中的方法,参考 link:#安全策略[安全策略] 打开安全选项,以下脚本才能正常执行。
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/java/stream/java_stream.ql[]
|
||
----
|
||
|
||
==== 函数式接口
|
||
|
||
Java8 中引入了 Function, Consumer, Predicate 等函数式接口,QLExpress 中的 link:#lambda-表达式[Lambda表达式] 可以赋值给这些接口,或者作为接收这些接口的方法参数:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/resources/testsuite/java/lambda/java_functional_interface.ql[]
|
||
----
|
||
|
||
== 附录一 升级指南
|
||
|
||
QLExpress 的上一版本因为多年的迭代停滞,在各项特性上和业界产生了较大差距。
|
||
|
||
QLExpress4 的目标之一就是一次性弥补这些差距,因此选择进行了大刀阔斧的升级,而有意放弃了部分兼容性。当然,基础的功能和体验还是和上一版本保持了对齐。
|
||
|
||
如果系统已经使用老版本的 QLExpress,升级之前务必要进行一次全面的回归测试,确保这些脚本都能在新版中正常执行,再进行升级。
|
||
|
||
如果没有时间或者方法对它们一一验证,那么不建议进行升级。
|
||
|
||
如果是新系统,建议直接采用 QLExpress4,未来 QLExpress4 的生态建设会越来越完善,而 3 会被逐渐抛弃。
|
||
|
||
下面将列表新版和旧版的主要不同,方便用户对已有脚本进行升级。如有遗漏,欢迎反馈:
|
||
|
||
=== 默认安全策略
|
||
|
||
如果完全使用默认选项,获取 Java 对象的字段(`o.field`),或者调用成员方法(`o.method()`),则会分别抛出 `FIELD_NOT_FOUND` 和 `METHOD_NOT_FOUND` 错误。
|
||
|
||
这是因为 3 可以没有限制地通过反射访问 Java 应用系统中的任意字段和方法,这在嵌入式脚本中被认为是不安全的。
|
||
|
||
如果想兼容 3 的行为,则在新建 `Express4Runner` 时, 要将安全策略设置为 “开放”,参考代码如下:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=securityStrategyOpen]
|
||
----
|
||
|
||
详细参考 link:#安全策略[安全策略] 章节。
|
||
|
||
=== 定义映射
|
||
|
||
QLExpress 老版本支持通过 `NewMap(key:value)` 的方式快速创建映射,虽然在文档中没有详细讨论,但是很多用户通过单元测试和询问的方式,知晓并使用了这个语法。
|
||
|
||
不过这种语法过于定制,也和业界的规范相差很大,因此在新版中将其移除。
|
||
|
||
新版原生支持 JSON 语法,直接采用 JSON 字典的格式(`{key:value}`)即可快速创建映射,更加直观。
|
||
|
||
详细参考 link:#方便语法元素[方便语法元素]
|
||
|
||
=== 全局变量污染上下文
|
||
|
||
QLExpress 支持在执行脚本时传入一个全局的上下文,即 context 参数。
|
||
|
||
在老版本中,如果脚本中定义了全局变量,则这些变量也会写入到 context。在脚本执行结束后,可以通过 context 获取到脚本中定义的全局变量的值。
|
||
|
||
一个老版本的列子如下:
|
||
|
||
[source,java]
|
||
----
|
||
// only for QLExpress 3.x
|
||
|
||
String express = "a=3;a+1";
|
||
ExpressRunner runner = new ExpressRunner(false, true);
|
||
DefaultContext<String, Object> context = new DefaultContext<>();
|
||
|
||
Object res = runner.execute(express, context, null, true, true);
|
||
// The result of the script execution should be 4 (a+1)
|
||
Assert.assertEquals(4, res);
|
||
// The variable 'a' defined in the script is also stored in the context
|
||
Assert.assertEquals(3, context.get("a"));
|
||
----
|
||
|
||
根据调研和反馈,我们认为这会导致全局上下文被脚本 “污染”,存在安全性问题。
|
||
|
||
因此在 QLExpress4 中,全局变量默认不会写入到 context 中。
|
||
|
||
如果想要兼容 3 的特性,需要将 `polluteUserContext` 选项设置为 `true`,参考代码如下:
|
||
|
||
[source,java,indent=0]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=polluteUserContext]
|
||
----
|
||
|
||
=== 分号可省略
|
||
|
||
"分号可省略" 已经是现代脚本语言的一个标配,QLExpress4 也跟进了这个特性,分号是可以省略的。
|
||
|
||
具体参考 link:#分号[分号] 章节。
|
||
|
||
=== 严格换行模式
|
||
|
||
由于 QLExpress4 支持分号可省略,解释器需要通过换行符来判断语句是否结束。因此,QLExpress4 对换行的要求比 QLExpress3 更加严格。
|
||
|
||
下面的脚本在 QLExpress3 中是合法的,但在 QLExpress4 中不是:
|
||
|
||
[source,java]
|
||
----
|
||
// 在 QLExpress3 中合法的脚本,但在 QLExpress4 中不是
|
||
商家应收=
|
||
价格
|
||
- 饭卡商家承担
|
||
+ 平台补贴
|
||
----
|
||
|
||
在 QLExpress4 中,上述脚本会被解析为两个独立的语句:
|
||
1. `商家应收 = 价格`
|
||
2. `- 饭卡商家承担 + 平台补贴`(第二个语句会报语法错误)
|
||
|
||
如果要在 QLExpress4 中实现同样的效果,需要将操作符放在行尾,而不是行首:
|
||
|
||
[source,java]
|
||
----
|
||
// QLExpress4 中正确的写法
|
||
商家应收=
|
||
价格 -
|
||
饭卡商家承担 +
|
||
平台补贴
|
||
----
|
||
|
||
这样解释器就知道当前行的表达式还未结束,会继续读取下一行。
|
||
|
||
如果您需要兼容 QLExpress3 的换行特性,可以设置 `strictNewLines` 选项为 `false`:
|
||
|
||
[source,java]
|
||
----
|
||
include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=notStrictNewLinesTest]
|
||
----
|
||
|
||
注意:非严格换行模式会让解释器忽略所有换行符,可能会影响代码的可读性和错误提示的准确性。建议仅在需要兼容旧代码时使用。
|
||
|
||
=== 获得 char 类型
|
||
|
||
在 QLExpress 3 中,单引号包裹的单个字符会被解析为 char 类型,而不是 String。
|
||
|
||
这个给用户带来了不少困惑,比如 `"a"=='a'` 的判断结果是 `false`。
|
||
|
||
所以后来 QLExpress 3 中新增了 `ExpressRunner.setIgnoreConstChar` 选项,设置为 `true` 后,所有的单引号和双引号包裹的字符都会被解析为 String 类型。但是这个选项默认是关闭的,需要用户手动开启。
|
||
|
||
考虑到脚本用户很少会使用到 `char` 这种底层类型,我们在 QLExpress 4 中直接取消了这个选项,所有的单引号和双引号包裹的字符都会被解析为 String 类型。
|
||
|
||
如果您在脚本还是需要使用 `char` 类型,可以通过两种方法获得:
|
||
|
||
* 类型强转:`(char) 'a'`
|
||
* 类型声明:`char a = 'a'`
|
||
|
||
== 附录二 如何贡献?
|
||
|
||
QLExpress 对社区的更改完全开放,任何建议和修改,都会受到欢迎,讨论后合理最后会被接纳到主干中。
|
||
|
||
首先需要将代码 clone 到本地,在正式修改代码前,需要先进行如下准备:
|
||
|
||
1. 项目根目录执行 `mvn compile`:项目刚刚下载到本地时,会有大量的类找不到,需要先生成 Antlr4 的运行时代码
|
||
2. 配置代码格式化:QLExpress 项目有统一的代码格式规范,开发前需要配置在 git 提交前的自动格式化
|
||
|
||
在项目目录下新建文件 `.git/hooks/pre-commit`,内容如下:
|
||
|
||
[source,bash]
|
||
----
|
||
#!/bin/sh
|
||
mvn spotless:apply
|
||
git add -u
|
||
exit 0
|
||
----
|
||
|
||
这样在每次 git commit 之前,就会自动执行 maven 的 spotless 插件执行代码格式化,具体代码格式配置见 link:spotless_eclipse_formatter.xml[]
|
||
|
||
3. 执行单元测试:在开发完成代码之后,先在本地全量执行单元测试,确保代码质量
|
||
- JDK 8 环境:执行 `mvn test`
|
||
- JDK 9 及以上环境:执行 `mvn test -DargLine="--add-opens java.base/java.util.stream=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"`
|
||
|
||
== 附录三 QLExpress4性能提升
|
||
|
||
link:https://www.yuque.com/xuanheng-ffjti/iunlps/pgfzw46zel2xfnie?singleDoc#%20%E3%80%8AQLExpress3%E4%B8%8E4%E6%80%A7%E8%83%BD%E5%AF%B9%E6%AF%94%E3%80%8B[QLExpress4与3性能对比]
|
||
|
||
总结:常见场景下,无编译缓存时,QLExpress4能比3有接近10倍性能提升;有编译缓存,也有一倍性能提升。
|
||
|
||
== 附录四 开发者联系方式
|
||
|
||
* Email:
|
||
** qinyuan.dqy@alibaba-inc.com
|
||
** yumin.pym@taobao.com
|
||
** 704643716@qq.com
|
||
* WeChat:
|
||
** xuanheng: dqy932087612
|
||
** binggou: pymbupt
|
||
** linxiang: tkk33362
|
||
* DingTalk Support Group
|
||
|
||
image::images/qlexpress_support_group_qr_2026.jpg[]
|