Bean拷贝组件(注解驱动)方案设计与落地

2023-11-02

一、背景

数据流转在各层之间的过程,应当是改头换面的,字段属性数量,属性名称(一般不变,但也有重构时出现变化的情况),类型名称(普遍变化例如BO、VO、DTO)。对于转换的业务对象,原始的做法时直接实例采用Getter与Setter方法进行逐一填充。这太低效了,那我们就先了解最简单的拷贝工具。

二、问题

业界采用BeanCopyUtils、Orika、ReflectionUtils等填充工具类实现字段的拷贝。默认的实现都是以Field.getName()的值进行比对拷贝。所以针对属性名发生变化的情况很容易在不注意的情况下拷贝成null值。一旦拷贝成null值,后续的业务就会受到不同程度的影响,所以我设想以下两种方案,解决字段变化,且字段耦合面比较广泛,无法直接修改字段名称的情况。

三、方案

方案一:二次封装Orkia组件,设计classMap字段映射配置类,使用ServiceLoader服务加载器加载配置类,自定义配置,随用随配。(缺点:需要维护Java类配置变化字段的映射,变化越多,类越重)

方案二:设计类型注解与字段注解,使用spring ApplicationContextAware接口设计统一快速注册classMap中的字段映射(性能高,快速装配)。

接下来的两种方案都有一些思路以及遇到的问题及其解决方法,加深相关技术理解。

四、实现

(1)方案一实现
        核心工具类BeanCopyUtil.java
import ma.glasnost.orika.MapperFacade;
import ma.glasnost.orika.MapperFactory;
import ma.glasnost.orika.impl.DefaultMapperFactory;
import java.util.List;
import java.util.ServiceLoader;

/**
 * @author : forestSpringH
 * @description:
 * @date : Created in 2023/9/14
 * @modified By:
 * @project: 
 */
public class BeanCopyUtil {
    private static final MapperFactory MAPPER_FACTORY;
    private static final MapperFacade MAPPER_FACADE;

    static {
        MAPPER_FACTORY = new DefaultMapperFactory.Builder().build();
        MAPPER_FACADE = MAPPER_FACTORY.getMapperFacade();
        ServiceLoader<CopyInterface> serviceLoader = ServiceLoader.load(CopyInterface.class);
        for (CopyInterface beanCopyRules : serviceLoader) {
            beanCopyRules.register(MAPPER_FACTORY);
        }
    }

    public static <S, T> T map(S source, Class<T> targetClass) {
        return MAPPER_FACADE.map(source, targetClass);
    }

    public static <S, T> List<T> mapAsList(Iterable<S> source, Class<T> targetClass) {
        return MAPPER_FACADE.mapAsList(source, targetClass);
    }

}
        接口CopyInterface.java
import ma.glasnost.orika.MapperFactory;

/**
 * @author : forestSpringH
 * @description:
 * @date : Created in 2023/9/14
 * @modified By:
 * @project: 
 */
public interface CopyInterface {
    void register(MapperFactory mapperFactory);
}
         变化字段配置类BeanCopyRules.java
import com.runjing.tms.domain.dto.applet.RiderWaybillsDistributionDetailsDto;
import com.runjing.tms.repository.model.TransportExpressWaybills;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import ma.glasnost.orika.MapperFactory;

/**
 * @author : forestSpringH
 * @description:
 * @date : Created in 2023/9/14
 * @modified By:
 * @project: 
 */
@Slf4j
public class BeanCopyRules implements CopyInterface{
    @Override
    public void register(MapperFactory mapperFactory) {
        log.info("加载字段映射工厂自定义字段映射");
        mapperFactory.classMap(TransportExpressWaybills.class, RiderWaybillsDistributionDetailsDto.class)
                .field("expectTime","expectStartTime")
                .field("id","waybillId").byDefault().register();

    }
}
        注意点:

                ServiceLoader服务加载器需要查找META-INF.services下的文件,加载对应的类路劲,所以如果文件中填写的也是接口CopyInterface.java的路径而不是其实现类BeanCopyRules.java的路径,就会加载出来ServiceLoader<CopyInterface>内部的实例为空,无法进入循环。

        META-INF.service下的com.runjing.tms.util.orika.CopyInterface文件
com.runjing.tms.util.orika.BeanCopyRules
(2)方案二实现
         代码分包结构:

         类型注解EnableOpenFieldCopy.java
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

import java.lang.annotation.*;

