后端:如何自定义表达式实现特殊需求?扩展内置函数表达式

平台提供了很多的表达式,如果这些表达式不满足场景?那我们应该如何新增表达式去满足项目的需求?
目前平台支持的表达式内置函数,参考


1. 扩展表达式的场景

注解@Validation的rule字段支持配置表达式校验
如果需要判断入参List类型字段中的某一个参数进行NULL校验,发现平台的内置函数不支持该场景的配置,这里就可以通过平台的机制,对内置函数进行扩展。

常见的一些代码场景,如下:

package pro.shushi.pamirs.demo.core.action;

……引用类

@Model.model(PetShopProxy.MODEL_MODEL)
@Component
public class PetShopProxyAction extends DataStatusBehavior<PetShopProxy> {

    @Override
    protected PetShopProxy fetchData(PetShopProxy data) {
        return data.queryById();
    }
    @Validation(ruleWithTips = {
            @Validation.Rule(value = "!IS_BLANK(data.code)", error = "编码为必填项"),
            @Validation.Rule(value = "LEN(data.name) < 128", error = "名称过长,不能超过128位"),
    })
    @Action(displayName = "启用")
    @Action.Advanced(invisible="!(activeRecord.code !== undefined && !IS_BLANK(activeRecord.code))")
    public PetShopProxy dataStatusEnable(PetShopProxy data){
        data = super.dataStatusEnable(data);
        data.updateById();
        return data;
    }

……其他代码

}

2. 新建一个自定义表达式的函数

校验入参如果是个集合对象的情况下,单个对象的某个字段如果为空,返回false的函数。

例子:
新建一个CustomCollectionFunctions类

package xxx.xxx.xxx;

import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.meta.annotation.Fun;
import pro.shushi.pamirs.meta.annotation.Function;
import pro.shushi.pamirs.meta.common.constants.NamespaceConstants;
import pro.shushi.pamirs.meta.util.FieldUtils;

import java.util.List;

import static pro.shushi.pamirs.meta.enmu.FunctionCategoryEnum.COLLECTION;
import static pro.shushi.pamirs.meta.enmu.FunctionLanguageEnum.JAVA;
import static pro.shushi.pamirs.meta.enmu.FunctionOpenEnum.LOCAL;
import static pro.shushi.pamirs.meta.enmu.FunctionSceneEnum.EXPRESSION;

/**
 * 自定义内置函数
 */
@Fun(NamespaceConstants.expression)
@Component
public class CustomCollectionFunctions {

    /**
     * LIST_FIELD_NULL 就是我们自定义的表达式,不能与已经存在的表达式重复!!!
     *
     * @param list
     * @param field
     * @return
     */
    @Function.Advanced(
            displayName = "校验集成的参数是否为null", language = JAVA,
            builtin = true, category = COLLECTION
    )
    @Function.fun("LIST_FIELD_NULL")
    @Function(name = "LIST_FIELD_NULL", scene = {EXPRESSION}, openLevel = LOCAL,
            summary = "函数示例: LIST_FIELD_NULL(list,field),函数说明: 传入一个对象集合,校验集合的字段是否为空"
    )
    public Boolean listFieldNull(List list, String field) {
        if (null == list) {
            return false;
        }
        if (CollectionUtils.isEmpty(list)) {
            return false;
        }
        for (Object data : list) {
            Object value = FieldUtils.getFieldValue(data, field);
            if (value == null) {
                return false;
            }
        }
        return true;
    }

}

3. 将自定义的表达式类,注册到平台的白名单

新建CustomFaasScriptAllowListApi类,@Order优先级要高于平台默认的优先级才会生效。

package xxx.xxx.xxx;

import org.apache.commons.collections4.SetUtils;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import pro.shushi.pamirs.framework.faas.fun.builtin.*;
import pro.shushi.pamirs.framework.faas.spi.api.guard.FaasScriptAllowListApi;
import pro.shushi.pamirs.meta.common.constants.NamespaceConstants;
import pro.shushi.pamirs.meta.common.spi.SPI;

import java.util.Set;

/**
 * 自定义:支持表达式调用的函数白名单与黑名单SPI实现
 */
@Order(1) //此处把自定义的类优先级调高
@Component
@SPI.Service
public class CustomFaasScriptAllowListApi implements FaasScriptAllowListApi {

