作者:宁海翔

1 前言

方针复制,是咱们在开发进程中,绕不开的进程,既存在于Po、Dto、Do、Vo各个表现层数据的转化,也存在于体系交互如序列化、反序列化。

Java方针复制分为深复制和浅复制,现在常用的特点复制东西,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct都是浅复制。

1.1 深复制

深复制:对根本数据类型进行值传递,对引用数据类型,创立一个新的方针,并复制其内容称为深复制。

深复制常见有以下四种完结办法:

  • 结构函数
  • Serializable序列化
  • 完结Cloneable接口
  • JSON序列化

Java对象拷贝原理剖析及最佳实践

1.2 浅复制

浅复制:对根本数据类型进行值传递,对引用数据类型进行引用传递般的复制称为浅复制。经过完结Cloneabe接口并重写Object类中的clone()办法可以完结浅克隆。

Java对象拷贝原理剖析及最佳实践

2 常用方针复制东西原理剖析及功能比照

现在常用的特点复制东西,包括Apache的BeanUtils、Spring的BeanUtils、Cglib的BeanCopier、mapstruct。

  • Apache BeanUtils:BeanUtils是Apache commons组件里面的成员,由Apache供给的一套开源 api,用于简化对javaBean的操作,可以对根本类型自动转化。
  • Spring BeanUtils:BeanUtils是spring框架下自带的东西,在org.springframework.beans包下, spring项目可以直接运用。
  • Cglib BeanCopier:cglib(Code Generation Library)是一个强大的、高功能、高质量的代码生成类库,BeanCopier依托于cglib的字节码增强才能,动态生成完结类,完结方针的复制。
  • mapstruct:mapstruct 是一个 Java注释处理器,用于生成类型安全的 bean 映射类,在构建时,根据注解生成完结类,完结方针复制。

2.1 原理剖析

2.1.1 Apache BeanUtils

运用办法:BeanUtils.copyProperties(target, source);
BeanUtils.copyProperties 方针复制的核心代码如下:


// 1.获取源方针的特点描绘
PropertyDescriptor[] origDescriptors = this.getPropertyUtils().getPropertyDescriptors(orig);
PropertyDescriptor[] temp = origDescriptors;
int length = origDescriptors.length;
String name;
Object value;
// 2.循环获取源方针每个特点,设置方针方针特点值
for(int i = 0; i < length; ++i) {
PropertyDescriptor origDescriptor = temp[i];
name = origDescriptor.getName();
// 3.校验源方针字段可读切方针方针该字段可写
if (!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
try {
// 4.获取源方针字段值
value = this.getPropertyUtils().getSimpleProperty(orig, name);
// 5.复制特点
this.copyProperty(dest, name, value);
} catch (NoSuchMethodException var10) {
}
}
}

循环遍历源方针的每个特点,关于每个特点,复制流程为:

  • 校验来源类的字段是否可读isReadable
  • 校验方针类的字段是否可写isWriteable
  • 获取来源类的字段特点值getSimpleProperty
  • 获取方针类字段的类型type,并进行类型转化
  • 设置方针类字段的值

由于单字段复制时每个阶段都会调用PropertyUtilsBean.getPropertyDescriptor获取特点装备,而该办法经过for循环获取类的字段特点,严重影响复制效率
获取字段特点装备的核心代码如下:

PropertyDescriptor[] descriptors = this.getPropertyDescriptors(bean);
if (descriptors != null) {
for (int i = 0; i < descriptors.length; ++i) {
if (name.equals(descriptors[i].getName())) {
return descriptors[i];
}
}
}

2.1.2 Spring BeanUtils

运用办法: BeanUtils.copyProperties(source, target);
BeanUtils.copyProperties核心代码如下:

PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
PropertyDescriptor[] arr$ = targetPds;
int len$ = targetPds.length;
for(int i$ = 0; i$ < len$; ++i$) {
PropertyDescriptor targetPd = arr$[i$];
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
} catch (Throwable var15) {
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
}
}
}
}
}

复制流程简要描绘如下:

  • 获取方针类的所有特点描绘
  • 循环方针类的特点值做以下操作
    • 获取方针类的写办法
    • 获取来源类的该特点的特点描绘(缓存获取)
    • 获取来源类的读办法
    • 读来源特点值
    • 写方针特点值

与Apache BeanUtils的特点复制相比,Spring经过Map缓存,防止了类的特点描绘重复获取加载,经过懒加载,初次复制时加载所有特点描绘。

Java对象拷贝原理剖析及最佳实践

2.1.3 Cglib BeanCopier

运用办法:

BeanCopier beanCopier = BeanCopier.create(AirDepartTask.class, AirDepartTaskDto.class, false);
beanCopier.copy(airDepartTask, airDepartTaskDto, null);

create调用链如下:

BeanCopier.create
-> BeanCopier.Generator.create
-> AbstractClassGenerator.create
->DefaultGeneratorStrategy.generate
-> BeanCopier.Generator.generateClass

BeanCopier 经过cglib动态代理操作字节码,生成一个复制类,触发点为BeanCopier.create

Java对象拷贝原理剖析及最佳实践

2.1.4 mapstruct

运用办法:

  • 引进pom依赖
  • 声明转化接口

mapstruct根据注解,构建时自动生成完结类,调用链如下:
MappingProcessor.process -> MappingProcessor.processMapperElements
MapperCreationProcessor.process:生成完结类Mapper
MapperRenderingProcessor:将完结类mapper,写入文件,生成impl文件
运用时需求声明转化接口,例如:

@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface AirDepartTaskConvert {
AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);
AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
}

生成的完结类如下:

