BeanUtils.copyProperties 方法在项目中的使用非常频繁,但我们对它知之甚少,在一次使用中,我遇到了下面的这种情况,直接上代码:
public class ParentSrc {
private String attr;
public String getAttr() {
return attr;
}
public void setAttr(String attr) {
this.attr = attr;
}
}
public class Src extends ParentSrc {
private String subAttr;
public String getSubAttr() {
return subAttr;
}
public void setSubAttr(String subAttr) {
this.subAttr = subAttr;
}
}
public class ParentTarget {
private String attr;
public String getAttr() {
return attr;
}
public void setAttr(String attr) {
this.attr = attr;
}
}
public class Target extends ParentTarget {
private String subAttr;
public String getSubAttr() {
return subAttr;
}
public void setSubAttr(String subAttr) {
this.subAttr = subAttr;
}
@Override
public String toString() {
return "Target{" +
"attr='" + getAttr() + '\'' +","+
"subAttr='" + subAttr + '\'' +
'}';
}
}
public class Test {
public static void main(String[] args) {
Src src=new Src();
src.setSubAttr("subAttr");
src.setAttr("attr");
Target target = new Target();
BeanUtils.copyProperties(src,target);
System.out.println(target);
}
}
代码中我们可以看到,我将src的属性复制给target 。那么 src 的父类 ParentSrc 的属性 attr 能否复制给 target 的父类 ParentTarget 呢 ?答案是可以。但我的第一反应是不确定,所以我决定看一下它的源码是如何实现的,直接看 BeanUtils 中的源码 :
源码中我们可以看到,editable 和 ignoreProperties 为空,直接忽略。重点看356行 getPropertyDescriptors 方法:
上图中可以看到会通过target的Class属性构造一个 CachedIntrospectionResults 类,然后获取其 PropertyDescriptor的值,我们直接看 forClass 方法:
上图中可以看到第一次通过target的class获取的 results 肯定为空,所以我们直接看重点,第70行的代码如何构造 CachedIntrospectionResults :
上图中,我们先忽略141-143行的日志打印代码,重点看145行的 getBeanInfo 方法:
上图中的这段代码,我们需要详细解释下,首先我们看下 beanInfoFactories 是如何进行初始化的:
这里我们不详细解释它的初始化过程,重点代码我已经红框圈出来,主要就是先通过classpath下的 META-INF/spring.factories 文件获取 org.springframework.beans.BeanInfoFactory 为key的值,如下图:
然后初始化 org.springframework.beans.ExtendedBeanInfoFactory 这个类,将其放入 beanInfoFactories 中,所以它只有ExtendedBeanInfoFactory 这一个类,所以我们继续往下看 getBeanInfo 方法:
133行中可以看到会进入 ExtendedBeanInfoFactory 的 getBeanInfo 方法:
我们继续看 supports 方法:
这里会获取target的所有方法,去30行判断返回什么,直接看 ExtendedBeanInfo.isCandidateWriteMethod 方法:
重点在94的判断,这里首先需要方法名称的长度大于3 且 方法名称是以set开头 且 (方法的返回类型是非void的 或 方法是静态的),到这里就很明显了,我们通常的实体类中的set方法的返回类型一定是void的和非静态的,所以 ExtendedBeanInfo.isCandidateWriteMethod 会返回false, 继续往下看 supports 也会返回 false ,所以 ExtendedBeanInfoFactory 的 getBeanInfo 方法返回空。所以我们继续往下看上图中的 CachedIntrospectionResults 的 getBeanInfo 方法:
第一次循环结束, beanInfo == null 为true,所以进入while中的第二次循环,128行中可以看到 var1.hasNext() 已经没有下一个了,所以返回false。继续看 shouldIntrospectorIgnoreBeaninfoClasses 属性的初始化过程:
这里不详细解释,主要是从classpath下的 spring.properties 文件中获取 spring.beaninfo.ignore 这个key的值,通常 spring.properties 这个文件不存在,所以 shouldIntrospectorIgnoreBeaninfoClasses 属性默认为 false,所以继续往下走:
直接查看 Introspector.getBeanInfo 方法:
上图中可以看到 首先 ReflectUtil.isPackageAccessible 返回true, 继续往下走,第一次通过beanClass 获取 beanInfo 肯定为空,所以直接看173行的重点代码,首先看 Introspector的初始化过程:
这里的 stopClass为null ,flag 为 USE_ALL_BEANINFO ,继续看397-399行的代码,不多解释, explicitBeanInfo 最后获取的为 null。重点看401-407行的代码,首先会获取target的父类 superClass ,然后会将superClass 作为参数传进去通过 getBeanInfo 方法获取对应的beanInfo 信息赋值给 superBeanInfo 。这里就是为什么src的父类属性可以复制给 target 的父类的原因。
我们继续往下看 Introspector 的 getBeanInfo() 方法:
这里我们重点看428行的 getTargetPropertyInfo 方法,为什么?看下图:
由上文和上图中的151行我们可以推断出:BeanUtils.copyProperties 就是使用 PropertyDescriptor 这个属性来完成2个实体类之间的属性复制。
我们继续看getTargetPropertyInfo 方法 :
上文我们已经分析过了 explicitBeanInfo 为null,所以直接往下看 465-467行的代码,由上文分析,这里 superBeanInfo 不为null, 所以这里会将父类的属性也一并加入到子类中。继续往下看这里的 additionalBeanInfo 和 explicitProperties 都为空,直接看483行以后的代码:
上图中可以看到首先会获取beanClass的所有public方法,然后会过滤掉静态方法和方法名称长度小于3且方法名称不是以is开头的method。继续往下看,重点在512行,这是最常用到的代码。它首先判断方法参数的个数为0(也就是没有方法参数),然后判断方法名称是以get开头的,就会构造一个 PropertyDescriptor 类,我们来看一下他的属性:
上图中可以看到主要构造了5个属性,以本文的demo为例:
Class0 : Target、name : subAttr、readMethod : getSubAttr、writeMethod : null、baseName : SubAttr
Class0为当前目标类Target,name为方法名称subAttr,readMethod为目标类的获取属性值方法getSubAttr,baseName为首字母大写的方法名称SubAttr,由于是get方法开头的,所以WriteMethod为空。
我们继续看上图中的 getTargetPropertyInfo 方法的515行,若属性为布尔类型的,方法名称就会从第2位开始截取。
继续往下看:
当方法参数个数为1时,我们重点看520-522行,这是我们最经常用到的代码。首先判断方法的返回类型为 void 且方法名称是set开头的,就会构造一个专属set方法的 PropertyDescriptor ,它的5个属性为:
Class0 : Target、name : subAttr、readMethod : null、writeMethod : setSubAttr、baseName : SubAttr
可以看出来,它与get方法构造的PropertyDescriptor基本一样,就是readMethod 和 writeMethod 分别对应get 和 set 方法而已。527-534行的代码基本很少用到,这里我们直接跳过,继续往下看:
我们重点先看549行的 addPropertyDescriptor 方法:
上图中可以看到,首先会从 pdStore 中通过属性名称获取 PropertyDescriptor 的集合,若集合为空,则直接创建一个新的集合,将其放入pdStore中。这里主要用来属性方法名称相同的 PropertyDescriptor ,通常这里存储的就是get和set方法的属性集合。继续往下看,由于beanClass 等于 class0 ,所以583-607行的代码不做分析。那么最后 pdStore 中存储的key是方法名称propName ,value 是长度为二的PropertyDescriptor的集合。这里的addPropertyDescriptor方法就分析结束。
继续往下看processPropertyDescriptors 方法:
这个方法的代码很长,我们挑红框里重点的说,其中有2个for循环,其中会将变量 gpd 对应 ReadMethod 属性描述,spd 对应 WriteMethod 属性描述,继续往下看:
这里是将ReadMethod 和 WriteMethod 合并为一个 PropertyDescriptor,继续往下看:
最后将所有的属性以属性名称为key,PropertyDescriptor 为value放入 properties 中。
我们继续回到 getTargetPropertyInfo 方法,往下看:
最后将 properties 的value 作为数组赋值给 result 返回。到这里我们的 getBeanInfo 的方法就分析结束了,我们继续看CachedIntrospectionResults 的构造方法:
我们重点看161和162行代码,首先是beanClass!=Class的,所以条件判断为true。
我们继续看 buildGenericTypeAwarePropertyDescriptor 方法:
这里是将 PropertyDescriptor 这个类封装为它的子类 GenericTypeAwarePropertyDescriptor。
然后将其放入 propertyDescriptorCache 中,到这里CachedIntrospectionResults的构造方法已经分析完成。
我们继续看CachedIntrospectionResults.forClass 方法:
这里的72行首先判断当前beanClass 是否和 CachedIntrospectionResults 使用同一个类加载器加载的,这里通常都是使用AppClassLoader 进行加载的,所以 if 判断为false , classCacheToUse 为 strongClassCache,最后将beanClass 和 results 关联起来做缓存,便于在下一次同样的类做属性复制时,可以直接从strongClassCache的缓存中取。
到这里CachedIntrospectionResults.forClass就分析完成,继续看 getPropertyDescriptors 方法:
这段代码很简单,不多做解释,主要是将propertyDescriptorCache 中的value转存储到 PropertyDescriptor 数组 pds中,然后返回。到这里我们的准备工作基本完成,我们最后回到 BeanUtils.copyProperties 方法中:
可以看到,这里首先 targetPds 这个变量接受返回的pds数组,然后对targetPds数组进行遍历。
这里我们重点看365行的 getPropertyDescriptor 方法:
上图中可以看到它和356行的 getPropertyDescriptors 方法基本一样,主要功能就是通过获取source class的 PropertyDescriptor ,然后通过目标类的属性名获取对应的 PropertyDescriptor,这样就可以获取到对应属性的readMethod 。在本文的demo中,source class 为Src , 目标类的属性名subAttr。
我们继续往下看368行,这里会判断目标类写方法的参数类型是否和源类的读方法的返回类型相同或者为其父类,在本demo中writeMethod.getParameterTypes()[0] 为 Target 中 setSubAttr 方法的参数类型String, readMethod.getReturnType() 为 Src类中getSubAttr 方法的返回类型String,所以它们是相同的ClassUtils.isAssignable 的判断为true。继续往下看读方法的声明类标识符通常是 public的,375行的写方法的声明类的标识符通常也是public。
核心在374行和379行的代码, readMethod 通过反射调用Src 类的getSubAttr 方法获取其对应的属性值value(在本demo中value为subAttr),最后writeMethod 通过反射调用 Target类中的 setSubAttr 方法成功将Src的属性值复制给 subAttr属性。
到这里 BeanUtils.copyProperties 方法的源码就解析完成。
1. 看完整个 BeanUtils.copyProperties 方法,去掉表层的封装和缓存优化后,核心就是先通过反射获取 Target 目标类的属性集合,然后遍历属性集合通过属性名称获取 Src 源类中的属性描述器,反射调用将值复制给目标类中的属性。
2. 父类的属性在 Introspector 构造类中会获取父类的实体信息 superBeanInfo ,最后会将superBeanInfo中的属性描述器加入到子类中,最后统一遍历完成复制。