# dsl **Repository Path**: CoderReverse/dsl ## Basic Information - **Project Name**: dsl - **Description**: 动态脚本语言(DSL,Dynamic Script Language)解析框架 - **Primary Language**: Java - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 14 - **Created**: 2023-02-18 - **Last Updated**: 2023-02-18 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # DSL
## 简介 DSL的全称是动态脚本语言(Dynamic Script Language),它是对脚本语言的一种扩展。DSL使用`:`和参数名表示普通参数,使用`#`和参数名表示嵌入参数,并使用特殊字符`#[]`标记动态片段,当解析时,判断实际传入参数值是否为空(`null`)或不存在决定是否保留该动态片段,从而达到动态执行不同脚本目的。以此来避免程序员手动拼接繁杂的脚本,使得程序员能从繁杂的业务逻辑中解脱出来。此外,DSL脚本支持宏,来增强脚本的动态逻辑处理能力。由于具有很强的动态处理能力,目前DSL最成功的应用领域是动态结构化查询语言(DSQL)。 ## 参数 ### 普通参数 使用`:`和参数名表示普通参数。例如,:staffName。 ### 嵌入参数 使用`#`和参数名表示嵌入参数(例如,#staffName)。1.2.2版本开始支持嵌入参数,嵌入参数会被以字符串的形式嵌入到脚本中。**值得注意的是:如果在SQL脚本中使用嵌入参数,会有SQL注入风险,一定注意不要将前端传参直接作为嵌入参数使用,如果使用必需进行合法性检验**。 ### 动态参数 动态参数是指,根据具体情况确定是否在动态脚本中生效的参数,动态参数是动态片段的组成部分。动态参数既可以是普通参数,也可以嵌入参数。 ### 静态参数 静态参数是相对动态参数而言的,它永远会在动态脚本中生效。在动态片段之外使用的参数就是静态参数。静态参数既可以是普通参数,也可以嵌入参数。 ### 参数访问符 参数访问符包括两种,即`.`和`[]`, 使用`Map`传参时,优先获取键相等的值,只有键不存在时才会将键降级拆分一一访问对象,直到找到参数并返回,或未找到返回`null`。其中`.`用来访问对象的属性,例如`:staff.name`、`#staff.age`;`[]`用来访问数组、集合的元素,例如`:array[0]`、`#map[key]`。理论上,支持任意级嵌套使用,例如`:list[0][1].name`、`#map[key][1].staff.name`。1.2.2版本开始支持参数访问符。 ## 参数转换器 参数转换器用于对参数值进行转换,主要应用场景是统一获取用户输入参数后可能对应值的类型或者内容需要转换后才适合在动态脚本中使用。例如,从`ServletRequest`中获取的参数均为字符串类型(`java.lang.String`),有可能部分参数需要转换为数字类型(`java.lang.Number`)或者时间类型(`java.util.Date`)等。 ### ToNumberParamsConverter 将参数转换为数字 `java.lang.Number` 类型的转换器。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。 `formatter` | 格式模板 | 如 `#,###.00` 等。 ### ToDateParamsConverter 将参数转换为 `java.util.Date` 类型的转换器。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。 `formatter` | 格式模板 | 如 `yyyy-MM-dd HH:mm:ss.S` 等。 ### DateAddParamsConverter 时间(`java.util.Date`)参数加法运算转换器。 属性 | 含义 | 说明 ----------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。 `amount` | 时间量 | 整数,为负值时,相当于减法。 `unit` | 时间单位 | 可选值:`year/month/day/hour/minute/second/millisecond`,分别对应:年/月/日/时/分/秒/毫秒。 ### ToStringParamsConverter 将参数转换为 `java.lang.String` 类型的转换器。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。 `formatter` | 格式模板 | 当参数值为 `java.lang.Number`、`java.util.Date`、`java.util.Calendar` 的实例时,通过格式模板将对象格式化为字符串。如 `#,###.00`、`yyyy-MM-dd HH:mm:ss` 等 ### WrapStringParamsConverter 对 `java.lang.String` 类型的非 `null` 参数值进行包装的转换器。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。 `formatter` | 包装模板 | 使用占位符 `${value}` 表示参数值,可根据需要添加其他内容。 ### SplitParamsConverter 将类型为 java.lang.String 的非 null 参数值进行分割的转换器 属性 | 含义 | 说明 ---------|------------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。 `regex` | 分割正则表达式 | 如 “,” 等。 `limit` | 最大分割子字符串数 | 可缺省,指定时,必须为大于1的正整数。 ## 参数过滤器 ### BlankParamsFilter 空白字符串参数过滤器,作用是过滤掉值为空白字符串参数。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。默认为 `*`。 ### EqParamsFilter 等值参数过滤器,作用是过滤掉与指定的比较值相等的参数。比较之前会将两个值进行类型转换以确保类型一致,优先将比较值转换为参数值的类型。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。默认为 `*`。 `value ` | 供比较的值 | ### GtParamsFilter 大值参数过滤器,作用是过滤掉比指定的比较值大的参数。比较之前会将两个值进行类型转换以确保类型一致,优先将比较值转换为参数值的类型。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。默认为 `*`。 `value ` | 供比较的值 | ### GteParamsFilter 大于等于参数过滤器,作用是过滤掉与指定的比较值相等或者比指定的比较值大的参数。比较之前会将两个值进行类型转换以确保类型一致,优先将比较值转换为参数值的类型。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。默认为 `*`。 `value ` | 供比较的值 | ### LtParamsFilter 小值参数过滤器,作用是过滤掉比指定的比较值小的参数。比较之前会将两个值进行类型转换以确保类型一致,优先将比较值转换为参数值的类型。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。默认为 `*`。 `value ` | 供比较的值 | ### LteParamsFilter 小于等于参数过滤器,作用是过滤掉与指定的比较值相等或者比指定的比较值小的参数。比较之前会将两个值进行类型转换以确保类型一致,优先将比较值转换为参数值的类型。 属性 | 含义 | 说明 ------------|-------------|-------------------------------- `params` | 参数名表达式 | 可使用 `*` 作为通配符,多个参数表达式之间使用逗号分隔。默认为 `*`。 `value ` | 供比较的值 | ## 参数解析器 将动态脚本解析完成以后,在执行之前需要使用参数解析器将参数代入脚本中形成可执行脚本。目前已内置了两种参数解析器供选择。 ### JDBCParamsParser JDBC参数解析器。将脚本中的命名参数替换为 `?` ,并将参数以此放入 `ArrayList` 中。 ### PlaintextParamsParser 明文参数解析器抽象类。将脚本中的命名参数替换为参数值,其中字符串参数将在替换的参数值上添加单引号。由于不同场景下,时间类型参数的转换差异很大,因此这部分还需用户自行实现。 ## 动态片段 DSL使用特殊字符`#[]`标记动态片段,并连同动态参数一起构成动态片段,动态片段可以是任意脚本片段。 ### 例子 例如,可以对SQL脚本进行动态化解析。假设有一张员工信息表STAFF_INFO,表结构详见如下建表语句: ``` CREATE TABLE STAFF_INFO ( STAFF_ID VARCHAR(20) NOT NULL, /*员工编号*/ STAFF_NAME VARCHAR(30) DEFAULT NULL, /*员工姓名*/ DEPARTMENT_ID VARCHAR(10) DEFAULT NULL, /*部门编号*/ POSITION VARCHAR(30) DEFAULT NULL, /*所任职位*/ STATUS VARCHAR(20) DEFAULT 'IN_SERVICE',/*在职状态*/ PRIMARY KEY (`STAFF_ID`) ); ``` 通常,我们经常需要按员工编号或者按员工姓名查询员工信息。这就需要我们对查询条件进行排列组合,一共会存在 ```math 2^2=4 ``` 种可能的SQL。如果使用SQL拼接的技术实现,显然是比较低效的。如果查询条件数量更多,则拼接SQL会成为难以想象的难题。为此,我们必须有一种技术帮我们来完成这样的事情,动态片段应运而生。有了动态片段,我们对上述问题就能够轻松解决了。 ``` SELECT * FROM STAFF_INFO S WHERE 1=1 #[AND S.STAFF_ID = :staffId] #[AND S.STAFF_NAME LIKE :staffName] ``` 有了上述带动态片段的SQL,可以自动根据实际情况生成需要执行的SQL。例如: 1. 参数staffId为空(`null`),而staffName为非空(非`null`)时,实际执行的语句为: ``` SELECT * FROM STAFF_INFO S WHERE 1=1 AND S.STAFF_NAME LIKE :staffName ``` 2. 相反,参数staffName为空(`null`),而staffId为非空(非`null`)时,实际执行的语句为: ``` SELECT * FROM STAFF_INFO S WHERE 1=1 AND S.STAFF_ID = :staffId ``` 3. 或者,参数staffId、staffName均为空(`null`)时,实际执行的语句为: ``` SELECT * FROM STAFF_INFO S WHERE 1=1 ``` 4. 最后,参数staffId、staffName均为非空(非`null`)时,实际执行的语句为: ``` SELECT * FROM STAFF_INFO S WHERE 1=1 AND S.STAFF_ID = :staffId AND S.STAFF_NAME LIKE :staffName ``` ## 使用宏 宏是动态脚本语言(DSL)的重要组成部分,通过宏可以实现一些简单的逻辑处理。宏是基于Java内置的JavaScript引擎实现的,因此其语法是JavaScript语法,而不是Java。目前已实现的宏包括: ``` #[if(……)] ``` ``` #[if(……)] #[else] ``` ``` #[if(……)] #[elseif(……)] #[else] ``` 其中: ``` #[AND STAFF_ID = :staffId] ``` 等价于: ``` #[if(:staffId != null) AND STAFF_ID = :staffId] ``` 但遇到这种情况,极力推荐使用前者。因为前者代码更简洁,同时不需要运行JavaScript引擎而运行更快。 接上文,假如有如下表数据,以下再给两个运用宏的例子: 1. 部门编号为01的员工可以查看所有员工信息,其他员工仅可以查看自己所在部门的员工信息。假设当前员工所在部门参数为curDepartmentId,那么DSL可以这样编写: ``` SELECT * FROM STAFF_INFO S WHERE #[if(:curDepartmentId == '01') 1=1] #[else S.DEPARTMENT_ID = :curDepartmentId] #[AND S.STAFF_ID = :staffId] #[AND S.STAFF_NAME LIKE :staffName] ``` 2. 部门编号为01的员工可以查看所有员工信息,部门编号为02和03的员工可以查看本部门员工的信息,其他员工仅可以查看本部门跟自己职位一样的员工信息。假设当前员工职位参数为curPosition,所在部门参数为curDepartmentId,那么DSL可以这样编写: ``` SELECT * FROM STAFF_INFO S WHERE #[if(:curDepartmentId == '01') 1=1] #[elseif(:curDepartmentId == '02' || :curDepartmentId == '03') S.DEPARTMENT_ID = :curDepartmentId] #[else S.DEPARTMENT_ID = :curDepartmentId AND S.POSITION = :curPosition] #[AND S.STAFF_ID = :staffId] #[AND S.STAFF_NAME LIKE :staffName] ``` ## 扩展宏 可通过实现`cn.tenmg.dsl.Macro`接口来扩展宏。接口源码: ``` public interface Macro { /** * 执行宏并解析DSL动态片段。如果返回结果为{@code true},则DSL解析立即终止,并以当前宏解析DSL动态片段的结果为DSL解析的最终结果;否则,将当前宏解析的DSL片段结果拼接到DSL的主解析结果中,并继续后续解析。 * * @param context * DSL上下文 * @param attributes * 属性表。由当前层已运行的宏所存储,供本层后续执行的宏使用 * @param logic * 逻辑代码 * @param dslf * DSL动态片段 * @param params * 宏运行的参数 * @return 如果返回结果为{@code true},则DSL解析立即终止,并以当前宏解析DSL动态片段的结果为DSL解析的最终结果;否则,将当前宏解析的DSL片段结果拼接到DSL的主解析结果中,并继续后续解析。 */ boolean execute(DSLContext context, Map