这是本节的多页打印视图。 点击此处打印.

返回本页常规视图.

后端自定义函数扩展

用于介绍如何自定义后端表达式函数以及如何自定义校验规则

1 - 表达式自定义函数自定义语法校验规则

介绍如何自定义表达式后端函数的语法校验规则。

注:需要低代码平台版本2.8.0以上或表达式组件版本2.8.0以上支持

在表达式组件自定义函数时,某些场景下我们需要对函数定义特定的语法规则,比如,虽然函数的入参都是Long, 但是在V8平台中关联实体、枚举的实际类型都是Long ,如果我们要求入参只能是关联实体的Long , 那么就需要自定义语法规则限制参数的类型。

再比如,我们定义的函数的返回值类型可能是不固定的,比如绝对值函数,如果入参是整数,那么返回结果就是整数,如果入参是小数,返回结果则是小数,此时我们的静态函数的入参和返回值可能都是Object类型。自定义语法校验不仅可以校验参数的类型,还可以定义返回结果的类型。

1.代码示例

自定义校验示例代码:

@Slf4j
@FunctionSupporter(functions = {FunctionConstant.ABS_FUNCTION })
public class AbsFunctionSemanticAnalyzer implements FunctionSemanticAnalyzer {

    @Override
    public ExpressionDataType analyze(ASTNode astNode, SemanticAnalyzer semanticAnalyzer) {
        ASTNode paramNode = astNode.getChild(0);
        ExpressionDataType paramExpressionDataType = semanticAnalyzer.analyze(paramNode);
        DataType originDataType = paramExpressionDataType.getOriginDataType();

        if(DataType.INTEGER != originDataType && DataType.BIGINTEGER != originDataType &&
           DataType.DECIMAL != originDataType ){
            throw BusinessException.message("绝对值函数参数必须为数字类型!");
        }
        //注意:这里返回当前AST节点预期返回的类型,不是入参的类型
        return paramExpressionDataType;
    }


}

绝对值函数的自定义函数代码如下(自定义函数的规范参考《表达式组件自定义函数手册》 ):

@Function( category = FormulaFunctionCategoryEnum.Constants.MATH, displayName = ABS_DISPLAYNAME ,
            description = ABS_DESCRIPTION ,
            sort = 1 )
    @Parameters( value = {@Parameter(displayName = ABS_PARAM0_DISPLAYNAME, description = ABS_PARAM0_DESCRIPTION)})
    @ReturnValue( dataType = FormulaDataTypeEnum.Constants.DECIMAL, description = ABS_RESULT_DESCRIPTION )
    public static Object abs(Object number) {
        return number;
    }

2.定义规范

上述代码定义了绝对值函数abs(xxx) 的语法校验规则, 首页我们看到自定义函数abs的入参和返回值都是Object类型,这是为了同时兼容整数、长整数、小数这几种类型。在自定义语法校验逻辑中,我们定义一个类并实现FunctionSemanticAnalyzer接口 , 并在该类上打上**@FunctionSupporter注解, 其属性functions**表示该语法校验规则支持的函数,该属性为字符串数组 ,您可以将多个参数和返回值相似的函数定义为同一个语法校验规则,functions的值为自定义函数的方法名,比如这里的:abs 。

除了实现FunctionSemanticAnalyzer接口外,还需要将该类的全包名注册到项目工程的

META-INF/spring.factories 文件中, 这是因为自定义函数校验规则使用了spring-boot的SPI机制。如下所示:

com.seeyon.boot.starter.formula.parser.semantic.function.FunctionSemanticAnalyzer=\
com.seeyon.boot.starter.formula.parser.semantic.function.AbsFunctionSemanticAnalyzer

3.校验实现

我们可以看到FunctionSemanticAnalyzer接口有两个参数 , astNode 和 semanticAnalyzer 。

astNode: 表达式当前自定义函数的抽象语法树节点对象,您可以通过astNode.getLexeme().getValue()和astNode.getLexeme().getDesc() 获取到函数名称(abs)和函数的显示名称(取绝对值) 。

