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

一、背景

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

二、问题

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

三、方案

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

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

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

四、实现

(1)方案一实现
核心工具类BeanCopyUtil.java
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
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
typescript 复制代码
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内部的实例为空,无法进入循环。

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

​编辑

类型注解EnableOpenFieldCopy.java
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
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关键代码段
swift 复制代码
    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核心代码段
ini 复制代码
    @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测试实体
less 复制代码
@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测试实体
arduino 复制代码
@Data
public class PersonBo {
    private int id;
    private String name;
}
PersonDto.java测试实体
arduino 复制代码
@Data
public class PersonDto {
    private int id;
    private String personName;
}
单元测试代码
ini 复制代码
    @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包。

导入使用。

相关推荐
长栎1 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode1 小时前
Redis 在生产项目的使用
前端·后端
用户559822481221 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode1 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战1 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha1 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn1 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425911 小时前
ShardingJDBC
后端
行者全栈架构师1 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端
令人头秃的代码0_01 小时前
mac(m5)平台编译openjdk
java