Oinone协同开发源码分析

前提

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

什么是协同开发模式

协同开发模式解决的是不同开发,在开发同一个模型时,不会相互影响,也不会影响到测试环境
详见:Oinone协同开发使用手册

协同开发原理

在协同模式下,本地开发的元数据,配置pamirs.data.distribution.session.ownSign参数后,元数据前缀加ownSign值,然后只存在redis缓存,不落库。其它环境无法直接访问到该数据。
测试环境,或其它环境访问,需要在url上加ownSign等于设置的,则读redis数据时,除了加载通用数据,也会合并ownSign前缀的redis数据,显示出来

注意事项

  1. 协同开发仅支持界面设计器,其他设计器均不支持
  2. 不支持权限配置
  3. 不支持工作流触发

版本支持

完整支持5.1.0及以上

功能详解

启动时操作

  1. 做元数据保护检查
  2. 配置ownSign,则key拼接为 ownSign + ':' + key
  3. 清除掉ownSign的redis缓存数据;非ownSign不用清理
  4. 计算差量数据
  5. 有差量数据,放入ownSign标识数据,并清理本地标识
  6. dubbo注册服务,group拼接group + ownSign 后进行注册

读取时操作

读本地

  1. 组装key: ownSign + ':' + key
  2. 本地缓存有数据,更新缓存本地数据,返回
  3. 本地没有数据,读redis,并插入本地缓存

读远程

dubbo注册消费者,group拼接group + ownSign 后进行泛化调用

元数据保护检查

开启数据保护模式,在启动参数里加
-PmetaProtected=pamirs

会在启动时,往redis里写入数据

private static final String META_PROTECTED_KEY = "pamirs:check:meta-protected";
private void writeMetaProtected(String metaProtected) {
    stringRedisTemplate.opsForValue().set(META_PROTECTED_KEY, metaProtected);
}

如果同时又设置 pamirs.data.distribution.session.ownSign
则会报错 在使用元数据保护模式下,不允许设置 [pamirs.distribution.session.ownSign]

处理逻辑如下

  1. 看redis是否启用保护标识的值
  2. 获取pamirs.distribution.session.ownSign配置
  3. 没有启动参数 且redis没有值,则retrun
  4. 如果有启动参数且配置了ownSign,报错 在使用元数据保护模式下,不允许设置 [pamirs.distribution.session.ownSign]
  5. 如果有启动参数且 redis没有值或启动参数设置 -P metaForceProtected,则写入redis
  6. 如果有启动参数, 且启动参数跟redis值不同,则报错[公共环境开启了元数据保护模式,本地开发环境需配置[pamirs.distribution.session.ownSign]]
  7. 如果没有启动参数且redis有值,但没有配置ownSign 报错[公共环境开启了元数据保护模式,本地开发环境需配置[pamirs.distribution.session.ownSign]]

核心代码如下
MetadataProtectedChecker

public void process(AppLifecycleCommand command, Set<String> runModules,
                        List<ModuleDefinition> installModules, List<ModuleDefinition> upgradeModules, List<ModuleDefinition> reloadModules) {
        String currentMetaProtected = stringRedisTemplate.opsForValue().get(META_PROTECTED_KEY);
        String metaProtected = getMetaProtected();
        boolean hasCurrentMetaProtected = StringUtils.isNotBlank(currentMetaProtected);
        boolean hasMetaProtected = StringUtils.isNotBlank(metaProtected);
        if (!hasCurrentMetaProtected && !hasMetaProtected) {
            return;
        }
        if (hasMetaProtected) {
            if (Spider.getDefaultExtension(SessionFillOwnSignApi.class).handleOwnSign()) {
            // 如果有启动参数且配置了ownSign
                throw new UnsupportedOperationException("在使用元数据保护模式下,不允许设置 [pamirs.distribution.session.ownSign]");
            }
            if (!hasCurrentMetaProtected || isForceProtected()) {
                writeMetaProtected(metaProtected);
            } else if (!metaProtected.equals(currentMetaProtected)) {
            // 如果有启动参数, 且启动参数跟redis值不同
                throw unsupportedLocalOperation();
            }
        } else {
            if (Spider.getDefaultExtension(SessionFillOwnSignApi.class).handleOwnSign()) {
                return;
            }
            // 没有启动参数且redis有值,但没有配置ownSign 报错
            throw unsupportedLocalOperation();
        }
    }