astNode.getChild(index)可以获取到函数对应下标的参数节点, 比如这里的abs函数只有一个参数,所以可以通过 astNode.getChild(0)的方式获取到参数节点,其也为一个ASTNode对象。

semanticAnalyzer: 如果需要分析参数的类型,可以通过semanticAnalyzer.analyze(paramNode)的方式,获取到参数的类型ExpressionDataType , 通过paramExpressionDataType.getOriginDataType()可以获取参数的实际类型。

analyze方法的返回值类型即为该自定义函数的实际返回类型,比如我们已经通过semanticAnalyzer.analyze分析到了abs函数的入参类型,这里绝对值函数的返回类型就应该是入参的类型,所以直接返回paramExpressionDataType。

此外,如果返回值的类型需要自行定义,可以使用ExpressionDataType的构造方法返回一个新的数据类型,示例如下:

@Override
public ExpressionDataType analyze(ASTNode astNode, SemanticAnalyzer semanticAnalyzer) {
    ASTNode paramNode = astNode.getChild(0);
    ExpressionDataType paramNodeDataType = semanticAnalyzer.analyze(paramNode);
    if(paramNodeDataType.isMultiSelect()){
        return ExpressionDataType.ofDataType(DataType.STRING);
    }else{
        return ExpressionDataType.ofDataType(DataType.BIGINTEGER);
    }
}
在上面的示例中,我们判断参数是否是多选(枚举、关联实体)类型,如果是多选我们返回了一个String类型的返回值,如果是单选我们返回了一个Long类型的返回值。

2 - 表达式组件自定义函数手册

介绍如何自定义一个后端表达式函数。

一.静态函数定义示例

@FunctionClass
public class UdcBasicFunction {

    @Function(category = FormulaFunctionCategoryEnum.Constants.TEXT, displayName = ENDSWITH_DISPLAYNAME,
            description = ENDSWITH_DESCRIPTION , sort = 3)
    @Parameters(value = {@Parameter(displayName = ENDSWITH_PARAM0_DISPLAYNAME, description = ENDSWITH_PARAM0_DESCRIPTION) ,
            @Parameter(displayName = ENDSWITH_PARAM1_DISPLAYNAME, description = ENDSWITH_PARAM1_DESCRIPTION)})
    @ReturnValue(dataType = FormulaDataTypeEnum.Constants.BOOLEAN, description = ENDSWITH_RESULT_DESCRIPTION)
    public static Boolean endsWith(String str, String suffix) {
        return StringUtils.endsWith(str, suffix);
    }

}

二、注解含义

@FunctionClass 表示一个自定义函数类,添加此注解的 class会被扫描和注册到表达引擎
@Function 表示一个自定义函数,被该注解标记的静态方法,将会注册到引擎,并可以在组件的函数列表被使用。注意:一定是静态方法
category 函数分类,如果需要将自定义函数加入到预制函数分类,请使FormulaFunctionCategoryEnum.Constants提供的分类常量,常量说明见下文中的函数分类列表
displayName 函数的显示名称: 比如描述一个函数名称 为"以文本结尾"
description 描述函数的用法,比如:判断文本是否以指定字符结尾,是则返回true,否则返回false
terminalSupport 函数支持的终端:ALL 前端后都支持的函数需要同时实现前端函数和后端函数 FRONTEND 仅前端函数支持,需要实现前端函数,后端仅定义函数即可BACKEND 仅后端函数支持,需要后端定义并实现函数,前端无需实现也无法执行``
sort 函数在表达式组件中分类下的排序,按从小到大的顺序
@Parameters 标注在静态方法上,描述自定义函数的入参,注解其value为一个数组属性,数组的数量和顺序一定要保持和静态方法的实际参数个数、顺序一致
value 参数数组
@Parameter 单个参数定义注解
displayName 参数名称,国际化规范见下文
description 参数描述
@ReturnValue 标注在静态方法上,对返回值进行描述
dataType 返回值的数据类型, 使用FormulaDataTypeEnum.Constants下的常量
description 返回值描述,国际化规范见下文

