一、背景
数据流转在各层之间的过程,应当是改头换面的,字段属性数量,属性名称(一般不变,但也有重构时出现变化的情况),类型名称(普遍变化例如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包。
导入使用。