取ownSign方式

  1. 看header是否有ownSign这个标识
  2. header没有,则从配置里取,并放到header里
    ownSign的获取核心代码
    CdDistributionSessionFillOwnSignApi

    @Override
    public String getCdOwnSign() {
        String cdOwnSign = null;
        // 看header是否有ownSign这个标识
        Map<String, String> headers = PamirsSession.getRequestVariables().getHeaders();
        if (MapUtils.isNotEmpty(headers)) {
            cdOwnSign = headers.get(OWN_SIGN);
        }
        // 页面没有ownSign标识,则从配置里取,并放到header里
        if (StringUtils.isBlank(cdOwnSign)) {
            cdOwnSign = getConfigCdOwnSign();
            if (headers != null) {
                headers.put(OWN_SIGN, cdOwnSign);
            }
        }
        return cdOwnSign;
    }

清理redis缓存

ownSign的rediskey组装形式,会在原有key前加入ownSign值 + ":"
ownSign + ':' + key
CdDistributionSessionCacheApi

    SEPARATOR_COLON = ":";
    default String keyOwnSign(String key) {
        String cdOwnSign = SessionFillOwnSignApiHolder.get().getCdOwnSign();
        if (StringUtils.isNotBlank(cdOwnSign)) {
            key = cdOwnSign + SEPARATOR_COLON + key;
        }
        return key;
    }
  1. 如果配置ownSign设置,则进行清理工作
  2. 组装Key: ownSign + ':' + key
  3. 执行redis删除
    核心代码 CdModelL2Cache

    public void clear() {
    
        if (handleOwnSign()) {
            SessionRedisUtils.deletePattern(keyOwnSign(super.getModelDefPattern()));
            SessionRedisUtils.deletePattern(keyOwnSign(super.getModelNamePattern()));
        }
    }

数据写入缓存

以模型为例
插入缓存入口,updateMetaData

public <T extends MetaBaseModel> void updateMetaData(String model, List<T> dataList) {
        if (CollectionUtils.isEmpty(dataList)) {
            return;
        }

        // 初始化. Init要放在allMetaRefresh的判断前面
        this.init();
        if (allMetaRefresh) {
            return;
        }

        switch (model) {
            case ModuleDefinition.MODEL_MODEL:
            case UeModule.MODEL_MODEL:
                asyncExecution(model, dataList.size(), MetaUpgradeCheckApi.UPDATE_OPERATOR, () -> fillSessionModule((List<ModuleDefinition>) dataList));
                break;
            ....
        }
    }

每个model轮询处理
    private boolean fillSessionModel(List<ModelDefinition> dataList, Boolean needFunctions) {
        ....
        return reliableExecution(() -> {
            for (ModelDefinition modelDefinition : dataList) {
                doOneModel(modelDefinition, needFunctions);
            }
        }, "模型元数据[变更]");
    }

    private void doOneModel(ModelDefinition modelDefinition, Boolean needFunctions) {
        // 构造模型配置
        ModelConfig modelConfig = SessionsHelper.fetchModelConfig(modelDefinition);
        String modelModel = modelConfig.getModel();
        String modelUniqueKey = modelModel + ":" + Optional.ofNullable(modelDefinition.getModelFields()).map(List::size).orElse(0);
        try {
            if (ObjectRepeatHelper.isNotRepeat(DistributionDataSupplier.getModelRepeats(), modelUniqueKey)) {
            // 放入缓存
                modelCache.put(modelModel, modelConfig);
            }
        } catch (Exception e) {
            ....
        }

        // 全量情况下metaData.getStandAloneFunctionList()的Function不全;需要把模型的Function也进行缓存
        if (needFunctions) {
            fillSessionModelFunction(modelConfig.getFunctionList());
        }
    }