三、V8平台数据类型和java数据类型映射

在V8的体系中,函数定义、校验(见: 《表达式自定义函数自定义语法校验规则》) 以及字段的数据类型元数据描述都是使用: com.seeyon.boot.enums.DataType 这个枚举类。其枚举类型和java的数据类型有如下映射关系

V8数据类型 V8数据类型描述 java数据类型
ATTACHMENT 附件 java.lang.String
BIGINTEGER 长整数 java.lang.Long
BOOLEAN 布尔 java.lang.Boolean
CTPENUM 单选枚举(注意仅单选,多选对应String类型) java.lang.Long
CURRENCY 货币 java.math.BigDecimal
DATE 日期 java.util.Date
DATETIME 日期时间 java.util.Date
DECIMAL 小数 java.math.BigDecimal
ENTITY 单选实体(注意仅单选,多选对应String类型) java.lang.Long
INTEGER 整数 java.lang.Integer
MULTILINESTRING 多行文本 java.lang.String
STRING 文本 java.lang.String
TIME 时间 java.lang.String
OBJECT 对象 java.lang.Object

四、国际化规范

1.函数分类国际化规范

在定义函数的注解@Function中 , category属性用来描述函数的分类, 该属性是一个整数code , 所以需要自定义国际化词条。目前预置的函数分类提供了以下几种:

分类编码 分类名称
0 基础函数
1 系统函数(目前仅用于系统变量,函数列表中无法选择)
2 日期函数
3 数学函数
4 文本函数
5 聚合函数(已弃用)
6 条件函数
7 业务函数(已弃用)
8 聚合函数
9 变化函数
10 枚举函数

但是以编码可能不满用户自定义的函数分类,所以可以单独指定一个整数编码,并提取为自定义的常量(建议自定编码从100开始,避免和预制函数分类编码冲突)。同时需要在项目的 resources/i18n 目录下, 增加国际化词条, 规则如下: formula.function.category. + 自定义的函数分类编码。

formula.function.category.100=自定义函数分类

2.函数参数、返回值描述国际化规范

函数入参和返回值的国际化规范适用于 **@Function.displayName 、@Function. description、 @Parameter.displayName 、@Parameter.description、@ReturnValue.description **

比如, 静态函数定义示例中的以上属性可以抽取为国际化词条:

formula.function.endsWith.displayName=以指定文本结尾
formula.function.endsWith.description=判断一个文本是否已指定文本结果
formula.function.endsWith.param0.displayName=当前文本
formula.function.endsWith.param0.description=需要判断的文本
formula.function.endsWith.param1.displayName=结尾文本
formula.function.endsWith.param1.description=是否已该文本结尾
formula.function.endsWith.result.description=如果结尾文本结尾,则返回truy,否则返回false

词条同样需要放到 **resources/i18n 目录下 , **如果不定义国际化词条,也可以直接在注解中定义上述属性的值,表达式引擎会直接将该属性的值不经过国际化处理显示到页面。

3.函数用法描述

函数用法的定义主要是针对如图所示的场景,用于描述函数的一个具体使用案例

定义用法如下:

在手写工程的resources根目录下添加一个名为: formula-functions.xml 的文件

xml文件内容示例:

注意函数用法并没有做国际化处理,指定定义一套语言环境下的用法描述。后续版本会根据 formula-functions_en.xml 、formula-functions_zh_CN.xml 等文件后缀的方式区分国际化内容。

配置名称 含义
<function></function> 定义一个函数用法示例
<name>myFunction </name> 对应静态函数的方法名,注意不能重复
<args></args> 多个参数,没有参数可以不定义
<arg></arg> 定义单个参数的具体含义
<tips></tips> 整个函数在示例中的含义
<functions>
  <!-- 函数示例配置 -->
  <function>
    <name>round</name>
    <args>
      <arg>3.14159</arg>
      <arg>3</arg>
    </args>
    <tips>返回3.142</tips>
    <link>可以忽略</link>
  </function>
<functions>