Oinone远程调用链路源码分析

前提

源码分析版本是 5.1.x版本

概要

在服务启动时,获取注解REMOTE的函数,通过dubbo的泛化调用发布。在调用函数时,通过dubbo泛化调用获取结果。

注册服务者

  1. 在spring 启动方法installOrLoad中初始化
  2. 寻找定义REMOTE的方法
  3. 组装dubbo的服务配置
  4. 组装服务对象实现引用,内容如下,用于注册
    • 调用前置处理
      • 放信息到SessionApi
      • 函数调用链追踪,放到本地TransmittableThreadLocal
      • 从redis中获取到的数据进行反序列化并存在到本地的线程里
      • Trace信息,放一份在sessionApi中 和ThreadLocal
    • 调用函数执行
    • 返回数据转成特定格式
  5. 通过线程组调用dubbo的ServiceConfig.export 服务发布

时序图

Oinone远程调用链路源码分析
注册

源码分析

根据条件判断,确定向dubbo进行服务发布
RemoteServiceLoader

public void publishService(List<FunctionDefinition> functionList,Map<String,Runnable> isPublished) {
        // 因为泛化接口只能控制到namespace,控制粒度不能到fun级别,这里进行去重处理
        Map<String, Function> genericNamespaceMap = new HashMap<>();
        for (FunctionDefinition functionDefinition : functionList) {
            Function function = new Function(functionDefinition)

            try {
               //定义REMOTE, 才给予远程调用
                if (FunctionOpenEnum.REMOTE.in(function.getOpen()) && !ClassUtils.isInterface(function.getClazz())) {
                    genericNamespaceMap.putIfAbsent(RegistryUtils.getRegistryInterface(function), function);
                }
            } catch (PamirsException e) {
            }
        }
        // 发布远程服务
        for (String namespace : genericNamespaceMap.keySet()) {
            Function function = genericNamespaceMap.get(namespace);
            if(isPublished.get(RegistryUtils.getRegistryInterface(function)) == null){
                // 发布,注册远程函数服务,底层使用dubbo的泛化调用
                Runnable registryTask = () -> remoteRegistry.registryService(function);
                isPublished.put(RegistryUtils.getRegistryInterface(function),registryTask);
            }else{

            }
        }
    }

构造ServiceConfig方法,设置成泛化调用,进行发布export()
DefaultRemoteRegistryComponent

     public void registryGenericService(String interfaceName, List<MethodConfig> methods,
                                       String group, String version, Integer timeout, Integer retries) {
        ....
        try {
            ServiceConfig<GenericService> service = new ServiceConfig<>();
            // 服务接口名
            service.setInterface(interfaceName);
            // 服务对象实现引用
            service.setRef(genericService(interfaceName));
            if (null != methods) {
                service.setMethods(methods);
            }
            // 声明为泛化接口
            service.setGeneric(Boolean.TRUE.toString());
            // 基础元数据
            constructService(group, version, timeout, retries, service);
            service.export();
        } catch (Exception e) {
           .....
        }
    }

// 服务对象实现引用
private GenericService genericService(String interfaceName) {
        return (method, parameterTypes, args) -> {
            PamirsSession.clear();
            Function function = Objects.requireNonNull(PamirsSession.getContext()).getFunction(RegistryUtils.getFunctionNamespace(method), RegistryUtils.getFunctionFun(method));
            if (log.isDebugEnabled()) {
                log.debug("interfaceName: " + interfaceName + ", isDataManage: " + function.isDataManager());
            }
            try {
                //前置处理:服务提供者,对请求参数进行对象化拆解,并对请求携带的上下文进行处理
                // 放信息到SessionApi
                // 函数调用链追踪,放到本地TransmittableThreadLocal
                // CommonMetaDataCacheApi.computeMetaData() 从redis中获取到的数据进行反序列化并存在到本地的TL中
                // DataAuditApi.computeDataAuditSession() Trace信息,放一份在sessionApi中 和ThreadLocal中
                Object[] args1 = Spider.getDefaultExtension(RemoteRequestArgApi.class).providerHandle(function.getNamespace(), function.getFun(), args, function.getArguments());

                Object result = FunEngine.get().exclude(ScriptType.REMOTE).run(function, args1);

                //后置处理:服务提供者,对结果进行对象化封装、携带请求上下文进行处理
                return Spider.getDefaultExtension(RemoteResponseApi.class).providerHandle(function, method, result);
            } catch (Throwable e) {
                return Spider.getDefaultExtension(RemoteResponseApi.class).providerExceptionHandle(function, method, e);
            } finally {
                PamirsSession.clear();
            }
        };
    }