    //白名单
    public static final Set<String> DEFAULT_SET = SetUtils.hashSet(

            //白名单,直接复制默认实现,pro.shushi.pamirs.framework.faas.spi.service.DefaultFaasScriptAllowListApi
            CollectionFunctions.class.getName(),
            ContextFunctions.class.getName(),
            DateFunctions.class.getName(),
            LogicFunctions.class.getName(),
            MapFunctions.class.getName(),
            MathFunctions.class.getName(),
            ObjectFunctions.class.getName(),
            RegexFunctions.class.getName(),
            TextFunctions.class.getName(),

            //下面添加自己的白名单类
            CustomCollectionFunctions.class.getName()

    );

    @Override
    public Set<String> classWhiteList() {
        return DEFAULT_SET;
    }

    @Override
    public Set<String> namespaceWhiteList() {
        return SetUtils.hashSet(NamespaceConstants.expression);
    }

}

4. 使用自定义的表达式

使用场景demo:

    /**
     * 注意点:自定义函数的 [field]字段是个文本,一定要加个引号代表参数是文本,不然无法解析到数据,其他场景类似
     *
     * @param data
     * @return
     */
    @Action.Advanced(name = FunctionConstants.create, managed = true)
    @Action(displayName = "确定", summary = "创建", bindingType = ViewTypeEnum.FORM)
    @Function(name = FunctionConstants.create)
    @Function.fun(FunctionConstants.create)
    @Validation(ruleWithTips = {
            @Validation.Rule(value = "LIST_FIELD_NULL(data.itemAttributes,'itemId')", error = "字段不能为空"),
    })
    public DemoItem create(DemoItem data) {
        return demoItemService.create(data);
    }

结语:可以通过平台的机制,去沉淀一套满足自己场景需求的表达式。

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

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

Like (0)
oinone's avataroinone
Previous 2024年5月30日 pm4:26
Next 2024年5月30日 pm10:03