放入缓存
CdModelL2Cache

    private void cdPut(String key, ModelConfig value) {
        String modelDefCacheKey = cdModelDefCacheKey(key);
        String modelNameCacheKey = cdModelNameCacheKey(value.getName());
        byte[] bytes = MetadataSerializationHelper.serialize(value.getModelDefinition());
        SessionRedisUtils.set(modelDefCacheKey, bytes);
        SessionRedisUtils.set(modelNameCacheKey, bytes);
        //从模型设计器保存后,需要清除掉本地空的标识。后面再get的时候会从redis中获取
DistributionMetaDataLocalCache.removeModelDef(modelDefCacheKey);
        DistributionMetaDataLocalCache.removeModelDef(modelNameCacheKey);
    }

其它处理ownSign数据对象有:

CdFunctionByNameL2Cache
CdFunctionL2Cache
CdActionL2Cache
CdViewL2Cache
....

写入dubbo服务

写入dubbo链路看文章Oinone远程调用链路源码分析

  1. 在group加后缀 group = group + ownSign
  2. 调用dubbo泛化调用

核心代码DefaultRemoteRegistry

    // 泛化调用
    public void registryService(Function function) {
        String interfaceName = RegistryUtils.getRegistryInterface(function);
        String group = getGroup(function, true);
        String version = getVersion(function);
        Integer timeout = getTimeout(function);
        Integer retries = function.getRetries();
        if (log.isDebugEnabled()) {
            log.debug("register service. interfaceName: {}, group: {}, version: {}, timeout: {}, retries: {}", interfaceName, group, version, timeout, retries);
        }
        remoteRegistryComponent.registryGenericService(interfaceName, group, version, timeout, retries);
}

// group加前缀
private String getGroup(Function function, boolean usingOwnSign) {
        String group = function.getGroup();
...
        if (usingOwnSign) {
            String ownSign = Spider.getDefaultExtension(SessionFillOwnSignApi.class).getCdOwnSign();
            if (StringUtils.isNotBlank(ownSign)) {
                group = group + ownSign;
            }
        }
        return group;
    }

读缓存

  1. 如果配置ownSign参数,则走ownSign处理逻辑
  2. 组装key: cdOwnSign + SEPARATOR_COLON + key
  3. 本地有缓存,更新本地缓存,并返回
  4. 本地没有缓存,读取redis数据,后放入本地缓存,返回

CdModelL2Cache

public ModelConfig get(String key) {
        ModelConfig modelConfig;
        if (handleOwnSign()) {
            modelConfig = cdGet(key);
            if (modelConfig == null) {
                modelConfig = super.get(key);
            }
        } else {
            modelConfig = super.get(key);
        }
        return modelConfig;
    }

private ModelConfig cdGet(String key) {
        if (DistributionBaseKeyer.MODEL_MODEL_BASE_KEYS.contains(key)) {
            return CommonApiFactory.getApi(DistributionL1CacheProxy.class).getModelL1Cache().get(key);
        }
        String modelDefCacheKey = cdModelDefCacheKey(key);
        ModelDefinition modelDef = DistributionMetaDataLocalCache.getModelDef(modelDefCacheKey);
        // 本地有数据,返回,没有则放入本地缓存,再返回
        if (modelDef != null || DistributionMetaDataLocalCache.keyModelDef(modelDefCacheKey)) {
            return super.covertModelConfig(modelDef);
        }
        // 读取redis数据
        byte[] bytes = SessionRedisUtils.get(modelDefCacheKey);
        modelDef = KryoSerializer.deserialize(bytes);
        if (modelDef != null) {
            // 放入本地缓存DistributionMetaDataLocalCache.setModelDef(modelDefCacheKey, modelDef);
        }
        return super.covertModelConfig(modelDef);
    }