注册消费者

  1. 函数处理调用
  2. 注册服务消费者
    • 从ReferenceConfigCache获取泛化
  3. 调用dubbo泛化调用接口
  4. 获取返回信息
    • 获取用户id,放入PamirsSession
    • 如果开启debug模式
      • 存入DEBUG_THREAD_LOCAL本地线程
    • 返回格式
      • IWrapper
      • Pagination
      • Result

时序图

Oinone远程调用链路源码分析

源码分析

泛化调用dobbo接口,并解析返回对象
RemoteComputer

public Object compute(Function function, Object... args) {

        .....
        List<Arg> functionArguments = function.getArguments();
        String methodName = RegistryUtils.getGenericServiceMethodName(function);
        String[] argTypes = FunctionUtils.fetchArgTypes(functionArguments);
        //前置处理:服务消费者,对请求参数进行对象化封装、携带请求上下文进行处理
        Object[] arguments = getRemoteRequestApi().consumerHandle(function.getNamespace(),function.getFun(), args, functionArguments);
        // 泛化调用
        Object result = invoke(function, methodName, argTypes, arguments);
        .....
        //后置处理:服务消费者,对返回结果进行对象化拆解,并对结果携带的上下文进行处理
        // 数据转成IWrapper/Pagination/Result
        return getRemoteResponseApi().consumerHandle(function, result);
}

// 配置请求信息,通过$invoke 实际调用
private Object invoke(Function function, String methodName, String[] argTypes, Object[] arguments) {
        Object result;
        String configCdOwnSign = getSessionFillOwnSignApi().getConfigCdOwnSign();
        if (StringUtils.isBlank(configCdOwnSign)) {
.....
        } else {
            try {
            // 获取服务,由于是泛化调用,所以获取的一定是GenericService类型
                GenericService remoteClient = CommonApiFactory.getApi(RemoteRegistry.class).registryOriginConsumer(function);
         // 第一个参数是需要调用的方法名
         // 第二个参数是需要调用的方法的参数类型数组,为String数组,里面存入参数的全类名。
         // 第三个参数是需要调用的方法的参数数组,为Object数组,里面存入需要的参数
                result = remoteClient.$invoke(methodName, argTypes, arguments);
            } catch (RpcException e) {
                ....            }
        }
        return result;
    }

从缓存中获取泛化
DefaultRemoteRegistryComponent

    public GenericService registryGenericConsumer(String interfaceName, List<MethodConfig> methods,
                                                  String group, String version, Integer timeout, Integer retries) {
        ....
        // 创建服务引用配置
        ReferenceConfig<GenericService> reference = new ReferenceConfig<>();
        reference.setInterface(interfaceName);
        // 设置为泛化调用
        reference.setGeneric(Boolean.TRUE.toString());
        if (null != methods) {
            reference.setMethods(methods);
        }
        constructReference(group, version, timeout, retries, reference);
        return ReferenceConfigCache.getCache().get(reference);
    }

名词解释

泛化调用是指在调用方没有服务方提供的API(SDK)的情况下,对服务方进行调用,并且可以正常拿到调用结果
泛化调用(客户端泛化)
实现泛化实现(服务端泛化)

Oinone社区 作者:oinone原创文章,如若转载,请注明出处:https://doc.oinone.top/backend/17027.html

访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验

(0)
oinone的头像oinone
上一篇 2024年9月3日 pm12:53
下一篇 2024年9月5日 pm8:13