相关推荐

  • action 和 function 有什么区别

    在 Oinone(开源低代码 / 企业应用开发平台) 里,Action 和 Function 都是“可被调用的逻辑单元”,但它们的定位和使用场景不同。可以简单理解为: Function = 纯逻辑函数(偏后端能力) Action = 面向业务操作的动作(偏应用行为 / UI触发) 下面给你详细对比一下。 1️⃣ Function:函数(逻辑能力) Function 更像是一个 可复用的服务方法。 特点 通常是 纯逻辑处理 不直接绑定 UI 可以被 Action / Service / 其他 Function 调用 用来封装 业务计算或工具逻辑 常见用途 比如: 价格计算 数据校验 数据转换 调用第三方 API 复杂业务规则 示例 @Function(openLevel = FunctionOpenEnum.API) @Function.Advanced(type = FunctionTypeEnum.QUERY) public TradeOrder computePrice(TradeOrder data) { return data; } 用途: 订单金额计算逻辑 然后可能被多个地方调用: Action -> 调用 Function Service -> 调用 Function Workflow -> 调用 Function 📌 核心:可复用业务逻辑 2️⃣ Action:动作(业务操作) Action 是一个 业务动作,通常是 用户触发的行为。 特点 通常绑定 UI 可以在 按钮 / 菜单 / API / 工作流 中触发 通常操作 模型数据 可以调用 Function 常见用途 例如: 创建订单 提交审批 发布文章 批量删除 导入数据 示例 @Action public void submitOrder(Order order){ order.setStatus("SUBMITTED"); } UI 可能是: 订单详情页 [提交订单] 按钮 点击按钮 → 调用 Action。 📌 核心:业务行为入口 3️⃣ 核心区别总结 维度 Action Function 定位 业务动作 逻辑函数 是否绑定 UI 通常是 否 是否直接给用户操作 是 否 是否可复用 一般 很高 是否操作模型 常见 不一定 调用关系 可调用 Function 不调用 Action 4️⃣ 调用关系(典型架构) 通常推荐的结构: UI按钮 ↓ Action(业务入口) ↓ Function(业务逻辑) ↓ DAO / Repository 例如: 提交订单按钮 ↓ submitOrderAction ↓ checkInventoryFunction calcPriceFunction createOrderFunction 这样: Action 只负责 流程 Function 负责 逻辑 代码会更清晰。 5️⃣…

    2026年3月12日
    55400
  • 动态视图支持权限配置

    动态页面在前端自定义之后,目前是没办法通过权限去控制路由页面的权限的,本篇文章将介绍如何解决这个问题。阅读本篇文章之前,应已经实现自定义页面渲染动态视图。参考文章:自定义视图内部渲染动态视图 实现思路: 通过自定义页面里设计的组件api名字,获取配置的路由页面名字。 解析路由页面包含的所有动作,拼接权限节点。 将路由页面的权限节点,拼到自定义视图所在菜单的权限管理上。 效果,路由页面的动作出现在自定义视图所在的菜单权限节点上 代码示例 package pro.shushi.pamirs.top.core.auth; import org.apache.commons.collections4.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import pro.shushi.pamirs.auth.api.entity.node.ActionPermissionNode; import pro.shushi.pamirs.auth.api.entity.node.PermissionNode; import pro.shushi.pamirs.auth.api.extend.load.PermissionNodeLoadExtendApi; import pro.shushi.pamirs.auth.api.loader.visitor.AuthCompileContext; import pro.shushi.pamirs.auth.api.loader.visitor.AuthCompileVisitor; import pro.shushi.pamirs.auth.api.loader.visitor.DslParser; import pro.shushi.pamirs.auth.api.pmodel.AuthResourceAuthorization; import pro.shushi.pamirs.auth.api.utils.AuthAuthorizationHelper; import pro.shushi.pamirs.boot.base.model.Menu; import pro.shushi.pamirs.boot.base.model.View; import pro.shushi.pamirs.boot.base.model.ViewAction; import pro.shushi.pamirs.boot.base.ux.model.UIView; import pro.shushi.pamirs.boot.base.ux.model.UIWidget; import pro.shushi.pamirs.boot.base.ux.model.view.UIField; import pro.shushi.pamirs.boot.web.loader.path.AccessResourceInfo; import pro.shushi.pamirs.boot.web.loader.path.ResourcePath; import pro.shushi.pamirs.boot.web.manager.MetaCacheManager; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.common.spi.SPI; import pro.shushi.pamirs.top.api.TopModule; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Optional; @Component @Order(88) @SPI.Service @Slf4j public class MyTestNodeLoadExtend implements PermissionNodeLoadExtendApi { @Autowired protected MetaCacheManager metaCacheManager; @Override public List<PermissionNode> buildNextPermissions(PermissionNode selected, List<PermissionNode> nodes) { // 动态页面所属的菜单name List<String> menuNames = new ArrayList<>(); menuNames.add("uiMenu3c094a75bd88461ba0ad780825069b32"); // 自定义组件api名称 List<String> apiNames = new ArrayList<>(); apiNames.add("dynamicView"); List<ActionPermissionNode> newNodes = new ArrayList<>(); for (String menuName : menuNames) { List<ActionPermissionNode> actionPermissionNodes = buildActionPermissionNodes(selected, menuName, apiNames); if (CollectionUtils.isNotEmpty(actionPermissionNodes)) { newNodes.addAll(actionPermissionNodes); } } nodes.addAll(newNodes); return nodes; } private List<ActionPermissionNode> buildActionPermissionNodes(PermissionNode selected, String menuName, List<String> apiNames) { String path = ResourcePath.generatorPath(TopModule.MODULE_MODULE, menuName); if (!path.equals(selected.getPath())) { return null; } Menu menu = metaCacheManager.fetchCloneMenus(TopModule.MODULE_MODULE).stream() .filter(v -> v.getName().equals(menuName)) .findFirst() .orElse(null); if (menu == null) { return null; } menu.fieldQuery(Menu::getViewAction); View mainView = fetchMainView(menu.getViewAction()); if (mainView == null) { return null; }…

    2025年9月10日
    1.1K00
  • 协同开发支持

    协同开发概述 在使用Oinone进行业务开发中,目前开发方式为: 开发各个本地启动项目 与 设计器环境共库共redis的方式进行。 在多个开发人员同时修改一个模型,或者没有及时更新其他同学提交的代码时,存在业务模型创建的数据表字段被删除的情况,协同开发模式正式为解决这个问题而生。 版本支持 4.7.x版本 已经包含分布式支持。 使用步骤 1、业务后端boot工程引入协同开发包 <dependency> <groupId>pro.shushi.pamirs.distribution</groupId> <artifactId>pamirs-distribution-session-cd</artifactId> </dependency> 2、yml文件配置ownSign pamirs: distribution: session: allMetaRefresh: false # ownSign根据实际情况修改,一般为研发各自名称 ownSign: wangxian 配置说明:allMetaRefresh,全量刷新Redis中的元数据,绝大多数情况下都不需要配置;1)第一次启动或者Redis的缓存被清空后,会自动进行全量。2)配置为true表示强制进行全量,一般都不需要配置;3)【推荐】默认增量的方式(即allMetaRefresh: false)写入redis的数据更少,相应的启动速度也更快4)【强制】ownSign是环境隔离的设置,同一个项目组不同的开发人员之间,ownSign配置成不同的(即各自配置成各自的,达到互不干扰) 3、业务系统DB和缓存的约束1)【强制】业务库和设计器Redis共用,包括Redis的前缀,租户和系统隔离键都需要一样(这三个值影响RedisKey的拼接)2)【强制】base库业务系统与设计器共用;3) 【强制】公共库即pamirs (资源-resource、用户-user、权限-auth、文件-file等)共用;4)【强制】「业务库」数据源的别名必须一直,每个开发人员必须配置到自己的本地 或者是远程库库加一个后缀区分; 4、开发同学在各自访问设计器时,URL最后面增加;ownSign=wangxian后回车,ownSign会被保存到浏览器缓存中,后续访问其他的URL访问不需要再次输入;如果需要去掉ownSign的值,则直接把界面上的悬浮窗删掉即可。说明:访问设计URL上增加的ownSign需要与开发各自本地项目yml文件中ownSign的值相同。(每个开发人员各自用各自的ownSign)PS:具体参数配置详见Oinone协同开发使用手册

    2023年12月4日
    2.0K00
  • 导入设计数据时dubbo超时导入失败

    问题描述 在本地启动导入设计数据的工程时,会出现dubbo调用超时导致设计数据无法完整导入的问题。 org.apache.dubbo.remoting.TimeoutException 产生原因 pom中的包依赖出现问题,导致没有使用正确的远程服务。 本地可能出现的异常报错堆栈信息如下: xception in thread "fixed-1-thread-10" PamirsException level: ERROR, code: 10100025, type: SYSTEM_ERROR, msg: 函数执行错误, extra:, extend: null at pro.shushi.pamirs.meta.common.exception.PamirsException$Builder.errThrow(PamirsException.java:190) at pro.shushi.pamirs.framework.faas.fun.manage.ManagementAspect.around(ManagementAspect.java:118) at sun.reflect.GeneratedMethodAccessor498.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644) at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633) at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:175) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) at pro.shushi.pamirs.framework.orm.DefaultWriteApi$$EnhancerBySpringCGLIB$$b4cea2b4.createOrUpdateBatchWithResult(<generated>) at pro.shushi.pamirs.meta.base.manager.data.OriginDataManager.createOrUpdateBatchWithResult(OriginDataManager.java:161) at pro.shushi.pamirs.meta.base.manager.data.OriginDataManager.createOrUpdateBatch(OriginDataManager.java:152) at pro.shushi.pamirs.ui.designer.service.installer.UiDesignerInstaller.lambda$install$0(UiDesignerInstaller.java:42) at pro.shushi.pamirs.core.common.function.AroundRunnable.run(AroundRunnable.java:26) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748) Caused by: org.apache.dubbo.rpc.RpcException: Failed to invoke the method createOrUpdateBatchWithResult in the service org.apache.dubbo.rpc.service.GenericService. Tried 1 times of the providers [192.168.0.123:20880] (1/1) from the registry 127.0.0.1:2181 on the consumer 192.168.0.123 using the dubbo version 2.7.22. Last error is: Invoke remote method timeout. method: $invoke, provider: dubbo://192.168.0.123:20880/ui.designer.UiDesignerViewLayout.oio.defaultWriteApi?anyhost=true&application=pamirs-demo&application.version=1.0.0&check=false&deprecated=false&dubbo=2.0.2&dynamic=true&generic=true&group=pamirs&interface=ui.designer.UiDesignerViewLayout.oio.defaultWriteApi&metadata-type=remote&methods=*&payload=104857600&pid=69748&qos.enable=false&register.ip=192.168.0.123&release=2.7.15&remote.application=pamirs-test&retries=0&serialization=pamirs&service.name=ServiceBean:pamirs/ui.designer.UiDesignerViewLayout.oio.defaultWriteApi:1.0.0&side=consumer&sticky=false&timeout=5000&timestamp=1701136088893&version=1.0.0, cause: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2023-11-28 10:23:05.835, end time: 2023-11-28 10:23:10.856, client elapsed: 695 ms, server elapsed: 4326 ms, timeout: 5000 ms, request: Request [id=0, version=2.0.2, twoway=true, event=false, broken=false, data=null], channel: /192.168.0.123:49449 -> /192.168.0.123:20880 at org.apache.dubbo.rpc.cluster.support.FailoverClusterInvoker.doInvoke(FailoverClusterInvoker.java:110) at org.apache.dubbo.rpc.cluster.support.AbstractClusterInvoker.invoke(AbstractClusterInvoker.java:265) at org.apache.dubbo.rpc.cluster.interceptor.ClusterInterceptor.intercept(ClusterInterceptor.java:47) at org.apache.dubbo.rpc.cluster.support.wrapper.AbstractCluster$InterceptorInvokerNode.invoke(AbstractCluster.java:92) at org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker.invoke(MockClusterInvoker.java:98) at org.apache.dubbo.registry.client.migration.MigrationInvoker.invoke(MigrationInvoker.java:170) at org.apache.dubbo.rpc.proxy.InvokerInvocationHandler.invoke(InvokerInvocationHandler.java:96) at org.apache.dubbo.common.bytecode.proxy0.$invoke(proxy0.java) at pro.shushi.pamirs.framework.faas.distribution.computer.RemoteComputer.compute(RemoteComputer.java:124) at pro.shushi.pamirs.framework.faas.FunEngine.run(FunEngine.java:80) at pro.shushi.pamirs.distribution.faas.remote.spi.service.RemoteFunctionHelper.run(RemoteFunctionHelper.java:68) at pro.shushi.pamirs.framework.faas.fun.manage.ManagementAspect.around(ManagementAspect.java:109) … 20 more Caused…

    2023年11月28日
    1.3K00
  • 【Excel导入/导出】多Sheet导入导出示例(4.7.x及以上版本)

    代码示例 示例仅供参考 点击下载代码示例 业务场景 准备工作:两个模型,物料(Material)和物料类别(MaterialCategory)。目标:在一个Excel模板中同时导入和导出两个模型的数据。 Material模型 @Model.model(Material.MODEL_MODEL) @Model.Advanced(unique = {"code"}) @Model(displayName = "物料", labelFields = {"name"}) public class Material extends IdModel { private static final long serialVersionUID = -2594216864389636135L; public static final String MODEL_MODEL = "maas.Material"; @Field.String @Field(displayName = "物料编码", required = true) private String code; @Field.String @Field(displayName = "物料名称", required = true) private String name; } MaterialCategory模型 @Model.model(MaterialCategory.MODEL_MODEL) @Model.Advanced(unique = {"code"}) @Model(displayName = "物料类别", labelFields = {"name"}) public class MaterialCategory extends IdModel { private static final long serialVersionUID = 6300896634558908349L; public static final String MODEL_MODEL = "maas.MaterialCategory"; @Field.String @Field(displayName = "类别编码", required = true) private String code; @Field.String @Field(displayName = "类别名称", required = true) private String name; } 模板定义 MaterialTemplate @Component public class MaterialTemplate implements ExcelTemplateInit { public static final String TEMPLATE_NAME = "materialTemplate"; @Override public List<ExcelWorkbookDefinition> generator() { WorkbookDefinitionBuilder builder = WorkbookDefinitionBuilder.newInstance(Material.MODEL_MODEL, TEMPLATE_NAME) .setDisplayName("物料和物料类别") .setEachImport(Boolean.FALSE);//设置importData的入参为 (ExcelImportContext importContext, List<MaterialCategory> data),如入参是单个对象,请删除setEachImport(Boolean.FALSE) createMaterialSheet(builder); createMaterialCategorySheet(builder); return Collections.singletonList(builder.build()); } private static void createMaterialSheet(WorkbookDefinitionBuilder builder) { builder.createSheet().setName("物料") .createBlock(Material.MODEL_MODEL, ExcelAnalysisTypeEnum.FIXED_HEADER, ExcelDirectionEnum.HORIZONTAL, "A1:B2") .createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle()).setIsConfig(Boolean.TRUE) .createCell().setField("code").setAutoSizeColumn(Boolean.TRUE).and() .createCell().setField("name").setAutoSizeColumn(Boolean.TRUE).and() .and() .createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle(v -> v.setBold(Boolean.TRUE)).setHorizontalAlignment(ExcelHorizontalAlignmentEnum.CENTER)) .createCell().setValue("物料编码").and() .createCell().setValue("物料名称"); } private static void createMaterialCategorySheet(WorkbookDefinitionBuilder builder) { builder.createSheet().setName("物料类别") .createBlock(MaterialCategory.MODEL_MODEL, ExcelAnalysisTypeEnum.FIXED_HEADER, ExcelDirectionEnum.HORIZONTAL, "A1:B2") .createHeader().setStyleBuilder(ExcelHelper.createDefaultStyle()).setIsConfig(Boolean.TRUE) .createCell().setField("code").setAutoSizeColumn(Boolean.TRUE).and()…

    2024年4月23日
    1.4K00

Leave a Reply

Please Login to Comment