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

返回本页常规视图.

后端源码扩展

低代码平台后端源码扩展,根据应用需求,可以在扩展工程中依赖第三方jar包、通过手写Java代码实现自定义微流程(服务定义、事件监听、定时任务)。

1 - 后端手写代码准备工作

在编写应用后端java源码之前,需要先做一些必要的准备工作,比如拉取某应用后端扩展工程源码、配置本地开发环境Maven setting.xml配置文件,修改扩展工程的pom文件,确定扩展工程应该提交到哪个分支才能生效等等。

找到当前环境低代码应用源码的仓库地址

我们都知道低代码平台制作的业务应用在发布之后,会自动生成前、后端源代码,生成好这些源代码之后会自动提交到当前环境对应的gitlab仓库中,我们的手写代码也不例外,需要提交到当前环境的gitlab仓库中。我们可以找到当前环境的运维管理员,登录Nacos查看当前环境Data Id为public的配置,其中seeyon.repo就是当前环境的仓库信息配置,例如:

20240826134900

20240826135110

  • seeyon.repo.gitlab.parentGroupPath为自动生成的前后端源码存放的分组;
  • seeyon.repo.gitlab.parentExtendGroupPath为后端扩展代码存放的分组;

如何找到自动生成的前后端源码

应用做了测试发布或者正式发布之后,我们可以从gitlab中将该应用的前、后端源码clone到本地。

找到前面seeyon.repo.gitlab.parentGroupPath配置的分组,打开该分组,通过应用的appName即可检索出应用前后端源码,例如:

20240826141135

找到应用后端扩展工程,创建对应版本号的分支

1.找到前面seeyon.repo.gitlab.parentExtendGroupPath配置的扩展代码分组,打开该分组,通过应用appName找到后端扩展源码工程,例如:

20240826141700

2.打开扩展工程,复制clone链接地址

20240826141932

3.通过步骤3得到的链接地址,将应用扩展工程clone到本地,根据当前应用发布的版本号前两位创建扩展工程分支名称,例如:

20240826142410

4.在扩展工程中创建相同名称的分支,例如:

20240826142739

后端扩展工程pom文件编写

后端扩展工程是个独立的工程,它和自动生成出来的源码工程的关系是通过pom文件进行定义的,我们在编写后端java源码之前,需要先将pom.xml编写好,以下是个示例,如果是其他应用,可以将其中的appName替换为您应用的appName即可,您的扩展工程如果需要依赖第三方jar包,也可以在该文件中用< dependency >标签进行依赖,此处需要特别注意的是< parent >标签中的定义的版本号,该版本号需要查看该应用对应的自动生成的源码中pom的版本号,示例:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.seeyon</groupId>
    <artifactId>edoc335172694483814428-ext</artifactId>
    <version>${parent.version}</version>
    <packaging>jar</packaging>
    <parent>
        <groupId>com.seeyon</groupId>
        <artifactId>edoc335172694483814428</artifactId>
        <version>3.6.39</version>
    </parent>

    <dependencies>
        <!-- 依赖自动生成的当前应用jar包 -->
        <dependency>
            <groupId>com.seeyon</groupId>
            <artifactId>edoc335172694483814428-biz</artifactId>
            <version>${parent.version}</version>
        </dependency>
        <dependency>
            <groupId>com.seeyon</groupId>
            <artifactId>edoc335172694483814428-web</artifactId>
            <version>${parent.version}</version>
        </dependency>
        <dependency>
            <groupId>com.seeyon</groupId>
            <artifactId>edoc335172694483814428-facade</artifactId>
            <version>${parent.version}</version>
        </dependency>
    </dependencies>
</project>

本地Maven setting文件配置

Maven 是一个基于项目对象模型 (POM) 的项目管理和构建工具,主要用于 Java 项目。它提供了一种标准化的方式来管理项目的构建、报告和文档。Maven 的核心功能包括依赖管理、构建自动化和项目生命周期管理,更多Maven使用请查阅其他资料。低代码平台创建的应用就是通过Maven进行管理的。

1.联系运维从Nacos配置中查找到当前环境对应的Maven仓库配置

20240826145338

2.根据配置修改本地Maven的setting配置文件

20240826150013

可以联系当前环境运维工程师配合给出当前Maven库的setting配置文件

2 - 自定义定时任务微流程

自定义定时任务微流程是标准定时任务微流程的扩展,它们可以实现定时执行某个业务逻辑的效果,只是这个定时任务的实现不是通过可视化配置的方式实现,是开发人员通过手写java代码来实现,当通使用标准定时任务微流程流程图的方式去实现比较困难的时候,可以采用自定义定时任务微流程。

步骤

  1. 按需新增一个自定义定时任务类型的微流程,微流程会为您自动生成一个编码,不过我们还是建议您按照语义自己定义编码,比如当前示例中的sendMsgPerMounth。

    image1

  2. 点击第一个节点,按需设置循环周期

    image2

  3. 保存该微流程为正式态(应用发布的时候只会为正式态的微流程生成代码)

  4. 测试发布该应用,等待应用测试发布成功

  5. 应用发布成功之后,按照后端手写代码准备工作中的步骤将扩展工程准备好

  6. 刷新本地maven仓库,确保已经将该应用最新版本的jar包更新到了本地,maven刷新成功之后可以看到该应用的{appName}-facade.jar中CustomMicroFlowAppService这个interface中已经有我们的sendMsgPerMounth方法,编写一个实现类,实现该接口之后提交到源码仓库重新发布该应用即可。

