很多项目中都使用了VO、DTO、DO、PO等模型设计,在每一层进行参数传递之后,免不了会进行大量的对象属性之间的拷贝,此时我们会使用到BeanUtils这种工具类,使用copyProperties进行便捷的拷贝,代码如下:
实体类定义
package com.brianxia.reflection.instropector;
/**
* @author brianxia
* @version 1.0
* @date 2021/3/17 19:28
*/
public class Source {
private String name;
private int age;
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
package com.brianxia.reflection.instropector;
/**
* @author brianxia
* @version 1.0
* @date 2021/3/17 19:28
*/
public class Target {
private String name;
private long age;
public long getAge() {
return age;
}
public void setAge(long age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Target{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
引入相应的依赖
这里我们选择使用Spring和commons提供的BeanUtil
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>5.2.13.RELEASE</version>
</dependency>
<dependency>
<groupId>commons-beanutils</groupId>
<artifactId>commons-beanutils</artifactId>
<version>1.9.3</version>
</dependency>
</dependencies>
方法调用测试
Source source = new Source();
source.setName("张三");
source.setAge(18);
Target target = new Target();
//Spring
BeanUtils.copyProperties(source,target);
//commons
BeanUtils.copyProperties(target,source);
源码剖析
-
Spring实现
image.png
这是Spring的方法定义,这里有四个参数,Spring对于复制属性做了一些额外的处理:
source – 源bean
target – 目标bean
editable – 可以限制只拷贝父类或者接口中定义的属性
ignoreProperties – 可变参数列表,排除某些特定的属性
我们来看下Spring是如何实现的:
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;
}
如果传递了editable参数,那么就以editable的Class为准,获取属性。
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
通过内省,获取属性的信息。内省在后文中详细描述。我们详细来看getPropertyDescriptors这个方法:

这里使用了一种
CachedIntrospectionResults
用来缓存曾经使用过的内省之后的数据,否则每次进行copy都需要重新获取属性信息,性能太低,所以使用进行了缓存优化。CachedIntrospectionResults
定义了两种ConcurrentHashMap
存放属性信息:
Spring会先从
strongClassCache
中获取,获取不到再去softClassCache
中获取,如果都没有获取到,则进行创建。创建是在CachedIntrospectionResults
的构造方法中,其实创建的过程就是将目标类的所有属性的PropertyDescriptor
进行了缓存,注意: 如果有父类的话,父类的属性也会缓存起来。然后会将class作为key,将创建的CachedIntrospectionResults
作为value,默认缓存到strongClassCache
属性中(作为强引用)。源类也一样将PropertyDescriptor缓存到CachedIntrospectionResults
中。因此大大提升了性能。
static CachedIntrospectionResults forClass(Class<?> beanClass) throws BeansException {
CachedIntrospectionResults results = strongClassCache.get(beanClass);
if (results != null) {
return results;
}
results = softClassCache.get(beanClass);
if (results != null) {
return results;
}
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);
}
这里有一个方法需要注意:ClassUtils.isCacheSafe
,这个方法会检查给定的beanClass是否由给定的classloader或者此classloader的祖先加载的(双亲委派的原理)。
所以第一次加载同一个类的属性会比较慢,后续使用缓存就不用重复加载了。
回到拷贝代码部分,接下来就是比较常规的使用属性信息获取Read和Write方法,其实也就是获取setter和getter方法,使用反射进行调用:
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);
}
}
}
}
}
这里Spring对于非public的方法进行了setAccessible
处理,使之有处理权限。但是Spring比较大的问题是装箱类型Long
和Integer
等互相转换无法做到:
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())
这个方法会判断set和get的对象是否有继承关系,如果没有继承关系,就直接返回。而方法内部只是简简单单处理了基本数据类型和装箱类型的关系,并未对数据进行特殊的处理。
public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
Assert.notNull(lhsType, "Left-hand side type must not be null");
Assert.notNull(rhsType, "Right-hand side type must not be null");
if (lhsType.isAssignableFrom(rhsType)) {
return true;
} else {
Class resolvedWrapper;
if (lhsType.isPrimitive()) {
resolvedWrapper = (Class)primitiveWrapperTypeMap.get(rhsType);
return lhsType == resolvedWrapper;
} else {
resolvedWrapper = (Class)primitiveTypeToWrapperMap.get(rhsType);
return resolvedWrapper != null && lhsType.isAssignableFrom(resolvedWrapper);
}
}
}
整体来说,Spring对于copyProperties的实现过于简单,仅仅增加了ignore忽略和editable限制父类拷贝等基础功能,但未对复杂的数据类型转换做出特殊处理。接下来我们来看commons的实现。
-
commons实现
从整体结构上来说,commons的实现做了很多的特殊处理提升性能。
image.png
它将数据类型分成了三种,DynaBean(姑且称之为万能Bean,自行查询下资料)、Map、JavaBean。
DynaBean使用较少,我们先说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());
}
}
直接从Map中遍历Entry,然后将Entry的内容写入到目标对象中。而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类似,内部维护了两个WeakFastHashMap
缓存属性信息,WeakFastHashMap
的设计很巧妙,借鉴了CopyOnWrite的思想,查询数据时无需加锁,它会选择clone一份新的数据进行修改,在clone出来的数据上进行修改,然后再替换原来的数据。比如如下代码:
@Override
public V get(final Object key) {
if (fast) {
return (map.get(key));
} else {
synchronized (map) {
return (map.get(key));
}
}
}
@Override
public void putAll(final Map<? extends K, ? extends V> in) {
if (fast) {
synchronized (this) {
final Map<K, V> temp = cloneMap(map);
temp.putAll(in);
map = temp;
}
} else {
synchronized (map) {
map.putAll(in);
}
}
}
commons对于数据类型转换,有专门的函数convertForCopy
来进行处理。
protected Object convert(final Object value, final Class<?> type) {
final Converter converter = getConvertUtils().lookup(type);
if (converter != null) {
log.trace(" USING CONVERTER " + converter);
return converter.convert(type, value);
} else {
return value;
}
}
首先根据数据类型找到对应的converter,比如IntegerConverter[UseDefault=true, UseLocaleFormat=false]
,使用装饰者模式进行增强。根据具体转换出的类型,使用Number进行处理。
@Override
protected <T> T convertToType(final Class<T> targetType, final Object value) throws Throwable {
final Class<?> sourceType = value.getClass();
// Handle Number
if (value instanceof Number) {
return toNumber(sourceType, targetType, (Number)value);
}
// Handle Boolean
if (value instanceof Boolean) {
return toNumber(sourceType, targetType, ((Boolean)value).booleanValue() ? ONE : ZERO);
}
// Handle Date --> Long
if (value instanceof Date && Long.class.equals(targetType)) {
return targetType.cast(new Long(((Date)value).getTime()));
}
// Handle Calendar --> Long
if (value instanceof Calendar && Long.class.equals(targetType)) {
return targetType.cast(new Long(((Calendar)value).getTime().getTime()));
}
// Convert all other types to String & handle
final String stringValue = value.toString().trim();
if (stringValue.length() == 0) {
return handleMissing(targetType);
}
// Convert/Parse a String
Number number = null;
if (useLocaleFormat) {
final NumberFormat format = getFormat();
number = parse(sourceType, targetType, stringValue, format);
} else {
if (log().isDebugEnabled()) {
log().debug(" No NumberFormat, using default conversion");
}
number = toNumber(sourceType, targetType, stringValue);
}
// Ensure the correct number type is returned
return toNumber(sourceType, targetType, number);
}
比如此例中Long
转换成Integer
类型,只需要调用toNumber
即可。针对Long
型进行特殊的处理。
// Long
if (targetType.equals(Long.class)) {
return targetType.cast(new Long(value.longValue()));
}
总结:commons的实现要比Spring功能更加强大,不仅使用了具备COW技术的缓存大大增强并发读取能力,同时对数据转换做了严格的处理。
内省
最后我们来说一下Java中的内省(Introspector)机制。Introspector与反射类似,主要是对Java Bean属性、方法等的一种处理方法。
-
PropertyDescriptor类:
PropertyDescriptor类表示JavaBean类通过存储器导出一个属性。主要方法:
1. getPropertyType(),获得属性的Class对象;
2. getReadMethod(),获得用于读取属性值的方法;getWriteMethod(),获得用于写入属性值的方法;
3. hashCode(),获取对象的哈希值;
4. setReadMethod(Method readMethod),设置用于读取属性值的方法;
5. setWriteMethod(Method writeMethod),设置用于写入属性值的方法。 -
Introspector类:
将JavaBean中的属性封装起来进行操作。在程序把一个类当做JavaBean来看,就是调用Introspector.getBeanInfo()方法,得到的BeanInfo对象封装了把这个类当做JavaBean看的结果信息,即属性的信息。
getPropertyDescriptors(),获得属性的描述,可以采用遍历BeanInfo的方法,来查找、设置类的属性。
手写一个copyProperties
package com.brianxia.reflection.instropector;
import org.springframework.beans.BeanUtils;
import javax.xml.ws.spi.Invoker;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author brianxia
* @version 1.0
* @date 2021/3/17 19:22
*/
public class InstropectorDemo {
public static Map<Class,Map<String,PropertyDescriptor>> sourcePd = new ConcurrentHashMap<>();
public synchronized static void createPd(Object source) throws IntrospectionException {
Class clazz = source.getClass();
if(!sourcePd.containsKey(clazz)){
sourcePd.put(clazz,new HashMap<>());
}
Map putData = sourcePd.get(clazz);
//获取BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(source.getClass());
//获取属性描述信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
putData.put(propertyDescriptor.getName(),propertyDescriptor);
}
}
public static PropertyDescriptor getPd(String name,Class clazz) {
if(!sourcePd.containsKey(clazz)){
return null;
}
return sourcePd.get(clazz).get(name);
}
/**
*
* @param source the source bean
* @param target the target bean
*/
public static void copyProperties(Object source,Object target) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
//获取BeanInfo
BeanInfo beanInfo = Introspector.getBeanInfo(target.getClass());
//获取属性描述信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
Class<?> aClass = source.getClass();
//创建source的描述信息map
createPd(source);
//遍历属性描述信息,进行copy
for (PropertyDescriptor propertyDescriptor : propertyDescriptors) {
String name = propertyDescriptor.getName();
PropertyDescriptor sourcePd = getPd(name,aClass);
//如果source没有对应属性,直接continue
if(sourcePd == null){
continue;
}
//获取getter和setter方法
Method writeMethod = propertyDescriptor.getWriteMethod();
Method readMethod = sourcePd.getReadMethod();
//授予权限 private也可以访问
if(writeMethod != null && readMethod != null){
if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())){
writeMethod.setAccessible(true);
}
if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())){
readMethod.setAccessible(true);
}
//复制属性
Object invoke = readMethod.invoke(source);
writeMethod.invoke(target,invoke);
}
}
}
public static void main(String[] args) throws IntrospectionException, InvocationTargetException, IllegalAccessException {
Source source = new Source();
source.setName("张三");
source.setAge(18L);
Target target = new Target();
long start = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
//target.setAge(source.getAge());
// target.setName(source.getName());
copyProperties(source,target);
//BeanUtils.copyProperties(source,target);
//org.apache.commons.beanutils.BeanUtils.copyProperties(target,source);
}
long end = System.currentTimeMillis();
System.out.println(end - start);
System.out.println(target);
}
}
此案例中类型转换并未特别处理,大家可以根据commons的实现自行处理简单的转换。
网友评论