/**
 * @author : forestSpringH
 * @description:
 * @date : Created in 2023/9/14
 * @modified By:
 * @project: 
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Component
@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public @interface EnableOpenFieldCopy {
    boolean value() default true;

    boolean callSuper() default false;

    boolean callSoon() default false;
}
        字段注解FieldCopyMapping.java
import java.lang.annotation.*;

/**
 * @author : forestSpringH
 * @description: 字段映射注解
 * @date : Created in 2023/9/14
 * @modified By:
 * @project:
 */
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface FieldCopyMapping {
    String targetFieldName() default "";

    Class<?>[] targetClass() default {};
}
        SpringHolder.java关键代码段
    public static List<Class<?>> getBeanByAnnotation(Class<? extends Annotation> annotationClazz){
        Assert.notNull(serviceApplicationContext, "容器上下文获取失败");
        Assert.notNull(annotationClazz,"注解字节码入参为空");
        List<String> collect = Arrays.stream(serviceApplicationContext.getBeanNamesForAnnotation(annotationClazz)).collect(Collectors.toList());
        List<Class<?>> classList = new LinkedList<>();
        if (!CollectionUtils.isEmpty(collect)){
            collect.forEach(s -> classList.add(getBeanByName(s).getClass()));
        }
        return classList;
    }
         BeanCopyService.java核心代码段
    @PostConstruct
    public void init() {
        log.info("初始化BeanCopyService组件");
        mapperFactory = new DefaultMapperFactory.Builder().build();
        mapperFacade = mapperFactory.getMapperFacade();
        log.info("加载字段拷贝映射注解类");
        List<Class<?>> beanList = SpringHolder.getBeanByAnnotation(EnableOpenFieldCopy.class);
        register(beanList);
    }
    public <S, T> T copyBean(S source, Class<T> targetClass) {
        return mapperFacade.map(source, targetClass);
    }
    private void register(List<Class<?>> beanCopyList) {
        if (!CollectionUtils.isEmpty(beanCopyList)) {
            beanCopyList.forEach(clazz -> {
                //获取类的属性
                log.info("获取映射注解类:{}下字段集合", clazz.getName());
                List<Field> collect = Arrays.stream(clazz.getDeclaredFields()).collect(Collectors.toList());
                if (!CollectionUtils.isEmpty(collect)) {
                    collect.forEach(field -> {
                        //获取属性中打上映射注解的注解
                        if (field.isAnnotationPresent(FieldCopyMapping.class)) {
                            FieldCopyMapping annotation = field.getAnnotation(FieldCopyMapping.class);
                            String sourceFieldName = field.getName();
                            //获取注解上的目标字段名
                            String targetFieldName = annotation.targetFieldName();
                            log.info("配置字段:{} 映射 {}", sourceFieldName, targetFieldName);
                            //获取注解上的目标拷贝对象字节码数组
                            List<Class<?>> targetClazzList = Arrays.stream(annotation.targetClass()).collect(Collectors.toList());
                            if (!CollectionUtils.isEmpty(targetClazzList)) {
                                //逐一注册
                                log.info("逐一注册字段映射模型列表");
                                targetClazzList.forEach(targetClazz -> {
                                    MapperModel model = new MapperModel(clazz.getName() + targetClazz.getName(), clazz, targetClazz, sourceFieldName, targetFieldName);
                                    mapperModelList.add(model);
                                });
                            }
                        }
                    });
                }
            });

            Map<String, List<MapperModel>> group = groupByMapperKey(mapperModelList);
            if (!CollectionUtils.isEmpty(group)) {
                group.values().forEach(modelList -> {
                    log.info("开始映射:{}", modelList);
                    ClassMapBuilder<?, ?> classMapBuilder = mapperFactory.classMap(modelList.get(0).getSourceClass(), modelList.get(0).getTargetClass());
                    for (MapperModel model : modelList) {
                        if (Objects.equals(modelList.get(modelList.size() - 1), model)) {
                            log.info("映射注册完毕:{}", model.getMapperKey());
                            classMapBuilder.field(model.getSourceFieldName(), model.getTargetFieldName()).byDefault().register();
                        } else {
                            classMapBuilder.field(model.getSourceFieldName(), model.getTargetFieldName());
                        }
                    }
                });
            }
        }
    }
    private Map<String, List<MapperModel>> groupByMapperKey(List<MapperModel> modelList) {
        Map<String, List<MapperModel>> groupMap = new HashMap<>();
        if (CollectionUtils.isEmpty(modelList)) {
            return groupMap;
        }
        Set<String> keys = modelList.stream().map(MapperModel::getMapperKey).collect(Collectors.toSet());
        keys.forEach(key -> {
            List<MapperModel> mapperModels = new LinkedList<>();
            modelList.forEach(mapperModel -> {
                if (Objects.equals(mapperModel.getMapperKey(), key)) {
                    mapperModels.add(mapperModel);
                }
            });
            groupMap.put(key, mapperModels);
        });
        return groupMap;
    }

 五、测试

        Person.java测试实体
