BeanUtils对象属性copy的性能对比以及源码分析
1. 对象属性拷贝的常见方式及其性能
在日常编码中,经常会遇到DO、DTO对象之间的转换,如果对象本身的属性比较少的时候,那么我们采用硬编码手工setter也还ok,但如果对象的属性比较多的情况下,手工setter就显得又low又效率又低。这个时候我们就考虑采用一些工具类来进行对象属性的拷贝了。
我们常用的对象属性拷贝的方式有:
Hard Code
net.sf.cglib.beans.BeanCopier#copy
org.springframework.beans.BeanUtils.copyProperties
org.apache.commons.beanutils.PropertyUtils.copyProperties
org.apache.commons.beanutils.BeanUtils.copyProperties
针对以上的拷贝方式,我做了一个简单的性能测试,结果如下:
拷贝方式 | 对象数量: 1 | 对象数量: 1000 | 对象数量: 100000 | 对象数量: 1000000 |
---|---|---|---|---|
Hard Code | 0 ms | 1 ms | 18 ms | 43 ms |
cglib.BeanCopier | 111 ms | 117 ms | 107 ms | 110 ms |
spring.BeanUtils | 116 ms | 137 ms | 246 ms | 895 ms |
apache.PropertyUtils | 167 ms | 212 ms | 601 ms | 7869 ms |
apache.BeanUtils | 167 ms | 275 ms | 1732 ms | 12380 ms |
测试环境:OS=
macOS 10.14
, CPU=2.5 GHz,Intel Core I7
, Memory=16 GB, 2133MHz LPDDR3
测试方法:通过copy指定数量的复杂对象,分别执行每个Case
10
次,取其平均值
版本:commons-beanutils:commons-beanutils:1.9.3
,org.springframework:spring-beans:4.3.5.RELEASE
,cglib:cglib:2.2.2
结论:从测试结果中很明显可以看出采用Hard Code
方式进行对象属性Copy性能最佳;采用net.sf.cglib.beans.BeanCopier#copy
方式进行对象属性copy性能最稳定;而org.apache.commons.beanutils.BeanUtils.copyProperties
方式在数据量大时性能下降最厉害。所以在日常编程中遇到具有较多属性的对象进行属性复制时优先考虑采用net.sf.cglib.beans.BeanCopier#copy
。
以上的数据之所产生巨大差距的原因在于其实现原理与方式的不同而导致的,Hard Code直接调用getter & setter
方法值,cglib
采用的是字节码技术
,而后三种均采用反射
的方式。前两者性能优异众所周知,但为何同样采用反射的方式进行属性Copy时产生的差异如此巨大呢? 这正是本文我们想要去探究的内容。
我们首先解读
org.apache.commons.beanutils.BeanUtils
的源码,其次解读org.springframework.beans.BeanUtils
源码,最后通过它们各自实现方式来进行论证性能差异
apache.BeanUtils
与spring.BeanUtils
均采用反射技术实现,也都调用了Java关于反射的高级API——Introspector
(内省),因此我们首先要了解Introspector
是什么.
2. Introspector
Introspector(内省)
是jdk提供的用于描述Java bean
支持的属性、方法以及事件的工具;利用此类可得到BeanInfo
接口的实现对象,BeanInfo
接口中有两个重要的方法:
BeanDescriptor getBeanDescriptor();
,BeanDescriptor
提供了java bean的一些全局的信息,如class类型、类名称等PropertyDescriptor[] getPropertyDescriptors()
PropertyDescriptor
描述了java bean中一个属性并导出了他们的getter & setter
方法的SoftReference
Jdk的内省接口极大的简化了反射类信息的方式,通过这组api我们可以很方便进行java bean的反射调用。本组api采用软引用、虚引用来充分利用了空闲的内存;在某些地方(如declaredMethodCache
)采用缓存来加速api的执行效率,并且此组api是线程安全的。
使用方式:
BeanInfo beanInfo = Introspector.getBeanInfo(icontext.getTargetClass()); PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); for(PropertyDescriptor descriptor: descriptors) { Method readMethod = descriptor.getReadMethod(); Method writeMethod = descriptot.getWriteMethod(); // readMethod.invoke(...); }
以上就是关于Introspector
的简单了解,接下来我们先来看apache.BeanUtils
的源码.
3. 源码:apache.BeanUtils
apache.BeanUtils
是一个包含了很多静态方法的工具类,而几乎所有的静态方法均是BeanUtilsBean
的单例对象提供的实现。BeanUtilsBean
是进行JavaBean属性操作的入口方法,它以单实例对外提供功能。但这里有一个不同于普通单例的地方:不同的类加载器拥有不同的实例,每一个类加载器只有一个实例 ,所以这里的单例其实是一个伪单例pseudo-singletion 。
// ContextClassLoaderLocal对象管理了BeanUtilsBean的所有实例 private static final ContextClassLoaderLocal<BeanUtilsBean> BEANS_BY_CLASSLOADER = new ContextClassLoaderLocal<BeanUtilsBean>() { @Override protected BeanUtilsBean initialValue() { return new BeanUtilsBean(); } }; public static BeanUtilsBean getInstance() { return BEANS_BY_CLASSLOADER.get(); } // {@link ContextClassLoaderLocal#get} public synchronized T get() { valueByClassLoader.isEmpty(); try { final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); // 获取当前线程的类加载器 if (contextClassLoader != null) { T value = valueByClassLoader.get(contextClassLoader); if ((value == null) && !valueByClassLoader.containsKey(contextClassLoader)) { value = initialValue(); // 初始化BeanUtilsBean,即 new BeanUtilsBean(); valueByClassLoader.put(contextClassLoader, value); } return value; } } catch (final SecurityException e) { /* SWALLOW - should we log this? */ } if (!globalValueInitialized) { globalValue = initialValue(); globalValueInitialized = true; } return globalValue; }
当获取到了BeanUtilsBean
的实例之后,接下来就是我们进行对象属性拷贝的时候了.
// omit exception public static void copyProperties(final Object dest, final Object orig){ BeanUtilsBean.getInstance().copyProperties(dest, orig); }
在copyProperties
方法中,针对原始对象的类型分别采用了不同的逻辑:
Map
: 通过Map的Key与dest中的属性进行匹配,然后赋值;DynaBean
:DynaBean
顾名思义,它是一种可以形成动态java bean的对象,也就是说它内部会存储属性名称、类型以及对应的值,在copy属性时也是将其内部的属性名称与dest对象的属性名称对应后赋值;标准Java Bean
:这个是我们主要进行分析的类型,它是标准的JavaBean对象;与前两者的差异只是在于对原始bean的取值的处理上.
3.1 针对标准JavaBean进行属性copy时的步骤
public void copyProperties(final Object dest, final Object orig) { // omit some code (省略一部分代码) ... 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 } } } }
- 根据原始bean的类型解析、缓存其
PropertyDescriptor
- 轮询原始bean的每一个
PropertyDescriptor
,判断PropertyDescriptor
在原始bean中是否可读、在目标bean中是否可写,只有这两个条件都成立时才具备copy的资格 - 根据
PropertyDescriptor
从原始bean中获取对应的值,将值copy至目标bean的对应属性上
3.2 获取Bean的PropertyDescriptor
final PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig);
获取PropertyDescriptor
委托给PropertyUtilsBean
对象来实现:
public BeanUtilsBean() { this(new ConvertUtilsBean(), new PropertyUtilsBean()); }
PropertyUtilsBean
是用于使用java 反射API来操作Java Bean上getter和setter方法的,此类中的代码原先是位于BeanUtilsBean
中的,但是考虑到代码量的原因进行了分离(Much of this code was originally included in BeanUtils, but has been separated because of the volume of code involved
)。
在PropertyUtilsBean
中,每个Bean的PropertyDescriptor
会存储于BeanIntrospectionData
对象中,当每次需要获取PropertyDescriptor
时,会先从cahche中获取BeanIntrospectionData
;如果不存在,则通过内省API获取BeanIntrospectionData
并将其置于缓存中:
private BeanIntrospectionData getIntrospectionData(final Class<?> beanClass) { // omit some check code ... BeanIntrospectionData data = descriptorsCache.get(beanClass); if (data == null) { data = fetchIntrospectionData(beanClass); descriptorsCache.put(beanClass, data); } return data; } private BeanIntrospectionData fetchIntrospectionData(final Class<?> beanClass) { final DefaultIntrospectionContext ictx = new DefaultIntrospectionContext(beanClass); for (final BeanIntrospector bi : introspectors) { try { bi.introspect(ictx); } catch (final IntrospectionException iex) { log.error("Exception during introspection", iex); } } return new BeanIntrospectionData(ictx.getPropertyDescriptors()); }
在fetchIntrospectionData()
方法中,通过内置的内省器DefaultBeanIntrospector
使用java的内省API将获取的信息传递给DefaultIntrospectionContext
, 在通过DefaultIntrospectionContext
构造BeanIntrospectionData
。DefaultBeanIntrospector
具体的代码:
public void introspect(final IntrospectionContext icontext) { BeanInfo beanInfo = null; try { // JAVA 的 Instrospector beanInfo = Introspector.getBeanInfo(icontext.getTargetClass()); } catch (final IntrospectionException e) { return; } PropertyDescriptor[] descriptors = beanInfo.getPropertyDescriptors(); if (descriptors == null) { descriptors = new PropertyDescriptor[0]; } // 解决IndexedPropertyDescriptor在不同版本的JDK下的差异 handleIndexedPropertyDescriptors(icontext.getTargetClass(), descriptors); icontext.addPropertyDescriptors(descriptors); }
3.3 判断属性是否可读/可写
要进行属性copy,那么首先得确保原始对象的属性可读、目标对象属性可写。在PropertyUtilsBean
中通过isWriteable(); isReadable()
方法,这两个方法看上去比较长,我们把关于exception的处理省略掉拿出来看下:
public boolean isReadable(Object bean, String name) { // Omit Validate method parameters // Resolve nested references, 解析内嵌的属性,形如 student.name while (resolver.hasNested(name)) { final String next = resolver.next(name); Object nestedBean = nestedBean = getProperty(bean, next); if (nestedBean == null) { throw new NestedNullException("Null property value for); } bean = nestedBean; name = resolver.remove(name); } // Remove any subscript from the final name value, 在最终的方法名中移除所有的下标 name = resolver.getProperty(name); if (bean instanceof WrapDynaBean) { bean = ((WrapDynaBean)bean).getInstance(); } if (bean instanceof DynaBean) { // All DynaBean properties are readable,所有DynaBean的属性均是可读的 return (((DynaBean) bean).getDynaClass().getDynaProperty(name) != null); } else { final PropertyDescriptor desc = getPropertyDescriptor(bean, name); if (desc != null) { Method readMethod = getReadMethod(bean.getClass(), desc); if (readMethod == null) { if (desc instanceof IndexedPropertyDescriptor) { readMethod = ((IndexedPropertyDescriptor) desc).getIndexedReadMethod(); } else if (desc instanceof MappedPropertyDescriptor) { readMethod = ((MappedPropertyDescriptor) desc).getMappedReadMethod(); } readMethod = MethodUtils.getAccessibleMethod(bean.getClass(), readMethod); } return (readMethod != null); } else { return (false); } } }
从以上代码我们可以得知,每个属性的可读、可写在每次使用时都需要获取Method,然后进行判断,并且还需要处理DynaBean、Nested的逻辑;当我们进行批量的属性copy时,依然需要执行以上步骤,并未将method的判断结果进行缓存,这也是其相比于其他的jar低效的原因.
3.4 读取原始Bean的属性值、设置目标Bean的属性值
我们还是省略掉其中的有效性判断和异常的代码:
public Object getSimpleProperty(final Object bean, final String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException { // omit check null code ... // 校验属性 if (resolver.hasNested(name)) { throw new IllegalArgumentException ("Nested property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } else if (resolver.isIndexed(name)) { throw new IllegalArgumentException ("Indexed property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } else if (resolver.isMapped(name)) { throw new IllegalArgumentException ("Mapped property names are not allowed: Property '" + name + "' on bean class '" + bean.getClass() + "'"); } // DynaBean的特殊逻辑 if (bean instanceof DynaBean) { final DynaProperty descriptor = ((DynaBean) bean).getDynaClass().getDynaProperty(name); if (descriptor == null) { throw new NoSuchMethodException("Unknown property '" + name + "' on dynaclass '" + ((DynaBean) bean).getDynaClass() + "'" ); } return (((DynaBean) bean).get(name)); } final PropertyDescriptor descriptor = getPropertyDescriptor(bean, name); if (descriptor == null) { throw new NoSuchMethodException("Unknown property '" + name + "' on class '" + bean.getClass() + "'" ); } // 获取getter方法 final Method readMethod = getReadMethod(bean.getClass(), descriptor); if (readMethod == null) { throw new NoSuchMethodException("Property '" + name + "' has no getter method in class '" + bean.getClass() + "'"); } // 调用getter方法读取值 final Object value = invokeMethod(readMethod, bean, EMPTY_OBJECT_ARRAY); return (value); }
以上是读取属性值的方法。 读取到属性值之后,就是设置值到目标bean上了。 在BeanUtilsBean
的实现中,又重复的处理了属性的内嵌逻辑与DynaBean逻辑,最终获取到其setter方法将值赋予目标Bean.
4. 源码: spring.BeanUtils
BeanUtils
位于spring-beans
模块中,暴露出静态方法copyProperties
用以进行属性copy,每个copyProperties
最终均调用一个私有静态方法实现属性copy:
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties){ 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; } // 第一步 调用Java 内省API 获取PropertyDescriptor PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable); List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); // 第二步 轮询目标bean的PropertyDescriptor for (PropertyDescriptor targetPd : targetPds) { Method writeMethod = targetPd.getWriteMethod(); // 判断是否存在setter方法以及属性是否在需要忽略的属性列表中 if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { // 获取源bean的PropertyDescriptor PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); if (sourcePd != null) { // 获取getter方法 Method readMethod = sourcePd.getReadMethod(); if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) { try { // 如果getter方法不是public,则需要设置其accessible if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } // 反射获取属性值 Object value = readMethod.invoke(source); // 如果setter方法不是public则需要设置其accessible 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); } } } } } }
4.1 获取Bean的PropertyDescriptor
spring.BeanUtils
中对于bean的PropertyDescriptor
处理以及缓存均是由CachedIntrospectionResults
类来进行处理。 CacheIntrospectionResults
将数据缓存在静态集合中,使用了工厂方法的设计模式,通过forClass(Class)
方法暴露缓存:
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException { // 从缓存中获取CachedIntrospectionResults CachedIntrospectionResults results = strongClassCache.get(beanClass); if (results != null) { return results; } // 从缓存中获取CachedIntrospectionResults results = softClassCache.get(beanClass); if (results != null) { return results; } // 构造CachedIntrospectionResults results = new CachedIntrospectionResults(beanClass); ConcurrentMap<Class<?>, CachedIntrospectionResults> classCacheToUse; // 选取对应的缓存 if (ClassUtils.isCacheSafe(beanClass, CachedIntrospectionResults.class.getClassLoader()) || isClassLoaderAccepted(beanClass.getClassLoader())) { classCacheToUse = strongClassCache; } else { if (logger.isDebugEnabled()) { logger.debug("Not strongly caching class [" + beanClass.getName() + "] because it is not cache-safe"); } classCacheToUse = softClassCache; } CachedIntrospectionResults existing = classCacheToUse.putIfAbsent(beanClass, results); return (existing != null ? existing : results); }
我们可以看到此处具有两个缓存:strongClassCache
与softClassCache
,那他俩什么区别呢?
首先我们看他们的定义:
static final ConcurrentMap<Class<?>, CachedIntrospectionResults> strongClassCache = new ConcurrentHashMap<Class<?>, CachedIntrospectionResults>(64); static final ConcurrentMap<Class<?>, CachedIntrospectionResults> softClassCache = new ConcurrentReferenceHashMap<Class<?>, CachedIntrospectionResults>(64);
ConcurrentReferenceHashMap可以指定对应的引用级别,其内部采用分段锁实现,与jdk1.7的ConcurrentMap的实现原理类似。
strongClassCache
中持有的缓存是强引用
,而softClassCache
持有的缓存是软引用
(JDK有4中引用级别,分别是强引用,软引用,弱引用以及虚引用,引用级别体现在决定GC的时候持有的实例被回收的时机)。
strongClassCache
用于缓存cache-safe
的bean class数据,而softClassCache
用于缓存none-cache-safe
bean class数据;strongClassCache
中的数据与spring application的生命周期一致,而softClassCache
的生命周期则不由spring进行管理,因此为了防止因classloader提前关闭导致内存泄漏
,此处采用软引用进行缓存.
那什么样的数据会被cache在strongClassCache
中呢?beanClass的ClassLoader与当前相同时或者与程序指定的ClassLoader相同时会被存储于strongClassCache
,其余均为存储于softClassCache
中。
如果从以上cache中没有拿到数据,那么会new CachedIntrospectionResults(Class)
,相应的调用Java Introspector的相关API均在此构造函数中:
// omit some logger code ... private CachedIntrospectionResults(Class<?> beanClass) throws BeansException { try { BeanInfo beanInfo = null; // 对一些特殊的set方法(setA(int index, Object a))或者list的set方法进行处理 for (BeanInfoFactory beanInfoFactory : beanInfoFactories) { beanInfo = beanInfoFactory.getBeanInfo(beanClass); if (beanInfo != null) { break; } } if (beanInfo == null) { // fall back到默认获取BeanInfo的方式 beanInfo = (shouldIntrospectorIgnoreBeaninfoClasses ? Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : Introspector.getBeanInfo(beanClass)); } this.beanInfo = beanInfo; // propertyDescriptor缓存 this.propertyDescriptorCache = new LinkedHashMap<String, PropertyDescriptor>(); // 考虑到性能原因,对于每个PropertyDescriptor只处理一次 PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : pds) { if (Class.class == beanClass && ("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()))) { continue; } // 重新包装为GenericTypeAwarePropertyDescriptor pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); this.propertyDescriptorCache.put(pd.getName(), pd); } // 检查Java 8在接口中的默认实现方法 Class<?> clazz = beanClass; while (clazz != null) { Class<?>[] ifcs = clazz.getInterfaces(); for (Class<?> ifc : ifcs) { BeanInfo ifcInfo = Introspector.getBeanInfo(ifc, Introspector.IGNORE_ALL_BEANINFO); PropertyDescriptor[] ifcPds = ifcInfo.getPropertyDescriptors(); for (PropertyDescriptor pd : ifcPds) { if (!this.propertyDescriptorCache.containsKey(pd.getName())) { pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd); this.propertyDescriptorCache.put(pd.getName(), pd); } } } clazz = clazz.getSuperclass(); } this.typeDescriptorCache = new ConcurrentReferenceHashMap<PropertyDescriptor, TypeDescriptor>(); } catch (IntrospectionException ex) { throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex); } }
这段代码主要的作用就是通过内省接口得到BeanInfo,然后将PropertyDescriptor
缓存起来。具体流程:
- 首先通过BeanInfoFactory获取BeanInfo; 这里默认注册时
BeanInfoFactory
是ExtendedBeanInfoFactory
, 此类主要处理包含一些特殊set
方法的bean:
public static boolean isCandidateWriteMethod(Method method) { String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); int nParams = parameterTypes.length; return (methodName.length() > 3 && methodName.startsWith("set") && Modifier.isPublic(method.getModifiers()) && (!void.class.isAssignableFrom(method.getReturnType()) || Modifier.isStatic(method.getModifiers())) && (nParams == 1 || (nParams == 2 && int.class == parameterTypes[0]))); }
如果一个bean中包含这么一个方法:以set
开头 &&(返回值不为void
|| 是静态方法) && (具有一个参数 || 有两个参数其中第一个参数是int
), 形如:
// void.class.isAssignableFrom(method.getReturnType()) 方法返回值不为void public Bean setFoo(Foo foo) { this.foo = foo; return this; } public static void setFoos(Foo foo) { Bean.foo = foo; } public Bean setFoos(int index, Foo foo) { this.foos.set(index, foo); return this; }
如果该bean不包含以上的方法,则直接采用Java的内省API获取
BeanInfo
当获取到
BeanInfo
之后就可以对PropertyDescriptor
进行缓存了;这里会将PropertyDescriptor
重新包装为GenericTypeAwarePropertyDescriptor
, 进行这样封装的原因是为了重新处理BridgeMethod, 通俗点讲,就是处理当前类继承了泛型类或者实现泛型接口,那怎么识别这些方法呢?Bridge Method
: 桥接方法是jdk引入泛型后为了与之前的jdk版本兼容,在编译时自动生成的方法。桥接方法的字节码Flag会被标记为ACC_BRIDGE
(桥接方法)和ACC_SYNTHETIC
(由编译器生成)。通过Method.isBridge()
来判断一个方法是否为BridgeMethod
。如果一个方法覆写了泛型父类或者实现了泛型接口则会生成Bridge Method
.
public static Method findBridgedMethod(Method bridgeMethod) { if (bridgeMethod == null || !bridgeMethod.isBridge()) { return bridgeMethod; } // 获取所有与bridgeMethod名称、参数数量相匹配的方法(包括父类) List<Method> candidateMethods = new ArrayList<Method>(); Method[] methods = ReflectionUtils.getAllDeclaredMethods(bridgeMethod.getDeclaringClass()); for (Method candidateMethod : methods) { // candidateMethod是`Bridge Method`时将其加入候选方法列表 if (isBridgedCandidateFor(candidateMethod, bridgeMethod)) { candidateMethods.add(candidateMethod); } } if (candidateMethods.size() == 1) { return candidateMethods.get(0); } // 在众候选方法中找到其BridgeMethod,如果找不到返回原方法 Method bridgedMethod = searchCandidates(candidateMethods, bridgeMethod); if (bridgedMethod != null) { return bridgedMethod; }else { return bridgeMethod; } }
- 处理完类中的方法,就要处理接口中实现的方法了。 在Java8中,接口是可以有默认的方法的,举个例子:
public interface MethodAvailable { default String getHello(){ return "hello"; } String setHello(String hello); }
对于接口中实现的方法的处理逻辑与类中实现方法的处理逻辑一致。
当进行完以上步骤后,我们就拿到了缓存有内省结果的CachedIntrospectionResults
实例,然后选取对应的cahche,将结果缓存起来。(选取cahce的过程与前文读取cache的过程一致);
4.2 属性值copy
从缓存中获取到了目标类的PropertyDescriptor
后,就要轮询其每一个PropertyDescriptor
赋值了。
赋值的过程相对比较简单一点:
- 获取目标类的写方法(
setter
) - 如果目标类的写方法不为空且此方法对应的属性并不在配置的
igonreList
(忽略属性列表)中,则获取源类对应属性的读方法(getter
) - 获取到读方法之后,需要判断读方法的返回值是否与写方法的参数是同一个类型,不同类型当然无法copy了
- 判断读方法是否
public
,如果不是,则需要设置访问权限method.setAccessible(true);
(非public方法在反射访问时需要设置setAccessible(true)
获取访问权限),然后调用反射执行此方法,invoke(source)
; - 判断写方法是否
public
,如果不是则设置访问权限,然后将读到的值,通过放射赋给目标类invoke(taget, value)
;
至此,类的属性copy完成。
5. 总结
5.1 spring.BeanUtils与apache.BeanUtils的性能差异原因
在大数量copy时,apache.BeanUtils
相比于spring.BeanUtils
慢了近14
倍,究其原因,其实在于以下几点:
apache.BeanUtils
在实现了对每个类加载器缓存了一份BeanUtilsBean
的实例,在获取此实例时会加锁(synchronized
)apache.BeanUtils
支持了DynaBean
和Map
映射到Object的能力,但其在后期对于PropertyDescriptor
处理时,即使我采用的是简单的Object,也会去判断DynaBean
和Map
,此处如果采用策略模式将其分离应该会减少很多判断的时间apache.BeanUtils
在每次执行属性copy时,会重新从PropertyDescriptor
获取读写方法,虽然对PropertyDescriptor
进行了缓存,但每次获取readMethod/writeMethod
也是非常耗时的尤其是在对象实例数量较多时,此处如果对于readMethod/writeMethod
进行缓存,性能应该会提升很多- 反观
spring.BeanUtils
之所以比apache.BeanUtils
快,就是其对PropertyDescriptor
只处理一次后缓存。 相比之下可见对于PropertyDescriptor
的处理是非常耗时的。
5.2 收获
通过此次探究,了解到了以下的知识点:
Java Introspectpr
, 在之前用到反射的时候,都是采用比较原始的方法去获取信息然后缓存再Map中;这样的弊端就是在不同的模块都需要反射的时候,如果因沟通不畅导致另一个人也通过原始的反射接口获取类信息时,是无法利用的缓存的;采用内省的话,jdk默认会进行缓存。Bridge Method
, 之前对泛型擦除的理解只停留在编译期会进行泛型擦除,了解了Bridge Method
后,对于泛型的机制也有了更多的理解- 属性copy时各方式的使用场景:
- 对性能要求较高的时候,推荐采用手工方法调用
- 一般场景推荐使用
net.sf.cglib.beans.BeanCopier#copy
- 如果考虑到引入新jar包的风险时,推荐使用
org.springframework.beans.BeanUtils.copyProperties