public class AirDepartTaskConvertImpl implements AirDepartTaskConvert {
@Override
public AirDepartTaskDto convertToDto(AirDepartTask airDepartTask) {
if ( airDepartTask == null ) {
return null;
}
AirDepartTaskDto airDepartTaskDto = new AirDepartTaskDto();
airDepartTaskDto.setId( airDepartTask.getId() );
airDepartTaskDto.setTaskId( airDepartTask.getTaskId() );
airDepartTaskDto.setPreTaskId( airDepartTask.getPreTaskId() );
List<String> list = airDepartTask.getTaskBeginNodeCodes();
if ( list != null ) {
airDepartTaskDto.setTaskBeginNodeCodes( new ArrayList<String>( list ) );
}
// 其他特点复制
airDepartTaskDto.setYn( airDepartTask.getYn() );
return airDepartTaskDto;
}
}

2.2 功能比照

以航空业务体系中发货任务po到dto转化为例,跟着复制数据量的增大,研讨复制数据耗时情况

Java对象拷贝原理剖析及最佳实践

2.3 复制选型

经过以上剖析,跟着数据量的增大,耗时全体呈上升趋势

  • 全体情况下,Apache BeanUtils的功能最差,日常运用进程中不主张运用
  • 在数据规模不大的情况下,spring、cglib、mapstruct差异不大,spring框架下主张运用spring的beanUtils,不需求额外引进依赖包
  • 数据量大的情况下,主张运用cglib和mapstruct
  • 涉及很多数据转化,特点映射,格式转化的,主张运用mapstruct

3 最佳实践

3.1 BeanCopier

运用时可以运用map缓存,减少同一类方针转化时,create次数

/**
* BeanCopier的缓存,防止频繁创立,高效复用
*/
private static final ConcurrentHashMap<String, BeanCopier> BEAN_COPIER_MAP_CACHE = new ConcurrentHashMap<String, BeanCopier>();
/**
* BeanCopier的copyBean,高功能引荐运用,增加缓存
*
* @param source 源文件的
* @param target 方针文件
*/
public static void copyBean(Object source, Object target) {
String key = genKey(source.getClass(), target.getClass());
BeanCopier beanCopier;
if (BEAN_COPIER_MAP_CACHE.containsKey(key)) {
beanCopier = BEAN_COPIER_MAP_CACHE.get(key);
} else {
beanCopier = BeanCopier.create(source.getClass(), target.getClass(), false);
BEAN_COPIER_MAP_CACHE.put(key, beanCopier);
}
beanCopier.copy(source, target, null);
}
/**
* 不同类型方针数据copylist
*
* @param sourceList
* @param targetClass
* @param <T>
* @return
*/
public static <T> List<T> copyListProperties(List<?> sourceList, Class<T> targetClass) throws Exception {
if (CollectionUtils.isNotEmpty(sourceList)) {
List<T> list = new ArrayList<T>(sourceList.size());
for (Object source : sourceList) {
T target = copyProperties(source, targetClass);
list.add(target);
}
return list;
}
return Lists.newArrayList();
}
/**
* 回来不同类型方针数据copy,运用此办法需注意不能覆盖默许的无参结构办法
*
* @param source
* @param targetClass
* @param <T>
* @return
*/
public static <T> T copyProperties(Object source, Class<T> targetClass) throws Exception {
T target = targetClass.newInstance();
copyBean(source, target);
return target;
}
/**
* @param srcClazz 源class
* @param tgtClazz 方针class
* @return string
*/
private static String genKey(Class<?> srcClazz, Class<?> tgtClazz) {
return srcClazz.getName() + tgtClazz.getName();
}

3.2 mapstruct

mapstruct支持多种形式方针的映射,主要有下面几种

  • 根本映射
  • 映射表达式
  • 多个方针映射到一个方针
  • 映射调集
  • 映射map
  • 映射枚举
  • 嵌套映射
@Mapper(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface AirDepartTaskConvert {
AirDepartTaskConvert INSTANCE = getMapper(AirDepartTaskConvert.class);
// a.根本映射
@Mapping(target = "createTime", source = "updateTime")
// b.映射表达式
@Mapping(target = "updateTimeStr", expression = "java(new SimpleDateFormat( \"yyyy-MM-dd\" ).format(airDepartTask.getCreateTime()))")
AirDepartTaskDto convertToDto(AirDepartTask airDepartTask);
}
@Mapper
public interface AddressMapper {
AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);
// c.多个方针映射到一个方针
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
@Mapper
public interface CarMapper {
// d.映射调集
Set<String> integerSetToStringSet(Set<Integer> integers);
List<CarDto> carsToCarDtos(List<Car> cars);
CarDto carToCarDto(Car car);
// e.映射map
@MapMapping(valueDateFormat = "dd.MM.yyyy")
Map<String,String> longDateMapToStringStringMap(Map<Long, Date> source);
// f.映射枚举
@ValueMappings({
@ValueMapping(source = "EXTRA", target = "SPECIAL"),
@ValueMapping(source = "STANDARD", target = "DEFAULT"),
@ValueMapping(source = "NORMAL", target = "DEFAULT")
})
ExternalOrderType orderTypeToExternalOrderType(OrderType orderType);
// g.嵌套映射
@Mapping(target = "fish.kind", source = "fish.type")
@Mapping(target = "fish.name", ignore = true)
@Mapping(target = "ornament", source = "interior.ornament")
@Mapping(target = "material.materialType", source = "material")
@Mapping(target = "quality.report.organisation.name", source = "quality.report.organisationName")
FishTankDto map( FishTank source );
}

4 总结

以上便是我在运用方针复制进程中的一点浅谈。在日常体系开发进程中,要深究底层逻辑,哪怕发现一小点的改动可以使咱们的体系更加安稳、顺畅,都是值得咱们去改善的。

最终,希望跟着咱们的加入,体系会更加安稳、顺畅,咱们会变得越来越优秀。