需求场景
开发中我们经常需要将一个实体类的属性值赋值给另一个实体类,而两个实体类中的属性却大多数相同,如果实体类中的属性较多,手动一一赋值的方式就显得比较繁琐,并且很容易错误。
为了避免手动赋值的繁琐和易错问题,所以通常会使用工具类来实现两实体类属性值的copy;
常见的Bean copy util
有些开源的工具包已经为我们提供了这样工具类,例如
Apache commons-beanutils提供
commons-beanutils工具包下的两个工具类:
org.apache.commons.beanutils.BeanUtils.copyProperties(target, source);
org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source);
其中PropertyUtils性能稍微比BeanUtils高一点,但是这两个工具类性能都不是很高。不推荐使用。
Spring提供BeanUtils
而Spring中也给我们提供了一个BeanUtils可以实现实体类属性拷贝,其性能要比commons-beanutils中的高出很多,
其主要是利用反射+缓存的方式实现,通过缓存PropertyDescriptor,便于快速查询到Getter/Setter对应的Method.
避免每次反射获取Method,提高性能.(PropertyDescriptor中保存对应的Getter/Setter的Method)
org.springframework.beans.BeanUtils.copyProperties(source, target)
Cglib提供BeanCopier
Spring中还整合Cglib工具包,通过Cglib的BeanCopier也可实现高性能的实体类拷贝.
其利用动态生成Class的方式,拷贝性能比Spring的BeanUtils.copyProperties又高出很多.
import org.springframework.cglib.beans.BeanCopier;
BeanCopier copier = BeanCopier.create(source.getClass(), target.getClass(), false);
copier.copy(source, target, null);
通过Java7的方法句柄实现高性能的BeanCopyUtil
本文将讲解如果通过Java7的方法句柄实现比Spring的BeanUtils.copyProperties更高性能的BeanCopyUtil,
甚至在一定情况下可以比Cglib的BeanCopier性能还要高. 文末将会给出Spring BeanUtils.copyProperties,
Cglib BeanCopier,以及自己实现的BeanCopyUtil,三个工具之间的性能对比。
方法句柄是Java7中所提供的API,目的是为了提供比反射具有更高性能的Java方法间接访问技术。
方法句柄包含一对名为invokeExact和invoke的特殊调用程序方法。两个调用程序方法都提供了对方法句柄的底层方法、构造函数、字段或其他操作的直接访问方式。
常见API
//获取直接访问字段值的方法句柄,注意不是实体类中的Getter方法
java.lang.invoke.MethodHandles.Lookup#findGetter
//获取直接给字段赋值的方法句柄,注意不是实体类中的Setter方法
java.lang.invoke.MethodHandles.Lookup#findSetter
//获取普通方法的方法句柄
java.lang.invoke.MethodHandles.Lookup#findVirtual
//获取静态方法的方法句柄
java.lang.invoke.MethodHandles.Lookup#findStatic
//获取静态字段的方法句柄
java.lang.invoke.MethodHandles.Lookup#findStaticGetter
java.lang.invoke.MethodHandles.Lookup#findStaticSetter
BeanCopyUtil的具体实现:注意代码中的注释
public class BeanCopyUtil{
//用于缓存方法句柄,避免每次执行复制时获取,提高copy性能
private static final ConcurrentHashMap<CacheKey, List<CopyMethodHandle>> methodHandleCache = new ConcurrentHashMap<>();
//从缓存中获取或生成Getter/Setter方法句柄,并执行赋值
public static <T> T copy(Object source, T target) {
List<CopyMethodHandle> getSetMethodHandles = methodHandleCache
.computeIfAbsent(new CacheKey(source.getClass(), target.getClass()),
key -> getCopyMethodHandles(source, target));
try {
for (CopyMethodHandle getSetMethodHandle : getSetMethodHandles) {
getSetMethodHandle.invoke(source, target);
}
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return target;
}
//获取Getter/Setter方法句柄
private static <T> List<CopyMethodHandle> getCopyMethodHandles(Object source, T target) {
ArrayList<CopyMethodHandle> methodHandles = new ArrayList<>();
PropertyDescriptor[] sourcePropertyDescs = getPropertyDescs(source.getClass());
Map<String, PropertyDescriptor> targetPropertyDescsMap = getPropertyDescsMap(target.getClass());
for (PropertyDescriptor sourcePropertyDesc : sourcePropertyDescs) {
Method readMethod = sourcePropertyDesc.getReadMethod();
if (readMethod != null) {
PropertyDescriptor targetPropertyDesc = targetPropertyDescsMap.get(sourcePropertyDesc.getName());
Method writeMethod = null;
if (targetPropertyDesc != null
&& (writeMethod = targetPropertyDesc.getWriteMethod()) != null
&& ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
//利用Java7的API获取方法句柄
MethodHandle getMethodHandle = MethodHandles.lookup().unreflect(readMethod);
MethodHandle setMethodHandle = MethodHandles.lookup().unreflect(writeMethod);
methodHandles.add(new CopyMethodHandle(getMethodHandle, setMethodHandle));
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
return methodHandles;
}
//缓存Key,需要重写hashCode和equals方法
public static class CacheKey {
private Class<?> source;
private Class<?> target;
public CacheKey(Class<?> source, Class<?> target) {
this.source = source;
this.target = target;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CacheKey cacheKey = (CacheKey) o;
return Objects.equals(source, cacheKey.source) && Objects.equals(target, cacheKey.target);
}
@Override
public int hashCode() {
return Arrays.hashCode(new Object[]{source, target});
}
}
//封装源实体类的Getter方法句柄和目标实体类的Setter方法句柄
static class CopyMethodHandle {
private MethodHandle sourceGet;
private MethodHandle targetSet;
public CopyMethodHandle(MethodHandle sourceGet, MethodHandle targetSet) {
this.sourceGet = sourceGet;
this.targetSet = targetSet;
}
//执行方法句柄,调用Setter方法赋值
public void invoke(Object source, Object target) throws Throwable {
Object value = sourceGet.invoke(source);
targetSet.invoke(target, value);
}
}
private static PropertyDescriptor[] getPropertyDescs(Class<?> clazz) {
BeanInfo beanInfo = null;
try {
beanInfo = Introspector.getBeanInfo(clazz);
} catch (IntrospectionException e) {
e.printStackTrace();
}
if (beanInfo == null || beanInfo.getPropertyDescriptors() == null) {
return new PropertyDescriptor[0];
}
return beanInfo.getPropertyDescriptors();
}
private static Map<String, PropertyDescriptor> getPropertyDescsMap(Class<?> clazz) {
return Arrays.stream(getPropertyDescs(clazz))
.collect(Collectors.toMap(PropertyDescriptor::getName, (desc) -> desc));
}
}
压测
windows10系统,Java1.8.0U171版本,逻辑处理器8核,内存16G
开启15条线程分别调用对象拷贝方法,循环执行2次,预热1次,每次执行5s,结果输出平均耗时。
压测结果:
对BeanCopier缓存后的结果:
总结:
在没有对CglibCopier对象缓存之前,自己实现的BeanCopyUtil性能和Cglib的BeanCopier差不多,从数据上看略胜于BeanCopier。是Spring BeanUtils.copyProperties方法的两倍以上。
后对BeanCopier进行缓存,同样使用的CacheKey对象做为缓存Key,Cglib的BeanCopier略胜于BeanCopyUtil。Spring BeanUtils.copyProperties内存本身就存在缓存,但是仍然是最低的。
但是其实一般系统中Spring BeanUtils.copyProperties性能已经足够,
如果系统对赋值性能要求极高,可以考虑自定义的BeanCopyUtil和Cglib的BeanCopier。
对于Apache commons-beanutils中的BeanUtils.copyProperties不推荐使用。需要注意的是其方法参数含义和Spring的BeanUtils.copyProperties方法参数含义正好相反。