本文来说下Bean的各类拷贝工具
概述
在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型。
这种转换最原始的方式就是手动编写大量的 get/set代码,当然这是我们开发过程不愿意去做的,因为它确实显得很繁琐。为了解决这一痛点,就诞生了一些方便的类库,常用的有 apache的 BeanUtils,spring的 BeanUtils, Dozer,Orika等拷贝工具。
对象拷贝
在具体介绍各种BeanUtils之前,先来补充一些基础知识。它们本质上就是对象拷贝工具,而对象拷贝又分为深拷贝和浅拷贝,下面进行详细解释。
什么是浅拷贝和深拷贝
浅拷贝只会拷贝引用本身,而深拷贝还会拷贝引用所指向的数据信息
在Java中,除了 基本数据类型 之外,还存在 类的实例对象 这个引用数据类型,而一般使用 “=”号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际还是指向的同一个对象。
而浅拷贝和深拷贝就是在这个基础上做的区分,如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝 。反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝 。
简单来说:
浅拷贝 :对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
深拷贝 :对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
BeanUtils
前面简单讲了一下对象拷贝的一些知识,下面就来具体看下常用的BeanUtils工具
apache的BeanUtils
Address类
package cn.wideth.util;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class Address {
private String address;
}
user对象实体
package cn.wideth.util;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class PersonInfo {
private Integer id;
private String username;
private Address address;
}
user对象拷贝实体
package cn.wideth.util;
import lombok.Data;
@Data
public class PersonCopy {
private Integer id;
private String username;
private Address address;
}
测试程序
package cn.wideth.util;
import org.apache.commons.beanutils.BeanUtils;
public class TestApacheBeanUtils {
public static void main(String[] args) {
//下面只是用于单独测试
PersonInfo pi = new PersonInfo(1, "jack", new Address("北京"));
PersonCopy pc = new PersonCopy();
try {
BeanUtils.copyProperties(pc,pi);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
System.out.println("-------变更值以后----------");
pi.setId(10);
pi.setUsername("hello");
pi.getAddress().setAddress("上海");
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
}
}
测试结果
从测试程序中,可以看到apache的BeanUtils是一种浅拷贝。
从上面的例子可以看出,对象拷贝非常简单,BeanUtils最常用的方法就是:
//将源对象中的值拷贝到目标对象
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}
默认情况下,使用org.apache.commons.beanutils.BeanUtils对复杂对象的复制是引用,这是一种浅拷贝。
但是由于 Apache下的BeanUtils对象拷贝性能太差,不建议使用,而且在阿里巴巴Java开发规约插件 上也明确指出避免用Apache Beanutils进行属性的copy。
commons-beantutils 对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能,具体实现代码如下:
public void copyProperties(final Object dest, final Object orig)
throws IllegalAccessException, InvocationTargetException {
// Validate existence of the specified beans
if (dest == null) {
throw new IllegalArgumentException
("No destination bean specified");
}
if (orig == null) {
throw new IllegalArgumentException("No origin bean specified");
}
if (log.isDebugEnabled()) {
log.debug("BeanUtils.copyProperties(" + dest + ", " +
orig + ")");
}
// Copy the properties, converting as necessary
if (orig instanceof DynaBean) {
final DynaProperty[] origDescriptors =
((DynaBean) orig).getDynaClass().getDynaProperties();
for (DynaProperty origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
// Need to check isReadable() for WrapDynaBean
// (see Jira issue# BEANUTILS-61)
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
final Object value = ((DynaBean) orig).get(name);
copyProperty(dest, name, value);
}
}
} else if (orig instanceof Map) {
@SuppressWarnings("unchecked")
final
// Map properties are always of type <String, Object>
Map<String, Object> propMap = (Map<String, Object>) orig;
for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
final String name = entry.getKey();
if (getPropertyUtils().isWriteable(dest, name)) {
copyProperty(dest, name, entry.getValue());
}
}
} else /* if (orig is a standard JavaBean) */ {
final PropertyDescriptor[] origDescriptors =
getPropertyUtils().getPropertyDescriptors(orig);
for (PropertyDescriptor origDescriptor : origDescriptors) {
final String name = origDescriptor.getName();
if ("class".equals(name)) {
continue; // No point in trying to set an object's class
}
if (getPropertyUtils().isReadable(orig, name) &&
getPropertyUtils().isWriteable(dest, name)) {
try {
final Object value =
getPropertyUtils().getSimpleProperty(orig, name);
copyProperty(dest, name, value);
} catch (final NoSuchMethodException e) {
// Should not happen
}
}
}
}
}
spring的BeanUtils
使用spring的BeanUtils进行对象拷贝:
package cn.wideth.util;
import org.springframework.beans.BeanUtils;
public class TestSpringBeanUtils {
public static void main(String[] args) {
//下面只是用于单独测试
PersonInfo pi = new PersonInfo(2, "tom", new Address("杭州"));
PersonCopy pc = new PersonCopy();
BeanUtils.copyProperties(pi,pc);
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
System.out.println("-------变更值以后----------");
pi.setId(16);
pi.setUsername("hello");
pi.getAddress().setAddress("苏州");
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
}
}
程序结果
从测试程序中,可以看到spring的BeanUtils是一种浅拷贝。
spring下的BeanUtils也是使用 copyProperties方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。具体实现如下:
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
Class<?> actualEditable = target.getClass();
if (editable != null) {
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
for (PropertyDescriptor targetPd : targetPds) {
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 ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
可以看到,成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同。
cglib BeanCopier
maven导入
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
使用cglib BeanCopier进行对象拷贝:
package cn.wideth.util;
import org.springframework.cglib.beans.BeanCopier;
public class TeatBeanCopier {
public static void main(String[] args) {
//下面只是用于单独测试
PersonInfo pi = new PersonInfo(2, "tom", new Address("长春"));
PersonCopy pc = new PersonCopy();
BeanCopier beanCopier = BeanCopier.create(
pi.getClass(),
pc.getClass(), false);
beanCopier.copy(pi,pc,null);
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
System.out.println("-------变更值以后----------");
pi.setId(10);
pi.setUsername("jack");
pi.getAddress().setAddress("哈尔滨");
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
}
}
程序结果
在使用BeanCopier时,如果存在基本类型和包装类,是无法被正常拷贝,改为相同类型后才能被正常拷贝。另外,BeanCopier使用的仍然是浅拷贝,从测试程序中大家可以看出。
Hutool BeanUtil
hutool是个人平常使用比较频繁的一个工具包,对文件、加密解密、转码、正则、线程、XML等JDK方法进行封装,并且也可以进行对象的拷贝。在使用前引入坐标:
maven导入
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.6</version>
</dependency>
使用Hutool BeanUtil的测试程序
package cn.wideth.util;
import cn.hutool.core.bean.BeanUtil;
public class TestHutool {
public static void main(String[] args) {
//下面只是用于单独测试
PersonInfo pi = new PersonInfo(2, "tom", new Address("厦门"));
PersonCopy pc = new PersonCopy();
BeanUtil.copyProperties(pi,pc);
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
System.out.println("-------变更值以后----------");
pi.setId(23);
pi.setUsername("jack");
pi.getAddress().setAddress("福州");
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
}
}
程序结果
从程序结果,我们知道使用的也是浅拷贝方式。
Mapstruct
Mapstruct的使用和上面几种方式有些不同,因为上面的几种方式,spring和apache,hutool使用的都是反射,cglib是基于字节码文件的操作,都是在都代码运行期间动态执行的,但是Mapstruct不同,它在编译期间就生成了 Bean属性复制的代码,运行期间就无需使用反射或者字节码技术,所以具有很高的性能。
maven导入
<!--mapstruct-->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>1.3.0.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.0.Final</version>
</dependency>
需要额外写一个接口来实现:
package cn.wideth.util;
import org.mapstruct.Mapper;
@Mapper
public interface ConvertMapper {
PersonCopy infoToCopy(PersonInfo pi);
}
这里的@Mapper注解不是用于mybatis的注解,而是org.mapstruct.Mapper。使用起来也非常简单:
程序测试
package cn.wideth.util;
import org.mapstruct.factory.Mappers;
public class TestMapstruct {
public static void main(String[] args) {
//下面只是用于单独测试
PersonInfo pi = new PersonInfo(2, "tom", new Address("成都"));
ConvertMapper mapper = Mappers.getMapper(ConvertMapper.class);
PersonCopy pc = mapper.infoToCopy(pi);
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
System.out.println("-------变更值以后----------");
pi.setId(30);
pi.setUsername("jack");
pi.getAddress().setAddress("重庆");
System.out.println("pi: " + pi);
System.out.println("pc: "+ pc);
}
}
程序结果
从测试程序可以看出Mapstruct依然是浅拷贝。
本文小结
本文介绍了几种常用的bean拷贝对象。