From a33935cd6e693eb71cb34e9a0d6b299b09559d47 Mon Sep 17 00:00:00 2001 From: DQinYuan <932087612@qq.com> Date: Sun, 29 Dec 2024 22:37:16 +0800 Subject: [PATCH] readme adoc --- README-source.adoc | 342 +++++++++- README.md | 587 ------------------ pom.xml | 2 +- src/main/antlr4/QLParser.g4 | 2 +- src/main/antlr4/QLexer.g4 | 6 +- .../qlexpress4/Express4RunnerTest.java | 32 +- .../alibaba/qlexpress4/TestSuiteRunner.java | 2 +- .../aparser/SyntaxTreeFactoryTest.java | 4 +- .../doc/convenient_syntax_elements.ql | 16 + .../independent/doc/dynamic_string.ql | 9 + .../independent/doc/dynamic_typing.ql | 8 + .../testsuite/independent/doc/for.ql | 5 + .../testsuite/independent/doc/for_each.ql | 8 + .../testsuite/independent/doc/function.ql | 4 + .../resources/testsuite/independent/doc/if.ql | 8 + .../testsuite/independent/doc/if_as_expr.ql | 5 + .../testsuite/independent/doc/lambda.ql | 4 + .../independent/doc/list_map_filter.ql | 3 + .../testsuite/independent/doc/try_catch.ql | 6 + .../independent/doc/try_catch_as_expr.ql | 6 + .../testsuite/independent/doc/while.ql | 7 + .../independent/string/interpolation.ql | 14 +- 22 files changed, 473 insertions(+), 607 deletions(-) delete mode 100644 README.md create mode 100644 src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql create mode 100644 src/test/resources/testsuite/independent/doc/dynamic_string.ql create mode 100644 src/test/resources/testsuite/independent/doc/dynamic_typing.ql create mode 100644 src/test/resources/testsuite/independent/doc/for.ql create mode 100644 src/test/resources/testsuite/independent/doc/for_each.ql create mode 100644 src/test/resources/testsuite/independent/doc/function.ql create mode 100644 src/test/resources/testsuite/independent/doc/if.ql create mode 100644 src/test/resources/testsuite/independent/doc/if_as_expr.ql create mode 100644 src/test/resources/testsuite/independent/doc/lambda.ql create mode 100644 src/test/resources/testsuite/independent/doc/list_map_filter.ql create mode 100644 src/test/resources/testsuite/independent/doc/try_catch.ql create mode 100644 src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql create mode 100644 src/test/resources/testsuite/independent/doc/while.ql diff --git a/README-source.adoc b/README-source.adoc index a2630757..3e7e820f 100644 --- a/README-source.adoc +++ b/README-source.adoc @@ -1,4 +1,6 @@ -= QLExpress4 +:toc: + += QLExpress == 背景介绍 @@ -30,7 +32,7 @@ QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解 com.alibaba QLExpress4 - 4.0.0-beta + 4.0.0-beta.2 ---- @@ -39,4 +41,338 @@ QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解 [source,java,indent=0] ---- include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=firstQl] ----- \ No newline at end of file +---- + +=== 添加自定义函数与操作符 + +最简单的方式是通过 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] +---- + +=== 高精度计算 + +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] +---- + +=== 调用应用中的 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] +---- + +=== 表达式缓存 + +通过 `cache` 选项可以开启表达式缓存,这样相同的表达式就不会重新编译,能够大大提升性能。 + +注意该缓存没有限制大小,只适合在表达式为有限数量的情况下使用: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=cacheSwitch] +---- + +=== 扩展函数 + +利用 QLExpress 提供的扩展函数能力,可以给Java类中添加额外的成员方法。 + +扩展函数是基于 QLExpress 运行时实现的,因此仅仅在 QLExpress 脚本中有效。 + +下面的示例代码给 String 类添加了一个 `hello()` 扩展函数: + +[source,java,indent=0] +---- +include::./src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java[tag=extensionFunction] +---- + +=== 对象,字段和函数别名 + +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] +---- + +== 语法入门 + +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[] +---- + +=== 数字 + +对于未声明类型的数字, +QLExpress会根据其所属范围自动从 int, long, BigInteger, double, BigDecimal 等数据类型中选择一个最合适的: + +[source,java] +---- +include::./src/test/resources/testsuite/java/number/number_auto_type.ql[] +---- + +因此在自定义函数或者操作符时,建议使用 Number 类型进行接收,因为数字类型是无法事先确定的。 + +=== 动态字符串 + +QLExpress 支持 `$\{expression}` 的格式在字符串中插入表达式,更方便地进行动态字符串组装: + + +[source,java] +---- +include::./src/test/resources/testsuite/independent/doc/dynamic_string.ql[] +---- + +=== 分号 + +表达式语句可以省略结尾的分号,整个脚本的返回值就是最后一个表达式的计算结果。 + +以下脚本的返回值为 2: + +[source,java] +---- +a = 1 +b = 2 +// last express +1+1 +---- + +等价于以下写法: + +[source,java] +---- +a = 1 +b = 2 +// return statment +return 1+1; +---- + +=== 表达式 + +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[] +---- + +=== 控制结构 + +==== if 分支 + +[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:#_调用应用中的_java_类[调用应用中的Java类] 打开安全选项,以下脚本才能正常执行。 + +[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[] +---- + +== 附录一 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 + diff --git a/README.md b/README.md deleted file mode 100644 index cd8ccb77..00000000 --- a/README.md +++ /dev/null @@ -1,587 +0,0 @@ -# 背景介绍 - -由阿里的电商业务规则演化而来的嵌入式Java动态脚本工具,在阿里集团有很强的影响力,同时为了自身不断优化、发扬开源贡献精神,于2012年开源。 - -在基本的表达式计算的基础上,增加以下特色功能: - - - 灵活的自定义能力,通过 Java API 自定义函数和操作符,可以快速实现业务规则的 DSL - - 兼容Java语法,最新的QLExpress4可以兼容Java8语法,方便Java程序员快速熟悉 - - 默认安全,脚本默认不允许和应用代码进行交互,如果需要交互,也可以自行定义安全的交互方式 - - 解释执行,不占用 JVM 元空间,可以开启缓存提升解释性能 - -QLExpress4 作为 QLExpress 的最新演进版本,基于 Antlr4 重写了解析引擎,将原先的优点进一步发扬光大,彻底拥抱Java8和函数式编程,在性能和表达式能力上都进行了进一步增强。 -场景举例: - - - 电商优惠券规则配置:通过 QLExpress 自定义函数和操作符快速实现优惠规则 DSL,供运营人员根据需求自行动态配置 - - 表单搭建控件关联规则配置:表单搭建平台允许用户拖拽控件搭建自定义的表单,利用 QLExpress 脚本配置不同控件间的关联关系 - - 流程引擎条件规则配置 - - 广告系统计费规则配置 -...... - -# API 快速入门 - -## 引入依赖 - -```xml - - com.alibaba - QLExpress4 - 4.0.0-beta - -``` - -## 第一个 QLExpress 程序 - -```java -Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); -Map 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); -assertEquals(7, result); -``` - -## 添加自定义函数与操作符 - -最简单的方式是通过 Java Lambda 表达式快速定义函数/操作符的逻辑: - -```java -Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); -// 自定义函数 -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); -assertEquals("1,2,3", resultFunction); - -// 自定义操作符 -express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); -Object resultOperator = express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); -assertEquals("1,2,3", resultOperator); -``` - -如果自定义函数的逻辑比较复杂,或者需要获得脚本的上下文信息,也可以通过继承 `CustomFunction` 的方式实现。 - -比如下面的 `hello` 自定义函数,根据租户不同,返回不同的欢迎信息: - -```java -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; - } -} -``` - -```java -Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); -express4Runner.addFunction("hello", new HelloFunction()); -String resultJack = (String) express4Runner.execute("hello()", Collections.emptyMap(), QLOptions.builder() - .attachments(Collections.singletonMap("tenant", "jack")).build()); -assertEquals("hello,jack", resultJack); -String resultLucy = (String) express4Runner.execute("hello()", Collections.emptyMap(), QLOptions.builder() - .attachments(Collections.singletonMap("tenant", "lucy")).build()); -assertEquals("hello,lucy", resultLucy); -``` - -## 高精度计算 - -QLExpress 内部会用 BigDecimal 表示所有无法用 double 精确表示数字,来尽可能地表示计算精度: - -> 0.1 在 double 中无法精确表示 - -```java -Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); -Object result = express4Runner.execute("0.1", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); -assertTrue(result instanceof BigDecimal); -``` - -通过这种方式能够解决一些计算精度问题: - -比如 0.1+0.2 因为精度问题,在 Java 中是不等于 0.3 的。 -而 QLExpress 能够自动识别出 0.1 和 0.2 无法用双精度精确表示,改成用 BigDecimal 表示,确保其结果等于0.3 - -```java -assertNotEquals(0.3, 0.1 + 0.2, 0.0); -assertTrue((Boolean) express4Runner.execute("0.3==0.1+0.2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS)); -``` - -除了默认的精度保证外,还提供了 `precise` 开关,打开后所有的计算都使用BigDecimal,防止外部传入的低精度数字导致的问题: - -```java -Map 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)); -// open precise switch -assertTrue((Boolean) express4Runner.execute("0.3==a+b", context, QLOptions.builder().precise(true).build())); -``` - -## 调用应用中的 Java 类 - -> 需要放开安全策略,不建议用于终端用户输入 - -假设应用中有如下的 Java 类(`com.alibaba.qlexpress4.QLImportTester`): - -```java -package com.alibaba.qlexpress4; - -public class QLImportTester { - - public static int add(int a, int b) { - return a + b; - } - -} -``` - -在 QLExpress 中有如下两种调用方式。 - - 1. 在脚本中使用 `import` 语句导入类并且使用 - -```java -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. -Object result = express4Runner.execute("import com.alibaba.qlexpress4.QLImportTester;" + - "QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); -Assert.assertEquals(3, result); -``` - - 2. 在创建 `Express4Runner` 时默认导入该类,此时脚本中就不需要额外的 `import` 语句 - -```java -Express4Runner express4Runner = new Express4Runner(InitOptions.builder() - .defaultImport( - 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); -Assert.assertEquals(3, result); -``` - -## 表达式缓存 - -通过 `cache` 选项可以开启表达式缓存,这样相同的表达式就不会重新编译,能够大大提升性能。 - -注意该缓存没有限制大小,只适合在表达式为有限数量的情况下使用: - -```java -Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); -// open cache switch -express4Runner.execute("1+2", new HashMap<>(), QLOptions.builder() - .cache(true).build()); -``` - -## 扩展函数 - -利用 QLExpress 提供的扩展函数能力,可以给Java类中添加额外的成员方法。 - -扩展函数是基于 QLExpress 运行时实现的,因此仅仅在 QLExpress 脚本中有效。 - -下面的示例代码给 String 类添加了一个 `hello()` 扩展函数: - -```java - 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); - assertEquals("Hello,jack", result); -``` - -## 对象,字段和函数别名 - -QLExpress 支持通过 `QLAlias` 注解给对象,字段或者函数定义一个或多个别名,方便非技术人员使用表达式定义规则。 - -下面的例子中,根据用户是否 vip 计算订单最终金额。 - -用户类定义: - -```java -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; - } -} -``` - -订单类定义: - -```java -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 脚本规则计算最终订单金额: - -```java - 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); - assertEquals(80, result.intValue()); -``` - -# 语法入门 - -QLExpress4 兼容 Java8 语法的同时,也提供了很多更加灵活宽松的语法模式,帮助用户更快捷地编写表达式。 - -基于表达式优先的语法设计,复杂的条件判断语句也可以直接当作表达式使用。 - -## 变量声明 - -同时支持静态类型和动态类型: - - - 变量声明时不写类型,则变量是动态类型,也同时是一个赋值表达式 - - 变量声明如果写类型,则是静态类型,此时是一个变量声明语句 - -```java -// Dynamic Typeing -a = 1; -// Static Typing -int b = 2; -``` - -## 方便语法元素 - -列表,映射等常用语法元素在 Java 里构造过于麻烦,因此在 QLExpress 中对其进行了简化 - -```qlexpress -// list -l = [1,2,3] -// map -m = { - "aa": 10, - "bb": { - "cc": "cc1", - "dd": "dd1" - } -} -``` - -列表的实际数据类型是 Java 中的 ArrayList,映射的底层数据类型是 LinkedHashMap - -## 数字 - -对于未声明类型的数字, -QLExpress会根据其所属范围自动从 int, long, BigInteger, BigDecimal 等数据类型中选择一个最合适的: - -```qlexpress -// true -2147483647 instanceof Integer -// true -9223372036854775807 instanceof Long -// true -18446744073709552000 instanceof BigInteger -``` - -因此在自定义函数或者操作符时,建议使用 Number 类型进行接收,因为数字类型是无法事先确定的。 - -## 动态字符串 - -QLExpress 支持 `${expression$}` 的格式在字符串中插入表达式,更方便地进行动态字符串组装: - -```qlexpress -a = 123; -// Output: hello,122 -"hello,${a-1$}" -``` - -```qlexpress -b = "test" -// Output: m xx YYY -"m xx ${ - if (b like 't%') { - "YYY" - } -$}" -``` - -## 分号 - -表达式语句可以省略结尾的分号,整个脚本的返回值就是最后一个表达式的计算结果。 - -以下脚本的返回值为 2: - -```qlexpress -a = 1 -b = 2 -// last express -1+1 -``` - -等价于以下写法: - -```qlexpress -a = 1 -b = 2 -// return statment -return 1+1; -``` - -## 表达式 - -QLExpress 采用表达式优先的设计,其中 除了 import, return 和循环等结构外,几乎都是表达式。 - -if 语句也是一个表达式,以下脚本的输出为 11: - -```java -if (11 == 11) { - 10 -} else { - 20 + 2 -} + 1; -``` - -try catch 结构也是一个表达式,以下脚本的输出为 12: - -```java -1 + try { - 100 + 1/0 -} catch(e) { - // Throw a zero-division exception - 11 -} -``` - -## 控制结构 - -### if 分支 - -以下脚本的输出为 true: - -```qlexpress -a = 11; -if (a >= 0 && a < 5) { - true -} else if (a >= 5 && a < 10) { - false -} else if (a >= 10 && a < 15) { - true -} -``` - -### while 循环 - -以下脚本的输出为 2: - -```qlexpress -i = 0; -while (i < 5) { - if (++i == 2) { - break; - } -} -i -``` - -### for 循环 - -以下脚本的输出为列表 `[3,4,5]`: - -```qlexpress -l = []; -for (int i = 3; i < 6; i++) { - l.add(i); -} -l -``` - -### for-each 循环 - -以下脚本的输出为 8: - -```qlexpress -sum = 0; -for (i: [0,1,2,3,4]) { - if (i == 2) { - continue; - } - sum += i; -} -sum -``` - -### try-catch - -以下脚本的输出为 11: - -```qlexpress -try { - 100 + 1/0 -} catch(e) { - // Throw a zero-division exception - 11 -} -``` - -### 函数定义 - -以下脚本的输出为 2: - -```qlexpress -function sub(a, b) { - return a-b; -} -sub(3,1) -``` - -### Lambda表达式 - -QLExpress4 中,Lambda 表达式作为一等公民,可以作为变量进行传递或者返回。 - -以下脚本的输出为 3: - -```qlexpress -add = (a, b) -> { - return a + b; -}; -add(1,2) -``` - -### 列表过滤和映射 - -支持通过 filter, map 方法直接对列表类型进行函数式过滤和映射。 -底层通过在列表类型添加 [扩展函数](#扩展函数) 实现,注意和 [Stream Api](#stream-api) 中同名方法区分。 -相比 Stream Api,它可以直接对列表进行操作,返回值也直接就是列表,更加方便。 - -```qlexpress -l = ["a-111", "a-222", "b-333", "c-888"] -// Output: ["111", "222"] -l.filter(i -> i.startsWith("a-")).map(i -> i.split("-")[1]); -``` - -### 兼容 Java8 语法 - -QLExpress 可以兼容 Java8 的常见语法。 - -比如 [for-each循环](#for-each-循环), Stream 语法, 函数式接口等等。 - -### Stream Api - -可以直接使用 Java 集合中的 stream api 对集合进行操作。 - -因为此时的 stream api 都是来自 Java 中的方法,参考 [调用应用中的 Java 类](#调用应用中的-java-类) 打开安全选项,以下脚本才能正常执行。 - -```qlexpress -l = ["a-111", "a-222", "b-333", "c-888"] - -// Output: ["111", "222"] -l.stream() - .filter(i -> i.startsWith("a-")) - .map(i -> i.split("-")[1]) - .collect(Collectors.toList()) -``` - -### 函数式接口 - -Java8 中引入了 Function, Consumer, Predicate 等函数式接口,QLExpress 中的 [Lambda表达式](#lambda表达式) 可以赋值给这些接口, -或者作为接收这些接口的方法参数: - -```qlexpress -Supplier s = () -> "test"; -// Output: test -s.get() -``` - -# 附录一 QLExpress4性能提升 - -[QLExpress4与3性能对比](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有接近10倍性能提升;有编译缓存,也有一倍性能提升。 - -# 附录二 开发者联系方式 - - - email:qinyuan.dqy@alibaba-inc.com,yumin.pym@taobao.com,704643716@qq.com - - wechat: - - xuanheng: dqy932087612 - - binggou: pymbupt - - linxiang: tkk33362 \ No newline at end of file diff --git a/pom.xml b/pom.xml index e9e9ac95..622a755b 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.alibaba QLExpress4 jar - 4.0.0-beta.1 + 4.0.0-beta.2 QLExpress QLExpress is a powerful, lightweight, dynamic language for the Java platform aimed at improving developers’ productivity in different business scenes. diff --git a/src/main/antlr4/QLParser.g4 b/src/main/antlr4/QLParser.g4 index 1ccf3661..abfa900f 100644 --- a/src/main/antlr4/QLParser.g4 +++ b/src/main/antlr4/QLParser.g4 @@ -329,7 +329,7 @@ doubleQuoteStringLiteral ; stringExpression - : {interpolationMode == SCRIPT}? DyStrExprStart expression StrExpr_END + : {interpolationMode == SCRIPT}? DyStrExprStart expression RBRACE | {interpolationMode == VARIABLE}? DyStrExprStart SelectorVariable_VANME RBRACE ; diff --git a/src/main/antlr4/QLexer.g4 b/src/main/antlr4/QLexer.g4 index 39a98264..108f9639 100644 --- a/src/main/antlr4/QLexer.g4 +++ b/src/main/antlr4/QLexer.g4 @@ -1051,8 +1051,6 @@ SelectorVariable_RBRACE: RBRACE -> popMode, type(RBRACE); mode StringExpression; -StrExpr_END: '$}' -> popMode; - StrExpr_FOR: FOR -> type(FOR); StrExpr_IF: IF -> type(IF); StrExpr_ELSE: ELSE -> type(ELSE); @@ -1094,8 +1092,8 @@ StrExpr_IntegerOrFloatingLiteral: IntegerOrFloatingLiteral -> type(IntegerOrFloa StrExpr_LPAREN: LPAREN -> type(LPAREN); StrExpr_RPAREN: RPAREN -> type(RPAREN); -StrExpr_LBRACE: LBRACE -> type(LBRACE); -StrExpr_RBRACE: RBRACE -> type(RBRACE); +StrExpr_LBRACE: LBRACE -> pushMode(StringExpression), type(LBRACE); +StrExpr_RBRACE: RBRACE -> popMode, type(RBRACE); StrExpr_LBRACK: LBRACK -> type(LBRACK); StrExpr_RBRACK: RBRACK -> type(RBRACK); diff --git a/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java b/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java index ebb012d9..eb9b75b0 100644 --- a/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java +++ b/src/test/java/com/alibaba/qlexpress4/Express4RunnerTest.java @@ -32,6 +32,16 @@ */ public class Express4RunnerTest { + @Test + public void cacheDocTest() { + // tag::cacheSwitch[] + Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); + // open cache switch + express4Runner.execute("1+2", new HashMap<>(), QLOptions.builder() + .cache(true).build()); + // end::cacheSwitch[] + } + @Test public void docQuickStartTest() { // tag::firstQl[] @@ -47,6 +57,7 @@ public void docQuickStartTest() { @Test public void docAddFunctionAndOperatorTest() { + // tag::addFunctionAndOperator[] Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); // custom function express4Runner.addVarArgsFunction("join", params -> @@ -58,10 +69,12 @@ public void docAddFunctionAndOperatorTest() { express4Runner.addOperatorBiFunction("join", (left, right) -> left + "," + right); Object resultOperator = express4Runner.execute("1 join 2 join 3", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); assertEquals("1,2,3", resultOperator); + // end::addFunctionAndOperator[] } @Test public void docImportJavaTest() { + // tag::importJavaCls[] Express4Runner express4Runner = new Express4Runner(InitOptions.builder() // open security strategy, which allows access to all Java classes within the application. .securityStrategy(QLSecurityStrategy.open()) @@ -71,10 +84,12 @@ public void docImportJavaTest() { Object result = express4Runner.execute("import com.alibaba.qlexpress4.QLImportTester;" + "QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); Assert.assertEquals(3, result); + // end::importJavaCls[] } @Test public void docDefaultImportJavaTest() { + // tag::defaultImport[] Express4Runner express4Runner = new Express4Runner(InitOptions.builder() .addDefaultImport( Collections.singletonList(ImportManager.importCls("com.alibaba.qlexpress4.QLImportTester")) @@ -84,6 +99,7 @@ public void docDefaultImportJavaTest() { ); Object result = express4Runner.execute("QLImportTester.add(1,2)", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); Assert.assertEquals(3, result); + // end::defaultImport[] } @Test @@ -100,17 +116,25 @@ public void docTryCatchTest() { @Test public void docPreciseTest() { + // tag::bigDecimalForPrecise[] Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); Object result = express4Runner.execute("0.1", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); assertTrue(result instanceof BigDecimal); + // end::bigDecimalForPrecise[] + + // tag::preciseComparisonWithJava[] assertNotEquals(0.3, 0.1 + 0.2, 0.0); assertTrue((Boolean) express4Runner.execute("0.3==0.1+0.2", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS)); + // end::preciseComparisonWithJava[] + // tag::preciseSwitch[] Map 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)); + // open precise switch assertTrue((Boolean) express4Runner.execute("0.3==a+b", context, QLOptions.builder().precise(true).build())); + // end::preciseSwitch[] } @@ -231,6 +255,7 @@ public void invalidOperatorTest() { @Test public void extensionFunctionTest() { + // tag::extensionFunction[] ExtensionFunction helloFunction = new ExtensionFunction() { @Override public Class[] getParameterTypes() { @@ -258,6 +283,7 @@ public Object invoke(Object obj, Object[] args) throws InvocationTargetException .build()); Object result = express4Runner.execute("'jack'.hello()", Collections.emptyMap(), QLOptions.DEFAULT_OPTIONS); assertEquals("Hello,jack", result); + // end::extensionFunction[] } @Test @@ -587,6 +613,7 @@ public void qlAliasTest() { @Test public void qlAliasDocTest() { + // tag::qlAlias[] Order order = new Order(); order.setOrderNum("OR123455"); order.setAmount(100); @@ -601,10 +628,12 @@ public void qlAliasDocTest() { Number result = (Number) express4Runner.executeWithAliasObjects("用户.是vip? 订单.金额 * 0.8 : 订单.金额", QLOptions.DEFAULT_OPTIONS, order, user); assertEquals(80, result.intValue()); + // end::qlAlias[] } @Test - public void customFunctionDocTest() { + public void customComplexFunctionDocTest() { + // tag::customComplexFunction[] Express4Runner express4Runner = new Express4Runner(InitOptions.DEFAULT_OPTIONS); express4Runner.addFunction("hello", new HelloFunction()); String resultJack = (String) express4Runner.execute("hello()", Collections.emptyMap(), QLOptions.builder() @@ -613,6 +642,7 @@ public void customFunctionDocTest() { String resultLucy = (String) express4Runner.execute("hello()", Collections.emptyMap(), QLOptions.builder() .attachments(Collections.singletonMap("tenant", "lucy")).build()); assertEquals("hello,lucy", resultLucy); + // end::customComplexFunction[] } private void assertResultEquals(Express4Runner express4Runner, String script, Object expect) { diff --git a/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java b/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java index c63384a9..d79d9a7b 100644 --- a/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java +++ b/src/test/java/com/alibaba/qlexpress4/TestSuiteRunner.java @@ -56,7 +56,7 @@ public void suiteTest() throws URISyntaxException, IOException { @Test public void featureDebug() throws URISyntaxException, IOException { - Path filePath = getTestSuiteRoot().resolve("java/stream/java_stream.ql"); + Path filePath = getTestSuiteRoot().resolve("independent/doc/dynamic_typing.ql"); handleFile(filePath, filePath.toString(), true); } diff --git a/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java b/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java index 94b3a57d..cf7824ab 100644 --- a/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java +++ b/src/test/java/com/alibaba/qlexpress4/aparser/SyntaxTreeFactoryTest.java @@ -431,7 +431,7 @@ public void selectorTest() { @Test public void doubleQuoteStringScriptTest() { - List instructs = getScriptInstructions("\"a ${v-1$}\"", InterpolationMode.SCRIPT); + List instructs = getScriptInstructions("\"a ${v-1}\"", InterpolationMode.SCRIPT); ConstInstruction i0 = (ConstInstruction) instructs.get(0); assertEquals("a ", i0.getConstObj()); LoadInstruction i1 = (LoadInstruction) instructs.get(1); @@ -446,7 +446,7 @@ public void doubleQuoteStringScriptTest() { @Test public void doubleQuoteStringScriptTest2() { - QLParser.ProgramContext programContext = SyntaxTreeFactory.buildTree("\"Hello ${a$} ccc\"", + QLParser.ProgramContext programContext = SyntaxTreeFactory.buildTree("\"Hello ${a} ccc\"", new MockOpM(), true, false, System.out::println, InterpolationMode.SCRIPT); } diff --git a/src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql b/src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql new file mode 100644 index 00000000..4ef808f3 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/convenient_syntax_elements.ql @@ -0,0 +1,16 @@ +// 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) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/dynamic_string.ql b/src/test/resources/testsuite/independent/doc/dynamic_string.ql new file mode 100644 index 00000000..b0119940 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/dynamic_string.ql @@ -0,0 +1,9 @@ +a = 123 +assert("hello,${a-1}" == "hello,122") + +b = "test" +assert("m xx ${ + if (b like 't%') { + 'YYY' + } +}" == "m xx YYY") \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/dynamic_typing.ql b/src/test/resources/testsuite/independent/doc/dynamic_typing.ql new file mode 100644 index 00000000..7458a82a --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/dynamic_typing.ql @@ -0,0 +1,8 @@ +// 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") + diff --git a/src/test/resources/testsuite/independent/doc/for.ql b/src/test/resources/testsuite/independent/doc/for.ql new file mode 100644 index 00000000..92404894 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/for.ql @@ -0,0 +1,5 @@ +l = []; +for (int i = 3; i < 6; i++) { + l.add(i); +} +assert(l==[3,4,5]) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/for_each.ql b/src/test/resources/testsuite/independent/doc/for_each.ql new file mode 100644 index 00000000..9065e5e6 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/for_each.ql @@ -0,0 +1,8 @@ +sum = 0; +for (i: [0,1,2,3,4]) { + if (i == 2) { + continue; + } + sum += i; +} +assert(sum==8) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/function.ql b/src/test/resources/testsuite/independent/doc/function.ql new file mode 100644 index 00000000..6744785e --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/function.ql @@ -0,0 +1,4 @@ +function sub(a, b) { + return a-b; +} +assert(sub(3,1)==2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/if.ql b/src/test/resources/testsuite/independent/doc/if.ql new file mode 100644 index 00000000..64543ac3 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/if.ql @@ -0,0 +1,8 @@ +a = 11; +assert(if (a >= 0 && a < 5) { + true +} else if (a >= 5 && a < 10) { + false +} else if (a >= 10 && a < 15) { + true +} == true) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/if_as_expr.ql b/src/test/resources/testsuite/independent/doc/if_as_expr.ql new file mode 100644 index 00000000..424dbcc0 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/if_as_expr.ql @@ -0,0 +1,5 @@ +assert(if (11 == 11) { + 10 +} else { + 20 + 2 +} + 1 == 11) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/lambda.ql b/src/test/resources/testsuite/independent/doc/lambda.ql new file mode 100644 index 00000000..07c162a9 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/lambda.ql @@ -0,0 +1,4 @@ +add = (a, b) -> { + return a + b; +} +assert(add(1,2)==3) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/list_map_filter.ql b/src/test/resources/testsuite/independent/doc/list_map_filter.ql new file mode 100644 index 00000000..c8d86a45 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/list_map_filter.ql @@ -0,0 +1,3 @@ +l = ["a-111", "a-222", "b-333", "c-888"] +assert(l.filter(i -> i.startsWith("a-")) + .map(i -> i.split("-")[1]) == ["111", "222"]) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/try_catch.ql b/src/test/resources/testsuite/independent/doc/try_catch.ql new file mode 100644 index 00000000..cb836db1 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/try_catch.ql @@ -0,0 +1,6 @@ +assert(try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 11) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql b/src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql new file mode 100644 index 00000000..4a9ccfa3 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/try_catch_as_expr.ql @@ -0,0 +1,6 @@ +assert(1 + try { + 100 + 1/0 +} catch(e) { + // Throw a zero-division exception + 11 +} == 12) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/doc/while.ql b/src/test/resources/testsuite/independent/doc/while.ql new file mode 100644 index 00000000..8b2f7327 --- /dev/null +++ b/src/test/resources/testsuite/independent/doc/while.ql @@ -0,0 +1,7 @@ +i = 0; +while (i < 5) { + if (++i == 2) { + break; + } +} +assert(i==2) \ No newline at end of file diff --git a/src/test/resources/testsuite/independent/string/interpolation.ql b/src/test/resources/testsuite/independent/string/interpolation.ql index 950f401f..acd4e127 100644 --- a/src/test/resources/testsuite/independent/string/interpolation.ql +++ b/src/test/resources/testsuite/independent/string/interpolation.ql @@ -1,37 +1,37 @@ a = 123; b = "test" -assert("Hello ${a$} ${b $} ccc" == "Hello 123 test ccc"); +assert("Hello ${a} ${b } ccc" == "Hello 123 test ccc"); // $ escape assert("Hello \${a bb cc" == 'Hello ${a bb cc') // selector variable assert(${a} == 123) -assert("${a-1$}" == "122") +assert("${a-1}" == "122") assert("m xx ${ if (b like 't%') { 'YYY' } -$}" == "m xx YYY") +}" == "m xx YYY") assert("m xx ${ if (b like 't%') { "YYY" } -$}" == "m xx YYY") +}" == "m xx YYY") // nest interpolation assert("m xx ${ if (b like 't%') { - "YY${b$}Y" + "YY${b}Y" } -$}" == "m xx YYtestY") +}" == "m xx YYtestY") assert("m xx ${ if (b like 'mm%') { 'YYY' } -$}" == "m xx null") +}" == "m xx null")