读dubbo服务

读取dubbo链路看文章Oinone远程调用链路源码分析
RemoteComputer.invoke()

  1. 在group加后缀 group = group + ownSign
  2. 调用dubbo泛化调用

核心代码DefaultRemoteRegistry

    public GenericService registryConsumer(Function function) {
        String interfaceName = RegistryUtils.getRegistryInterface(function);
        // 
        String group = getGroup(function, true);
        String version = getVersion(function);
        Integer timeout = getTimeout(function);
        Integer retries = function.getRetries();
        if (log.isDebugEnabled()) {
            log.debug("register consumer. interfaceName: {}, group: {}, version: {}, timeout: {}, retries: {}", interfaceName, group, version, timeout, retries);
        }
        return remoteRegistryComponent.registryGenericConsumer(interfaceName, group, version, timeout, retries);
    }

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

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

Like (0)
oinone's avataroinone
Previous 2024年9月11日 am9:57
Next 2024年9月12日 pm6:12

相关推荐

  • Dubbo配置详解

    概述 Dubbo是一款高性能、轻量级的开源Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 Oinone平台默认使用dubbo-v2.7.22版本,本文以该版本为例进行描述。 基本概念 Dubbo在注册provider/consumer时使用Netty作为RPC调用的核心服务,其具备客户端/服务端(C/S)的基本特性。即:provider作为服务端,consumer作为客户端。 客户端通过服务中心发现有服务可被调用时,将通过服务中心提供的服务端调用信息,连接服务端并发起请求,从而实现远程调用。 服务注册(绑定Host/Port) JAVA程序启动时,需要将provider的信息注册到服务中心,并在当前环境为Netty服务开启Host/Port监听,以实现服务注册功能。 在下文中,我们通过绑定Host/Port表示Netty服务的访问地址,通过注册Host/Port表示客户端的访问地址。 使用yaml配置绑定Host/Port PS:该配置可在多种环境中通用,改变部署方式无需修改此配置。 dubbo: protocol: name: dubbo # host: 0.0.0.0 port: -1 假设当前环境的可用IP为192.168.1.100 以上配置将使得Netty服务默认绑定在0.0.0.0:20880地址,服务注册地址为192.168.1.100:20880 客户端将通过192.168.1.100:20880调用服务端服务 若发生20880端口占用,则自动向后查找可用端口。如20881、20882等等 若当前可用端口为20881,则以上配置将使得Netty服务默认绑定在0.0.0.0:20881地址,服务注册地址为192.168.1.100:20881 使用环境变量配置注册Host/Port 当服务端被放置在容器环境中时,由于容器环境的特殊性,其内部的网络配置相对于宿主机而言是独立的。因此为保证客户端可以正常调用服务端,还需在容器中配置环境变量,以确保客户端可以通过指定的注册Host/Port进行访问。 以下示例为体现无法使用20880端口的情况,将宿主机可访问端口从20880改为20881。 DUBBO_IP_TO_REGISTRY=192.168.1.100 DUBBO_PORT_TO_REGISTRY=20881 假设当前宿主机环境的可用IP为192.168.1.100 以上配置将使得Netty服务默认绑定在0.0.0.0:20881地址,服务注册地址为192.168.1.100:20881 客户端将通过192.168.1.100:20881调用服务端服务 使用docker/docker-compose启动 需添加端口映射,将20881端口映射至宿主机20881端口。(此处容器内的端口发生变化,若需要了解具体原因,可参考题外话章节) docker-run IP=192.168.1.100 docker run -d –name designer-allinone-full \ -e DUBBO_IP_TO_REGISTRY=$IP \ -e DUBBO_PORT_TO_REGISTRY=20881 \ -p 20881:20881 \ docker-compose services: backend: container_name: designer-backend image: harbor.oinone.top/oinone/designer-backend-v5.0 restart: always environment: DUBBO_IP_TO_REGISTRY: 192.168.1.100 DUBBO_PORT_TO_REGISTRY: 20881 ports: – 20881:20881 # dubbo端口 使用kubernetes启动 工作负载(Deployment) kind: Deployment apiVersion: apps/v1 spec: replicas: 1 template: spec: containers: – name: designer-backend image: harbor.oinone.top/oinone/designer-backend-v5.0 ports: – name: dubbo containerPort: 20881 protocol: TCP env: – name: DUBBO_IP_TO_REGISTRY value: "192.168.1.100" – name: DUBBO_PORT_TO_REGISTRY value: "20881" 服务(Services) kind: Service apiVersion: v1 spec: type: NodePort ports: – name: dubbo protocol: TCP port: 20881 targetPort: dubbo nodePort: 20881 PS:此处的targetPort为对应Deployment#spec. template.spec.containers.ports.name配置的端口名称。若未配置,可使用20881直接指定对应容器的端口号。 使用kubernetes其他暴露服务方式 在Kubernetes中部署服务,有多种配置方式均可用暴露服务。上述配置仅用于通过Service/NodePort将20881端口暴露至宿主机,其他服务可用通过任意Kubernetes节点IP进行调用。 若其他服务也在Kubernetes中进行部署,则可以通过Service/Service方式进行调用。将DUBBO_IP_TO_REGISTRY配置为${serviceName}.${namespace}即可。 若其他服务无法直接访问Kubernetes的master服务,则可以通过Ingress/Service方式进行调用。将DUBBO_IP_TO_REGISTRY配置为Ingress可解析域名即可。 Dubbo调用链路图解 PS: Consumer的绑定Host/Port是其作为Provider使用的,下面所有图解仅演示单向的调用链路。 名词解释 Provider: 服务提供者(JVM) Physical Machine Provider: 服务提供者所在物理机 Provider Container: 服务提供者所在容器 Kubernetes Service: Kubernetes Service资源类型 Consumer: 服务消费者(JVM) Registration Center: 注册中心;可以是zookeeper、nacos等。 bind: 服务绑定Host/Port到指定ip:port。 registry: 服务注册;注册Host/Port到注册中心的信息。 discovery: 服务发现;注册Host/Port到消费者的信息。 invoke: 服务调用;消费者通过注册中心提供的提供者信息向提供者发起服务调用。 forward: 网络转发;通常在容器环境需要进行必要的网络转发,以使得服务调用可以到达服务提供者。 物理机/物理机调用链路 “` mermaidsequenceDiagram participant p as Provider<br>(bind 0.0.0.0:20880)participant m as Physical Machine Provider<br>(bind 192.168.1.100:20881)participant…

    2024年8月10日
    2.3K00
  • 如何选择适合的模型类型?

    介绍 通过Oinone 7天从入门到精通的模型的类型章节我们可以知道模型有抽象模型、存储模型、代理模型、传输模型这四种。但是在在定义模型的时候我们可能不知道该如何选择类型,下面结合业务场景为大家讲解几种模型的典型使用场景。 抽象模型 抽象模型往往是提供公共能力和字段的模型,它本身不会直接用于构建协议和基础设施(如表结构等)。 场景:猫、鸟都继承自动物这个抽象模型 package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; import pro.shushi.pamirs.meta.annotation.sys.Base; import pro.shushi.pamirs.meta.base.IdModel; import pro.shushi.pamirs.meta.enmu.ModelTypeEnum; @Base @Model.model(AbstractAnimal.MODEL_MODEL) @Model.Advanced(type = ModelTypeEnum.ABSTRACT) @Model(displayName = "动物") public abstract class AbstractAnimal extends IdModel { public static final String MODEL_MODEL = "demo.AbstractAnimal"; @Field.String @Field(displayName = "名称") private String name; @Field.String @Field(displayName = "颜色") private String color; } package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; @Model.model(Cat.MODEL_MODEL) @Model(displayName = "猫") public class Cat extends AbstractAnimal { private static final long serialVersionUID = -5104390780952634397L; public static final String MODEL_MODEL = "demo.Cat"; @Field.Integer @Field(displayName = "尾巴长度") private Integer tailLength; } package pro.shushi.pamirs.demo.api.model; import pro.shushi.pamirs.meta.annotation.Field; import pro.shushi.pamirs.meta.annotation.Model; @Model.model(Bird.MODEL_MODEL) @Model(displayName = "鸟") public class Bird extends AbstractAnimal { private static final long serialVersionUID = -5144390780952634397L; public static final String MODEL_MODEL = "demo.Bird"; @Field.Integer @Field(displayName = "翼展宽度") private Integer wingSpanWidth; } 存储模型 存储模型用于定义数据表结构和数据的增删改查(数据管理器)功能,是直接与连接器进行交互的数据容器。 场景:存储模型对应传统开发模式中的数据表,上面例子中的Cat和Birdd都属于传输模型,由于模型定义的注解@Model.Advanced(type = ModelTypeEnum.STORE)默认值就是存储模型,所以一般不用手动指定 代理模型 代理模型是用于代理存储模型的数据管理器能力,同时又可以扩展出非存储数据信息的交互功能的模型。 场景一:隔离数据权限 场景二:增强列表的搜索项 场景三:导入导出的时候增加其他特殊信息 场景四:重写下拉组件的查询逻辑做数据过滤 传输模型 传输模型不会在数据库生成的表,只是作为数据的传输使用,跟传统开发模式中的DTO有一点相似。 场景一:批量处理数据 场景二:处理一些跟数据表无关的操作,如:清理指定业务的缓存、查看一些系统监控信息,可以根据业务信息建立对应的传输模型,在传输模型上创建action动作 场景三:通过传输模型完成复杂页面数据传输

    2024年4月7日
    1.7K00
  • 元数据表介绍

    模型 模型元数据的讲解 https://doc.oinone.top/oio4/9281.html base_model 模型表 字段名 备注 示例 system_source BASE是系统创建, MANUAL是人工创建 MANUAL pk 主键 id module 模块编码 demo_core model 模型编码 demo.PetType name api名称 petType lname 模型代码名称 pro.shushi.pamirs.demo.api.model.PetType table 逻辑数据表名称 demo_core_pet_type ds_key 逻辑数据源名 pamirs type 模型类型 store display_name 显示名称 品种 data_manager 是否允许系统根据模型变化自动创建表和更新表 1 ordering 排序 createDate DESC, id DESC super_models 父模型 demo.AbstractDemoIdModel uniques 唯一索引 indexes 索引 name,createDate 模块 模块元数据的讲解 https://doc.oinone.top/oio4/9279.html base_module 模块表 字段名 备注 示例 display_name 显示名称 OinoneDemo name api名称 DemoCore module 模块编码 demo_core module_dependencies 依赖模块编码列表 base,common,file,trigger module_exclusions 互斥模块编码列表 module_upstreams 上游模块编码列表 system_source BASE是系统创建, MANUAL是人工创建 MANUAL web web应用 1 default_home_page_model 默认主页模型编码 函数 函数元数据的讲解 https://doc.oinone.top/oio4/9282.html base_function 函数表 字段名 备注 示例 display_name 显示名称 根据条件分页查询记录列表和总数 clazz 函数位置 pro.shushi.pamirs.framework.orm.DefaultReadApi module 模块 demo_core method 函数方法 queryPage namespace 函数命名空间 demo.PetType argument_list 函数参数 [{"ltype":"pro.shushi.pamirs.meta.api.dto.condition.Pagination","model":"base.Pagination","modelGeneric":false,"multi":false,"name":"page","ttype":"m2o"},{"ltype":"pro.shushi.pamirs.meta.api.dto.wrapper.IWrapper","ltypeT":"java.lang.Object","model":"base.Condition","modelGeneric":true,"multi":false,"name":"queryWrapper","ttype":"m2o"}] fun 函数编码 queryPage return_type 返回值类型 {"ltype":"pro.shushi.pamirs.meta.api.dto.condition.Pagination","model":"base.Pagination","modelGeneric":false,"multi":false,"ttype":"m2o"} sys 由系统产生的元数据 1 type 函数类型 1: CREATE, 2: DELETE, 4: UPDATE, 8: QUERY 8 data_manager 数据管理器函数 1 codes 代码内容 open_level 开放级别 2: LOCAL, 4: REMOTE, 8: API, 6: LOCAL+REMOTE, 10: LOCAL+API, 12: REMOTE+API, 14:LOCAL+REMOTE+API 14 模型字段 字段讲解 https://doc.oinone.top/oio4/9239.html base_field 字段表 字段名 备注 示例 system_source BASE是系统创建, MANUAL是人工创建 MANUAL name api名称 name field 字段编码 name ttype 关系类型, 类型:m2o/o2m/m2m/enum/string/integer/map/datetime/related/money/html string model 模型编码…

    2024年8月23日
    1.2K00
  • 如何扩展自有的文件存储系统

    介绍 数式Oinone默认提供了阿里云、腾讯云、华为云、又拍云、Minio和本地文件存储这几种文件存储系统,如果我们有其他的文件存储系统需要对接,或者是扩展现有的文件系统,可以通过SPI继承AbstractFileClient注册新的文件存储系统。 代码示例 这里以扩展自有的本地文件系统为例 继承了内置的本地文件存储LocalFileClient,将其中上传文件的方法重写 package pro.shushi.pamirs.demo.core.file; import org.springframework.stereotype.Component; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.support.StandardMultipartHttpServletRequest; import pro.shushi.pamirs.framework.connectors.cdn.client.LocalFileClient; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.common.spi.SPI; import javax.servlet.http.HttpServletRequest; @Slf4j @Component // 注册新的文件存储系统类型 @SPI.Service(DemoLocalFileClient.TYPE) @RestController @RequestMapping("/demo_file") public class DemoLocalFileClient extends LocalFileClient { public static final String TYPE = "DEMO_LOCAL"; @Override public CdnFileForm getFormData(String fileName) { CdnConfig cdnConfig = getCdnConfig(); CdnFileForm fileForm = new CdnFileForm(); String uniqueFileName = Spider.getDefaultExtension(CdnFileNameApi.class).getNewFilename(fileName); String fileKey = getFileKey(cdnConfig.getMainDir(), uniqueFileName); //前端获取uploadUrl,上传文件到该地址 fileForm.setUploadUrl(cdnConfig.getUploadUrl() + "/demo_file/upload"); //上传后,前端将downloadUrl返回给后端 fileForm.setDownloadUrl(getDownloadUrl(fileKey)); fileForm.setFileName(uniqueFileName); Map<String, Object> formDataJson = new HashMap<>(); formDataJson.put("uniqueFileName", uniqueFileName); formDataJson.put("key", fileKey); fileForm.setFormDataJson(JSON.toJSONString(formDataJson)); return fileForm; } @ResponseBody @RequestMapping(value = "/upload", produces = "multipart/form-data;charset=UTF-8",method = RequestMethod.POST) public String uploadFileToLocal(HttpServletRequest request) { MultipartFile file = ((StandardMultipartHttpServletRequest) request).getFile("file"); // 例如可以根据file文件类型判断哪些文件是否可以上传 return super.uploadFileToLocal(request); } } 在application.yml内配置 cdn: oss: name: 本地文件系统 # 这里的type与代码中定义的文件存储系统类型对应 type: DEMO_LOCAL bucket: pamirs uploadUrl: http://127.0.0.1:8190 downloadUrl: http://127.0.0.1:6800 validTime: 3600000 timeout: 600000 active: true referer: localFolderUrl: /Users/demo/workspace/static

    2024年10月24日
    93000
  • 模型字段之序列化方式

    本文核心是带大家全面了解oinone的序列方式,包括支持的序列化类型、注意点、如果新增客户化序列化方式以及字段默认值的反序列化。 字段序列化方式说明 序列化方式 说明 备注 JSON JSON序列化 主要用于模型相关类型字段的序列化,是@Field.serialize默认选项 DOT 点拼接集合元素 COMMA 逗号拼接集合元素 BIT 按位与,2次幂数求和 非@Field.serialize可选项列表,用于二进制枚举序列化不需要配置,由oinone自动推断 字段序列化方式举例 1、给模型PetItemDetail 增加两个字段:petItemDetails类型为List 和 tags类型为List,并设置为不同的序列化方式,petItemDetails为JSON(缺省就是JSON,可不配),tags为COMMA。2、同时设置 @Field.Advanced(columnDefinition = "varchar(1024)"),防止序列化后存储过长。 @Model.model(PetItem.MODEL_MODEL) @Model(displayName = "宠物商品",summary="宠物商品",labelFields = {"itemName"}) public class PetItem extends AbstractDemoCodeModel{ public static final String MODEL_MODEL="demo.PetItem"; @Field(displayName = "品种") @Field.many2one @Field.Relation(relationFields = {"typeId"},referenceFields = {"id"}) private PetType type; @Field(displayName = "品种类型",invisible = true) private Long typeId; @Field(displayName = "详情", serialize = Field.serialize.JSON, store = NullableBoolEnum.TRUE) @Field.Advanced(columnDefinition = "varchar(1024)") private List<PetItemDetail> petItemDetails; @Field(displayName = "商品标签",serialize = Field.serialize.COMMA,store = NullableBoolEnum.TRUE,multi = true) @Field.Advanced(columnDefinition = "varchar(1024)") private List<String> tags; } 字段序列化注意点 必须使用Field#store属性将字段存储设置为NullableBoolEnum.TRUE。 使用Field#serialize属性指定序列化方式,默认为JSON。 如把PetItemDetail设置为存储模型,须在PetItem的petItemDetails字段上使用Field.Relation#store属性将关联关系存储设置为false。不然会同时存储petItemDetails字段和对应的PetItemDetail表记录 注册自己的序列化器 注册自己的序列化器(实现pro.shushi.pamirs.meta.api.core.orm.serialize.Serializer接口), 如oinone的DOT的序列化方式,用type()方法返回值做匹配,serialize和deserialize分别对应序列化和反序列化方法。 package pro.shushi.pamirs.framework.compute.serialize; import org.apache.commons.lang3.StringUtils; import org.springframework.stereotype.Component; import pro.shushi.pamirs.meta.annotation.fun.extern.Slf4j; import pro.shushi.pamirs.meta.api.core.orm.serialize.Serializer; import pro.shushi.pamirs.meta.common.constants.CharacterConstants; import pro.shushi.pamirs.meta.enmu.SerializeEnum; import pro.shushi.pamirs.meta.util.TypeUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; /** * 点表达式序列生成处理器实现 * @author shushi@shushi.pro * @version 1.0.0 */ @SuppressWarnings("rawtypes") @Slf4j @Component public class DotSerializeProcessor implements Serializer<Object, String> { @Override public String serialize(String ltype, Object value) { if (null == value) { return null; } if (List.class.isAssignableFrom(value.getClass())) { return StringUtils.join((List) value, CharacterConstants.SEPARATOR_DOT); } else { return StringUtils.join(Collections.singletonList(value), CharacterConstants.SEPARATOR_DOT); } } @SuppressWarnings("unchecked") @Override public Object deserialize(String ltype, String ltypeT, String value,…

    2024年5月24日
    2.0K00

Leave a Reply

Please Login to Comment