3 - 自定义服务定义微流程

自定义服务定义微流程是标准服务定义微流程的扩展,它们都是可以被其他前、后端微流程调用的服务接口,只是这个接口的实现不是通过可视化配置的方式实现,是开发人员通过手写java代码来实现,当通过画流程图的方式去实现标准服务定义微流程感觉比较困难的时候,可以采用自定义服务定义微流程。

步骤

  1. 按照需要,在规则-微流程菜单中新增一个自定义的服务定义微流程:
    image1
  2. 根据实际需要配置微流程参数
    image2
  3. 保存该微流程(注意需要保存为正式态)
    image3
  4. 测试发布应用,平台会在应用发布的时候为此微流程生成对应的Interface以及Interface中的方法,并且进行编译打包,上传maven仓库
    image4
  5. 按照后端手写代码准备工作说明的步骤,将该应用的扩展工程创建准备好
  6. 在扩展工程中新增包路径com.seeyon.{appName}.extend.appservice,在此包中新增微流程实现类,类名称自定,该新增的实现类实现微流程的Interface
package com.seeyon.renshiguanli4146632722591861173.extend.appservice;

import com.seeyon.boot.annotation.AppService;
import com.seeyon.boot.annotation.AppServiceOperation;
import com.seeyon.boot.transport.SingleRequest;
import com.seeyon.boot.transport.SingleResponse;
import com.seeyon.renshiguanli4146632722591861173.api.microflow.CustomMicroFlowAppService;
import com.seeyon.renshiguanli4146632722591861173.extend.service.ResumeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

/**
 * 微流程AppService
 **/
@AppService(value = "自定义人事管理接口")
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class CustomMicroFlowAppServiceImpl implements CustomMicroFlowAppService {
    
}
  1. 编写该微流程的方法实现,注意实现类的@AppService、@Transactional等注解一定要打上
package com.seeyon.renshiguanli4146632722591861173.extend.appservice;

import com.seeyon.boot.annotation.AppService;
import com.seeyon.boot.annotation.AppServiceOperation;
import com.seeyon.boot.transport.SingleRequest;
import com.seeyon.boot.transport.SingleResponse;
import com.seeyon.renshiguanli4146632722591861173.api.microflow.CustomMicroFlowAppService;
import com.seeyon.renshiguanli4146632722591861173.extend.service.ResumeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;

/**
 * 微流程AppService
 **/
@AppService(value = "自定义人事管理接口")
@Transactional(rollbackFor = Exception.class)
@Slf4j
public class CustomMicroFlowAppServiceImpl implements CustomMicroFlowAppService {

    @Autowired
    private ResumeService resumeService;

    /**
     * 简历解析
     *
     * @param request
     * @return
     */
    @Override
    @AppServiceOperation(value = "简历解析", returnValue = "简历解析")
    public SingleResponse<Void> parseResume(SingleRequest<String> request) {
        if (log.isDebugEnabled()) {
            log.debug("简历解析入参:{}", request.getData());
        }
        resumeService.parse(request.getData());
        return SingleResponse.ok();
    }


}
  1. 编写好之后确保本地没有任何编译错误即可将代码提交到代码仓库中,如果将有编译错误的代码提交到代码仓库中会导致您的应用发布失败

  2. 微流程实现代码提交之后,再次测试发布应用,即可将实现打包到应用中,其他的前/后端微流程都可以对此微流程进行调用

    image7

4 - 自定义事件监听微流程

自定义事件监听微流程是标准事件监听微流程的扩展,都是可以实现监听实体业务数据增、删、改事件的效果,只是这个业务的实现不是通过可视化配置的方式实现,是开发人员通过手写java代码来实现,当通过标准事件监听微流程,用微流程流程图的方式去实现一个事件监听微流程比较困难的时候,可以采用自定义事件监听微流程。

步骤

  1. 在点击规则-微流程菜单中的+号,新增一个自定义事件监听类型的微流程
    image1
  2. 点击第一个节点,为该事件监听类型微流程配置监听哪个应用的哪个实体的哪个事件
    image2
    本案例选择监听物资信息实体的新增前事件,平台为每个实体预制了9种事件,您可以按需进行选取,注意事项:监听xxx(操作成功)事件的行为动作和实体本身的新增、修改、删除的行为动作,在数据库事务层面,不是同一个数据库事务
    image3
  3. 设置好事件并且为改节点取名之后,点击微流程的保存,将该微流程保存为正式态
    image4
  4. 测试发布该应用
    image5
  5. 应用发布成功之后,按照后端手写代码准备工作中的步骤将扩展工程准备好
  6. 刷本地maven仓库,确保已经将该应用最新版本的jar包更新到了本地
  7. 如果还没有为CustomMicroFlowAppService添加实现类,则添加一个实现类(注意实现类的@AppService、@Transactional等注解一定要打上),如果已经有实现类实现了CustomMicroFlowAppService,则直接在你的实现类中@Override该方法,提交源码,然后重新发布应用即可
    image6

5 - 后端自定义函数扩展

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

5.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类型的返回值。

5.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>