相关推荐

  • Maven Setting 配置详解

    常用标签概览 servers/server:配置私有仓库用户名和密码进行认证,以 id 进行关联。 mirrors/mirror:配置镜像仓库拉取时的地址源和额外配置。 profiles/profile:配置多个可能使用的镜像仓库。 activeProfiles/activeProfile:配置默认激活的 profile,以 id 进行关联。 Oinone 私有仓库配置 以下配置可以在使用 Oinone 私有仓库的同时,也可以正常使用 aliyun 镜像源。 <servers> <server> <id>shushi</id> <username>${username}</username> <password>${password}</password> </server> </servers> <mirrors> <mirror> <id>shushi</id> <mirrorOf>shushi</mirrorOf> <url>http://ss.nexus.ixtx.fun/repository/public</url> <!– 忽略 https 认证,maven 版本过高时需要配置 –> <blocked>false</blocked> </mirror> </mirrors> <profiles> <profile> <id>shushi</id> <repositories> <repository> <!– 对应 server.id –> <id>shushi</id> <url>http://ss.nexus.ixtx.fun/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <!– 对应 server.id –> <id>shushi</id> <url>http://ss.nexus.ixtx.fun/repository/snapshots</url> <releases> <enabled>false</enabled> </releases> <snapshots> <enabled>true</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> <profile> <id>aliyun</id> <repositories> <repository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> <releases> <enabled>true</enabled> </releases> <snapshots> <enabled>false</enabled> </snapshots> </pluginRepository> </pluginRepositories> </profile> </profiles> <activeProfiles> <!– 使用 shushi 私有仓库 –> <activeProfile>shushi</activeProfile> <!– 使用 aliyun 镜像仓库 –> <activeProfile>aliyun</activeProfile> </activeProfiles> 常见问题 使用 mvn 时无法拉取 Oinone 最新版镜像,提示找不到对应的包 原因:在 Oinone 开源后,oinone-pamirs 内核相关包都被部署到 maven 中央仓库,但由于其他镜像仓库的同步存在延时,在未正确同步的其他镜像源拉取时会出现找不到对应的包相关异常。 解决方案:检查 mirrors 中是否配置了 aliyun 镜像源,如果配置了,使用上述 Oinone 私有仓库配置重新配置后,再进行拉取。这一问题是由于 mirrors 配置不当,拦截了所有从 maven 中央仓库拉取的地址替换为了 aliyun 镜像源导致的。

    2025年11月10日
    46500
  • 技术精要:数据导出与固化实用指南

    数据被认为是企业发展和决策的重要资产。随着业务的不断发展和数据量的不断增加,企业通常需要将数据从不同的源头导出,并将其固化到产品中,以便进行进一步的分析、处理和利用。数据导出与固化的过程涉及到数据的提取、清洗、整合和存储,是确保数据长期有效性和可用性的关键步骤。 了解数据导出与固化的流程和方法对于企业具有重要意义。通过有效的数据导出和固化,企业可以更好地管理和利用数据资源,提升决策的准确性和效率,实现业务的持续发展和创新。本次讨论将重点探讨数据导出与固化的流程和关键步骤,帮助参与者深入了解如何将数据从导出到产品中,为企业数据管理和应用提供有力支持。 1. 数据导出与固化:将数据从导出到产品中的流程 1.1. pom依赖: <dependency> <groupId>pro.shushi.pamirs.metadata.manager</groupId> <artifactId>pamirs-metadata-manager</artifactId> </dependency> 1.2 将第⼆步下载后的⽂件放⼊项⽬中(注意⽂件放置的位置)。放置⼯程的resources 下⾯。例如: 1.3 项⽬启动过程中,将⽂件中的数据导⼊(通常放在core模型的init包下 ⾯)。⽰例代码: package pro.shushi.pamirs.sys.setting.enmu; import com.google.common.collect.Lists; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Component; import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCom mand; import pro.shushi.pamirs.boot.common.api.init.LifecycleCompleted AllInit; import pro.shushi.pamirs.boot.common.extend.MetaDataEditor; import pro.shushi.pamirs.core.common.InitializationUtil; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.api.dto.meta.Meta; import pro.shushi.pamirs.meta.domain.module.ModuleDefinition; import pro.shushi.pamirs.metadata.manager.core.helper.DesignerIn stallHelper; import pro.shushi.pamirs.metadata.manager.core.helper.WidgetInst allHelper; import java.util.List; import java.util.Map; @Slf4j @Component public class DemoAppMetaInstall implements MetaDataEditor, LifecycleCompletedAllInit { @Autowired private ApplicationContext applicationContext; @Override public void edit(AppLifecycleCommand command, Map<String, Meta> metaMap) { if (!doImport()) { return; } log.info("[设计器业务元数据导⼊]"); InitializationUtil bizInitializationUtil = InitializationUtil.get(metaMap, DemoModule.MODULE_MODULE/ ***改成⾃⼰的Module*/, DemoModule.MODULE_NAME/***改成⾃⼰的 Module*/); DesignerInstallHelper.mateInitialization(bizInitializatio nUtil, "install/meta.json"); log.info("[⾃定义组件元数据导⼊]"); // 写法1: 将组件元数据导⼊到⻚⾯设计器. 只有在安装设计器的 服务中执⾏才有效果 WidgetInstallHelper.mateInitialization(metaMap, "install/widget.json"); // 写法2: 与写法1相同效果 InitializationUtil uiInitializationUtil = InitializationUtil.get(metaMap, "ui_designer", "uiDesigner"); if (uiInitializationUtil != null) { DesignerInstallHelper.mateInitialization(uiInitialization Util, "install/widget.json"); } // 写法3: 业务⼯程和设计器分布式部署,且希望通过业务⼯程导⼊ ⾃定义组件元数据. 业务模块需要依赖⻚⾯设计器模块,然后指定业务模块导 ⼊ DesignerInstallHelper.mateInitialization(bizInitializatio nUtil, "install/widget.json"); } @Override public void process(AppLifecycleCommand command, Map<String, ModuleDefinition> runModuleMap) { if (!doImport()) { return; } log.info("[设计器业务数据导⼊]"); // ⽀持远程调⽤,但是执⾏的⽣命周期必须是 LifecycleCompletedAllInit或之后. 本地如果安装了设计器,则没有要 求 DesignerInstallHelper.bizInitialization("install/ meta.json"); log.info("[⾃定义组件业务数据导⼊]"); // 当开发环境和导⼊环境的⽂件服务不互通时, 可通过指定js和 css的⽂件压缩包,⾃动上传到导⼊环境,并替换导⼊组件数据中的⽂件url // WidgetInstallHelper.bizInitialization("install/ widget.json", "install/widget.zip"); WidgetInstallHelper.bizInitialization("install/ widget.json"); return; } private boolean doImport() { // ⾃定义导⼊判断. 避免⽤于设计的开发环境执⾏导⼊逻辑 String[] envs = applicationContext.getEnvironment().getActiveProfiles(); List<String> envList = Lists.newArrayList(envs); return…

    2024年2月27日
    2.1K00
  • Oinone设计器部署参数说明

    概述 Oinone提供两种设计器部署方式,合作伙伴可以自行选择适合自己的部署方式。 Docker配置参数 环境变量 ARG_ENV:指定spring.profiles.active(默认:dev) ARG_LIFECYCLE:指定-Plifecycle(默认:INSTALL) JVM_OPTIONS:jvm参数 PROGRAM_ARGS:程序参数 JVM_OPTIONS和PROGRAM_ARGS参数说明 java [JVM_OPTIONS?] -jar boot.jar [PROGRAM_ARGS?] 端口说明 PS:以下为目前设计器镜像的全部端口,不同类型镜像的端口由于内置服务不同,使用的端口数量不同,但端口号是完全一致的。 80:前端服务端口(设计器访问入口) 8091:后端服务端口 8093:后端EIP服务端口 20880:Dubbo端口 3306:内置MySQL端口 2181:内置Zookeeper端口 6379:内置Redis端口 9876/10991:内置RocketMQ端口 9999:内置本地OSS默认端口 挂载目录说明(挂载虚拟卷) /opt/pamirs为镜像的工作目录,所有挂载目录均在该目录下。 /opt/pamirs/ext:应用配置文件目录;包含application.yml、logback.xml、license.lic等配置文件 /opt/pamirs/nginx/vhost:Nginx配置文件目录 /opt/pamirs/logs:后端服务日志目录 /opt/mq/conf/broker.conf:RocketMQ的broker配置文件 /opt/pamirs/outlib:非设计器内置包的外部加载目录(外部库),可以添加任何jar包集成到设计器中。 /opt/pamirs/dist:前端服务目录 /opt/pamirs/static:前端静态文件目录;LOCAL类型的OSS上传和下载目录; docker run启动常用参数 -e:指定环境变量 -p:指定端口映射 -v:指定挂载目录(挂载虚拟卷) docker run [OPTIONS] IMAGE [COMMAND] [ARG…] docker compose启动常用配置 services: container: image: $IMAGE container_name: $CONTAINER_NAME restart: always # docker run -e environment: KEY1: VALUE1 KEY2: VALUE2 … # docker run -p ports: – $machinePort1:$containerPort1 – $machinePort2:$containerPort2 … # docker run -v volumes: – $machinePath1:$containerPath1 – $machinePath2:$containerPath2 … docker compose常用命令 # 使用docker-compose.yaml启动 docker compose up -d # 使用docker-compose.yaml停止并删除容器 docker compose down -v # 指定配置文件启动 docker compose -f config.yaml up -d # 指定配置文件停止并删除容器 docker compose -f config.yaml down -v JAR包方式启动 下载Oinone专属启动器 oinone-boot-starter.zip 启动命令变化 # 原命令 java -jar boot.jar # 变更后命令 boot-starter java -jar boot.jar PS:更多命令可查看后端无代码设计器Jar包启动方法

    2024年11月4日
    1.3K00
  • 【HighGo】后端部署使用HighGo数据库

    HighGo数据库配置 驱动配置 jdbc仓库 https://mvnrepository.com/artifact/com.highgo/HgdbJdbc Maven配置(6.0.1版本可用) <highgo.version>6.0.1.jre8</highgo.version> <dependency> <groupId>com.highgo</groupId> <artifactId>HgdbJdbc</artifactId> <version>${highgo.version}</version> </dependency> JDBC连接配置 pamirs: datasource: base: type: com.alibaba.druid.pool.DruidDataSource driverClassName: com.highgo.jdbc.Driver url: jdbc:highgo://127.0.0.1:5866/oio_base?currentSchema=base,utl_file username: xxxxxx password: xxxxxx initialSize: 5 maxActive: 200 minIdle: 5 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true asyncInit: true 连接url配置 官方文档 https://www.highgo.com/document/zh-cn/application/jdbc.html url格式 jdbc:highgo://ip:端口号/数据库名?currentSchema=schema1,schema2 在jdbc连接配置时,${database}和${schema}必须完整配置,不可缺省。 jdbc指定schema时可以在currentSchema后指定多个schema,中间用,分隔,第一个schema为业务库表存放的主schema。 highgo数据库6.0版本里每个数据库默认会带一个utl_file的schema,该模式与文件访问功能有关,需要带在jdbc的schema中,但不能放在第一个。 其他连接参数如需配置,可自行查阅相关资料进行调优。 方言配置 pamirs方言配置 pamirs: dialect: ds: base: type: HighGoDB version: 6 major-version: 6.0.1 biz_data: type: HighGoDB version: 6 major-version: 6.0.1 数据库版本 type version majorVersion 6.0.x HighGo 6 6.0.1 PS:由于方言开发环境为6.0.1版本,其他类似版本(6.0.x)原则上不会出现太大差异,如出现其他版本无法正常支持的,可在文档下方留言。 schedule方言配置 pamirs: event: enabled: true schedule: enabled: true dialect: type: HighGoDB version: 6 major-version: 6.0.1 其他配置 逻辑删除的值配置 pamirs: mapper: global: table-info: logic-delete-value: (EXTRACT(epoch FROM CURRENT_TIMESTAMP) * 1000000 + EXTRACT(MICROSECONDS FROM CURRENT_TIMESTAMP))::bigint Highgo数据库用户初始化及授权 — init oio_base user (user name can be modified by oneself) CREATE USER oio_base WITH PASSWORD 'Test@12345678'; — if using automatic database and schema creation, this is very important. ALTER USER oio_base CREATEDB; SELECT * FROM pg_roles; — if using highgo database, this authorization is required. GRANT CREATE ON DATABASE highgo TO oio_base;

    2025年7月10日
    35700
  • 如何自定义覆盖内置模块的页面

    1.首先通过sql查询找到我们需要的页面,从其中的template字段复制出原视图的配置 通过模型编码model在base_view查找需要修改的视图 select * from base_view where model='workflow.WorkflowUserTask' and is_deleted = 0; 2.将base_view的template内容复制到java的core工程的resources目录下新建一个xml文件,修改里面的动作名称 <view widget="WorkFlowImplement"> <template slot="actions"> <action name="$$internal_GotoListTableRouter" priority="1" model="workflow.WorkflowUserTask" tag="contextFreeAction"/> <action name="approveStaging" widget="FlowTaskCommonAction" invisible="!(activeRecord.allowStaging)" priority="2" label="新暂存" model="workflow.WorkflowUserTask" displayName="新暂存"/> <action name="workflow_agree" invisible="!(activeRecord.allowAgree && activeRecord.status == &apos;ACTIVE&apos;)" priority="3" label="新同意" model="workflow.WorkflowUserTask" load="fetchDetailReadOnly" displayName="新同意" goBack="true" validateForm="true" loadRootData="true"/> <action name="workflow_rejust" invisible="!(activeRecord.allowReject && activeRecord.status == &apos;ACTIVE&apos;)" priority="4" label="新拒绝" model="workflow.WorkflowUserTask" displayName="新拒绝" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_turnon" invisible="!(activeRecord.taskType == &apos;APPROVE&apos; && activeRecord.allowTransfer && activeRecord.status == &apos;ACTIVE&apos;)" priority="5" label="新转交" model="workflow.WorkflowUserTask" load="fetchDetailReadOnly" displayName="新转交" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_addsign" invisible="!(activeRecord.taskType == &apos;APPROVE&apos; && activeRecord.allowAddSign && activeRecord.status == &apos;ACTIVE&apos;)" priority="6" label="新加签" model="workflow.WorkflowUserTask" load="fetchDetailReadOnly" displayName="新加签" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_write_fallback" invisible="!(activeRecord.taskType == &apos;WRITE&apos; && activeRecord.allowFallback && activeRecord.status == &apos;ACTIVE&apos;)" priority="7" label="新回退" model="workflow.WorkflowUserTask" displayName="新回退" goBack="true" loadRootData="true" tag="contextFreeAction"/> <action name="workflow_sharing" priority="8" label="新分享" model="workflow.WorkflowUserTask" displayName="新分享" goBack="true" loadRootData="true" tag="contextFreeAction"/> </template> <template slot="fields"> <field name="remark" widget="TextArea" invisible="activeRecord.taskType != &apos;APPROVE&apos;" priority="8" model="workflow.WorkflowUserTask" data="remark" displayName="意见备注"/> </template> </view> 3.在生命周期的元数据编辑方法内覆盖视图 package pro.shushi.pamirs.demo.core.init; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import pro.shushi.pamirs.boot.common.api.command.AppLifecycleCommand; import pro.shushi.pamirs.boot.common.extend.MetaDataEditor; import pro.shushi.pamirs.core.common.InitializationUtil; import pro.shushi.pamirs.demo.api.DemoModule; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.api.dto.meta.Meta; import pro.shushi.pamirs.meta.enmu.ViewTypeEnum; import pro.shushi.pamirs.workflow.app.api.model.WorkflowUserTask; import java.util.Map; @Slf4j @Component @Order(Ordered.LOWEST_PRECEDENCE) public class DemoModuleMetaInstall implements MetaDataEditor { @Override public void edit(AppLifecycleCommand command,…

    2024年7月2日
    1.5K00

Leave a Reply

登录后才能评论