@EnableOpenFieldCopy
@Data
public class Person {

    @FieldCopyMapping(targetFieldName = "id", targetClass = {PersonBo.class, PersonDto.class})
    private int age;

    @FieldCopyMapping(targetFieldName = "personName",targetClass = {PersonDto.class})
    private String name;
}
        PersonBo.java测试实体
@Data
public class PersonBo {
    private int id;
    private String name;
}
        PersonDto.java测试实体
@Data
public class PersonDto {
    private int id;
    private String personName;
}
        单元测试代码
    @Test
    public void copy(){
        Person person = new Person();
        person.setAge(1);
        person.setName("hlc");
        PersonBo personBo = beanCopyService.copyBean(person, PersonBo.class);
        PersonDto personDto = beanCopyService.copyBean(person, PersonDto.class);
        System.out.println(personBo);
        System.out.println(personDto);
    }
        断点查看结果

代码逻辑还需要继续优化,方案二跑通之后将会将其设计成jar包。

导入使用。 

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Bean拷贝组件(注解驱动)方案设计与落地 的相关文章

随机推荐

  • 贺中国信通院“星火·链网”数字原生资产(DNA)服务网络隆重发布

    5月20日 中国信通院 星火 链网 数字原生资产 DNA 服务网络发布会在云端圆满举办 中国信通院院长 中关村区块链产业联盟理事长余晓晖出席会议并为 星火 链网 数字原生资产 DNA 服务网络上线发表寄语 中国信通院总工程师敖立 新华网首席
  • 识别图像模板旋转角度_基于视觉的焊缝识别与定位技术

    为了实现焊前引导 必须首先通过视觉传感系统识别工件和焊缝 确定焊接的关键点位置 建立关键点的二维或三维坐标 发送给机器人 将机器人的末端执行器运动到焊接起始点 自动完成焊前导引 焊缝识别的准确率与识别精度直接影响焊缝跟踪的精度 因此 焊缝识
  • 通过nginx代理拦截请求,进行全局访问限制

    声明 本博文用于学习总结及工作心得 运行环境 Ubantu 14 0 tomcat7 nginx 1 4 6 更新后1 5 6 项目中经常会用到权限管理 必然的就会存在权限的设定和验证 对于登陆或者模块的权限设定验证 在项目中直接实现 那么
  • 地图服务标注显示乱码问题

    版本 ArcGIS 10 1 在Catalog中发布了一个地图服务 直接切了图 切图后发现标注有乱码 操作系统是win7 不会涉及Server对字体库的访问权限问题 排查了一下 发现了原因 标注字体不能使用不支持中文的英文或者其他非中文字体
  • Golang基础 变量与常量

    Golang基础 变量与常量 01 变量声明 02 常量声明 03 变量初始化 04 常量初始化 参考资料 01 变量声明 变量就是内存堆栈区的一块地址空间用于存储数据 Go语言在使用变量时需要先声明变量 常用的声明方式有两种 使用var关
  • 用python最新版本安装web3后调试错误原因和解决方法

    由于调试web3 安装了最新版本的python3 11 用命令安装 pip install web3 提示安装错误 无法完成 仔细观察根据错误提示发现是 VC 14没有安装的原因 根据提示从微软官方下载vs BuildTools并单独安装V
  • 闭包(闭包使用场景,闭包内存泄漏,js内存管理及垃圾回收)

    1 什么是闭包 在认识闭包之前 我们先简单了解两个知识点 JavaScript 中的作用域和作用域链 JavaScript 中的垃圾回收 目的就是为了方便我们更容易理解闭包 1 JavaScript 中的作用域和作用域链 作用域就是一个独立
  • 内存泄漏全解析,从此拒绝ANR,让OOM远离你的身边,跟内存泄漏say byebye

    http www cnblogs com liushilin p 5900089 html 一 写在前面 二 一些杂谈 1 这里先安利一下java的内存分配 2 四种引用类型的介绍 3 内存抖动 这样的图很熟悉有木有 当这样的时候 说明你的
  • [医学多模态融合系列 -1] A review: Deep learning for medical image segmentation using multi-modality fusion 解读

    医学多模态融合系列 1 A review Deep learning for medical image segmentation using multi modality fusion 0 Abstract 1 Introduction
  • redis漏洞修复:CVE-2022-35977、CVE-2023-22458、CVE-2023-28856

    提示 文章写完后 目录可以自动生成 如何生成可参考右边的帮助文档 文章目录 前言 一 漏洞内容 二 现状 三 更新redis 下载镜像 停止已有的容器 启动新的容器 四 更新后的版本 1 查看日志 2 查看版本 总结 前言 漏扫发现机器上的
  • MYSQL原理、设计与应用

    概述 数据库 Database DB 是按照数据结构来组织 存储和管理数据的仓库 其本身可被看作电子化的文件柜 用户可以对文件中的数据进行增删改查等操作 数据库系统是指在计算机系统中引入数据库后的系统 除了数据库 还包括数据库管理系统 Da
  • 攻防世界-MISC之如来十三掌

    一 下载打开附件1 出现一堆梵文 夜哆悉諳多苦奢陀奢諦冥神哆盧穆皤三侄三即諸諳即冥迦冥隸數顛耶迦奢若吉怯陀諳怖奢智侄諸若奢數菩奢集遠俱老竟寫明奢若梵等盧皤豆蒙密離怯婆皤礙他哆提哆多缽以南哆心曰姪罰蒙呐神 舍切真怯勝呐得俱沙罰娑是怯遠得呐數罰
  • 行人属性识别:HydraPlus-Net: Attentive Deep Features for Pedestrian Analysis

    参考文献 https arxiv org abs 1709 09930 代码实现 https github com xh liu HydraPlus Net 包括理解 HydraPlus Net Attentive Deep Feature
  • 小白学GAN系列4——torch.optim

    torch optim是一个实现了多种优化算法的包 大多数通用的方法都已支持 提供了丰富的接口调用 未来更多精炼的优化算法也将整合进来 为了使用torch optim 需先构造一个优化器对象Optimizer 用来保存当前的状态 并能够根据
  • 线程问题的核心: 怎么退出线程才是合适的----小话多线程(2)

    作者 陈曦 日期 2012 8 5 16 13 36 环境 Mac 10 7 1 Lion Intel i3 支持64位指令 gcc4 2 1 xcode4 2 苹果开源代码Libc 763 11 转载请注明出处 每日总结 优秀的架构都是类
  • 网络体系结构

    网络体系结构概述 1 网络协议 网络协议的三要素 语义 语法和同步 语法 规定通信双方彼此应该如何操作 即确定协议元素的格式 如 数据格式 信号平等规定 语义 规定通信双方要发出的控制信息 执行的动作和返回的应答等 包括用于调整和运行差错处
  • 云计算之k8s系列_第十二回

    上一回讲解了控制器 这一回详细看看控制器中Deployment控制器 k8s中 Deployment实现了一个非常重要的功能 pod的水平扩展与收缩 如果我们更新了Deployment的pod模板 那么deployment就需要 滚动更新
  • c# ??=

    空合并运算符 用于定义引用类型和可空类型的默认值 如果此运算符的左操作符不为Null 则此操作符返回左操作数 否则返回右操作数 例如 当a不为空时返回a 为null时返回b var c a b 空合并赋值运算符 C 8 0 及更高版本中可使
  • linux和Ubuntu如何创建共享文件夹

    1 打开虚拟机界面 并启动linux 2 选中上方功能栏中的虚拟机 选中设置 3 点击CD DVD SATA 看到右边有一个使用IOS镜像文件 4 路径填写安装这个虚拟机用的镜像文件路径 5 然后在选项里选择共享文件夹 右边选择总是启用 添
  • Bean拷贝组件(注解驱动)方案设计与落地

    一 背景 数据流转在各层之间的过程 应当是改头换面的 字段属性数量 属性名称 一般不变 但也有重构时出现变化的情况 类型名称 普遍变化例如BO VO DTO 对于转换的业务对象 原始的做法时直接实例采用Getter与Setter方法进行逐一