背景
在开发过程中, 在数据库查询和接口调用过程中,为了隔离,我们往往会将查询出来的对象(包括数据库返回和接口返回)和对外提供的实体对象隔离开来。此时就需要把一个对象的属性拷贝到目标对象中
通常有2种做法:
1、一个一个set b.setField(a.getField());
2、使用拷贝的工具类,比方说 BeanUtils.copyProperties (因为他们的类结构和属性字段大多是类似的)
对于字段比较多的场景,使用BeanUtils明显更加简洁
那么问题来了,
BeanUtils对于对象中包含的对象也能够帮我们进行拷贝吗?
如果会进行拷贝,它帮我们做的拷贝是深拷贝还是浅拷贝?
深浅拷贝 的主要区别就是:复制的是引用还是复制的是值。
如果仅仅复制了引用,也就是说,复制之后,原来的变量和新的变量指向同一个地址,彼此之间的操作会互相影响,为 浅拷贝。
而如果是在堆中重新分配内存,拥有不同的地址,但是值是一样的,复制后的对象与原来的对象是完全隔离,互不影响,就是深拷贝。
BeansUtils 实战
创建两个类 Student和Course 以及一个目标类StudentEntity(和Student类除了名字以外其余字段和结构都完全一致)
public class Student {
private String name;
private Integer age;
/**
* 是否已毕业
* */
private Boolean isGraduated;
private Course course;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean getGraduated() {
return isGraduated;
}
public void setGraduated(Boolean graduated) {
isGraduated = graduated;
}
public Course getCourse() {
return course;
}
public void setCourse(Course course) {
this.course = course;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", isGraduated=" + isGraduated +
", course=" + course +
'}';
}
public class Course {
private String courseName;
private Integer score;
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
@Override
public String toString() {
return "Course{" +
"courseName='" + courseName + '\'' +
", score=" + score +
'}';
}
}
测试类:
public class Test {
@org.junit.Test
public void testCopyStudent(){
Student student = new Student();
student.setName("chenpp");
student.setAge(21);
student.setGraduated(true);
Course course = new Course();
course.setCourseName("Chinese");
course.setScore(100);
student.setCourse(course);
StudentEntity copyStudent = new StudentEntity();
BeanUtils.copyProperties(student, copyStudent);
System.out.println("拷贝后的对象" + copyStudent);
copyStudent.getCourse().setCourseName("Math");
System.out.println("修改拷贝后的course,原来的对象" + student);
}
}
从上面的代码可以看出,spring-beans提供的BeanUtils.copyProperties对于外层的属性无论是基本数据类型还是引用数据类型都会进行拷贝,使用的是浅拷贝
Boolean和boolean类型的拷贝
在使用过程中,发现如果Student的isGraduated字段的set方法定义成isGraduated(),那么拷贝就会失败
public class Student {
private String name;
private Integer age;
/**
* 是否已毕业
* */
private Boolean isGraduated;
private Course course;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Boolean isGraduated() {
return isGraduated;
}
public void setIsGraduated(Boolean isGraduated) {
this.isGraduated = isGraduated;
}
}
测试结果: isGraduated字段没有被拷贝
跟踪下源码看看是什么原因?
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
Class<?> actualEditable = target.getClass();
//...
//根据targetClass获取其属性描述器
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
//...
for (PropertyDescriptor targetPd : targetPds) {
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
//根据sourceClass获取其属性描述器(和targetClass执行的方法一样)
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
//如果源对象的readMethod为空,就无法获取到源对象的值,也就无法通过反射进行赋值
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
//...
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
}
}
}
}
}
}
其源码路径如下
BeanUtils.getPropertyDescriptor -->
CachedIntrospectionResults.forClass(clazz) -->
new CachedIntrospectionResults(beanClass) -->
Introspector.getBeanInfo(Class<?> beanClass) -->
Introspector.getBeanInfo()
private BeanInfo getBeanInfo() throws IntrospectionException {
// the evaluation order here is import, as we evaluate the
// event sets and locate PropertyChangeListeners before we
// look for properties.
BeanDescriptor bd = getTargetBeanDescriptor();
MethodDescriptor mds[] = getTargetMethodInfo();
EventSetDescriptor esds[] = getTargetEventInfo();
//关键方法,获取目标对象的属性信息 这里就是StudentEntity
PropertyDescriptor pds[] = getTargetPropertyInfo();
int defaultEvent = getTargetDefaultEventIndex();
int defaultProperty = getTargetDefaultPropertyIndex();
return new GenericBeanInfo(bd, esds, defaultEvent, pds,
defaultProperty, mds, explicitBeanInfo);
}
private PropertyDescriptor[] getTargetPropertyInfo() {
Method methodList[] = getPublicDeclaredMethods(beanClass);
for (int i = 0; i < methodList.length; i++) {
//...
Class<?>[] argTypes = method.getParameterTypes();
Class<?> resultType = method.getReturnType();
int argCount = argTypes.length;
//...
if (argCount == 0) {
if (name.startsWith(GET_PREFIX)) {
// Simple getter
pd = new PropertyDescriptor(this.beanClass, name.substring(3), method, null);
} else if (resultType == boolean.class && name.startsWith(IS_PREFIX)) {
// Boolean getter
pd = new PropertyDescriptor(this.beanClass, name.substring(2), method, null);
}
}
//...
}
}
从这里就可以看到,如果属性是Boolean类型,其read方法需要以getXXX命名,而对于boolean类型,需要以isXX命名
根据class获取对应的属性信息之后,会缓存在propertyDescriptorCache里,debug可以看到StudentEntity的isGraduated字段对应的readMethod为空(Student的isGraduated字段对应的readMethod也为空)
总结
BeanUtils.copyProperties主要是通过反射实现的浅拷贝,如果对象都是单一的属性或者子对象不涉及到改动,可以BeanUtils进行拷贝
在拷贝的时候注意对于Boolean类型字段,其readMethod需要以getXXX来命名