Java反射小练之手写BeanUtils的copyProperties(Upgrade)
Posted Huterox
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java反射小练之手写BeanUtils的copyProperties(Upgrade)相关的知识,希望对你有一定的参考价值。
文章目录
前言
在一个风和日丽的下午,开起来一天的代码之旅,结果发现了一个bug,没错事情是这样子的:
有这样一段代码:
我们将MybatisPlus的一个QureyWrapper对象存储进去了这个Map里面,这个Map是这样的<String,Object> 所以的话我们是可以将这个对象存进去的。
之后我们还有一段代码需要解析:
需要将这个名义上的Object对象(实际上,我们拿到的那个accurate_query对象就是Wrapper类型)重新转化回来。
这样看炸一下是木有问题的,因为咱们的其实本来就是这个类型的,只是存储的时候类型放大了,现在放回来。但是不好意思过不了,那么这个时候我就开始想要解决方案了,很显然这个由于类型的问题。解决方案必然是通过反射进行解决,如何通过反射直接进行类型转换(这里提一嘴,我们其实可以去直接相等,写一下逻辑骗过编译器就可以,因为本质上他们就是一个类型的)。
问题解决思路
那么这个时候的话,明确了,我们需要通过反射去进行转换,我们可以去创建一个新的类,但是这个类的属性,对应的值必须是我们原来的。
那么这个时候,我们就要去看一看这个Wrapper里面到底有啥了。
我们进入这个Wrapper
可以发现里面其实没啥东西,继承了这个抽象的Wrapper,我们进入这个看看。
在这里的话我们可以发现很多的方法,比如我们常用的追加条件啥的。
所以这个我们就很容易想到一个解决方案:
那就是
那么这个时候有小伙伴问了,直接替换不就完了,为啥还要QueryWrapper的源码呀。其实很简单,主要是看看有没有特殊的需要注意的地方,验证一下我们能不能通过替换得到我们想要得到的类。
实际解决方案
既然知道了,那么我们就要解决。
解决方案其实很简单,那就是这个:
BeanUtils.copyProperties(accurate_query,blogEntityQueryWrapper);
就这么粗暴。
那我就不服了,我得看看这个玩意咋写的,虽然写得出来。
转换实现
ok,现在就进去看看有啥,可以看到源码:
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException
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;
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
List<String> ignoreList = ignoreProperties != null ? Arrays.asList(ignoreProperties) : null;
PropertyDescriptor[] var7 = targetPds;
int var8 = targetPds.length;
for(int var9 = 0; var9 < var8; ++var9)
PropertyDescriptor targetPd = var7[var9];
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 var15)
throw new FatalBeanException("Could not copy property '" + targetPd.getName() + "' from source to target", var15);
通过这段代码我们大概知道了这样几件事情:
1.复制的是属性,而不是字段
2.通过一个目标对象的父类或者其实现的接口来控制需要复制属性的范围
总体的实现也是非常简单,就是通过内省来做的。
和这段代码(初学的时候的代码):
Class<?> beanClass = Class.forName(prop.getProperty(name));
bean = beanClass.getConstructor().newInstance();
Object buyhouse = Class.forName(prop.getProperty("bean.PersonBuyHouse")).getConstructor().newInstance();
Object houseagent = Class.forName(prop.getProperty("bean.HouseAgent")).getConstructor().newInstance();
BeanInfo beanInfo = Introspector.getBeanInfo(beanClass);
PropertyDescriptor[] propertyDescriptor = beanInfo.getPropertyDescriptors();
for(PropertyDescriptor pd:propertyDescriptor)
if(pd.getName().equals("houseAgency"))
pd.getWriteMethod().invoke(bean, houseagent);
else if(pd.getName().equals("buyHouse"))
pd.getWriteMethod().invoke(bean, buyhouse);
区别就是,人家两个类在换属性。
升级
开玩笑,我彬彬超勇的,现在假设,如果我只是想要改变其中的某一个字段咋办,那这个不行啊,而且它提供的玩意也确实只有这些玩意,那我难受了。那不行,升级,必须升级。
基本功能
首先我们需要实现这些功能
- 可以选择性忽略字段
- 可以选择性忽略字段
- 可以选择性忽略字段
好了,很完美。
编码
ok,现在咱们进行编码。
首先,我们实现一个我们最基本的方法
public static void copyObjectProperties(Object source,Map<String,Field> sourceFieldMap,Object target,Map<String,Field> targetFieldMap)
//进行属性值复制
sourceFieldMap.forEach(
(fieldName,sourceField) ->
//查看目标对象是否存在这个字段
Field targetField = targetFieldMap.get(fieldName);
if(targetField != null)
try
//对目标字段进行赋值操作
targetField.set(target,sourceField.get(source));
catch(IllegalAccessException e)
e.printStackTrace();
);
为了实现我上面说的功能,我们这里需要单独存储字段,就是要的那些破玩意。这里是最根本的方法嘛,所以类型是<String,Field>。
我们通过这个方法来解析类:
并且将其存放到缓存
public static Map<String,Field> getClassFieldMapWithCache(Class<?> sourceClass)
//查看缓存里面有没有已经解析完毕的现成的数据
SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);
//确保classFieldMap的正确初始化和缓存
if(softReference == null || softReference.get() == null)
//解析字节码对象
return resolveClassFieldMap(sourceClass);
else
//从缓存里面正确的取出数据
return softReference.get();
通过这个玩意,将完成对类很多属性的操作,例如:
/**
* 获取一个对象里面字段不为null的字段名称集合
*/
public static String[] getNonNullValueFieldNames(Object source)
//非空校验
Objects.requireNonNull(source);
//获取空值字段名称
String[] nullValueFieldNames = getNullValueFieldNames(source);
Map<String,Field> classFieldMap = getClassFieldMapWithCache(source.getClass());
//获取全部的字段名称,因为原数据没办法修改,所以需要重新建立一个集合来进行判断
Set<String> allFieldNames = new HashSet<>(classFieldMap.keySet());
//移除掉值为null的字段名称
allFieldNames.removeAll(Arrays.asList(nullValueFieldNames));
return allFieldNames.toArray(new String[]);
那么到这里我们就可以去实现我们要对比字段完成的工作了。
public static void copyPropertiesWithIgnoreSourceFields(Object source,Object target,String ...ignoreFieldNames)
Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));
filterByFieldName(sourceFieldMap,ignoreFieldNames);
copyObjectProperties(source,sourceFieldMap,target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
完整实现
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ReflectUtils
private static final Map<Class<?>,SoftReference<Map<String,Field>>> resolvedClassCache = new ConcurrentHashMap<>();
/**
* 获取一个对象里面字段为null的字段名称集合
*/
public static String[] getNullValueFieldNames(Object source)
//非空校验:NullPointerException
Objects.requireNonNull(source);
Class<?> sourceClass = source.getClass();
//从缓存里面获取,如果缓存里面没有就会进行第一次反射解析
Map<String,Field> classFieldMap = getClassFieldMapWithCache(sourceClass);
List<String> nullValueFieldNames = new ArrayList<>();
classFieldMap.forEach(
(fieldName,field) ->
try
//挑选出值为null的字段名称
if(field.get(source) == null)
nullValueFieldNames.add(fieldName);
catch(IllegalAccessException e)
e.printStackTrace();
);
return nullValueFieldNames.toArray(new String[]);
/**
* 获取一个对象里面字段不为null的字段名称集合
*/
public static String[] getNonNullValueFieldNames(Object source)
//非空校验
Objects.requireNonNull(source);
//获取空值字段名称
String[] nullValueFieldNames = getNullValueFieldNames(source);
Map<String,Field> classFieldMap = getClassFieldMapWithCache(source.getClass());
//获取全部的字段名称,因为原数据没办法修改,所以需要重新建立一个集合来进行判断
Set<String> allFieldNames = new HashSet<>(classFieldMap.keySet());
//移除掉值为null的字段名称
allFieldNames.removeAll(Arrays.asList(nullValueFieldNames));
return allFieldNames.toArray(new String[]);
public static Map<String,Field> resolveClassFieldMap(final Class<?> sourceClass)
SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);
//判断是否已经被初始化
if(softReference == null || softReference.get() == null)
//对同一个字节码对象的解析是同步的,但是不同字节码对象的解析是并发的
synchronized(sourceClass)
softReference = resolvedClassCache.get(sourceClass);
if(softReference == null || softReference.get() == null)
Map<String,Field> fieldMap = new HashMap<>();
/*
Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this
Class object. This includes public, protected, default access, and private fields, but excludes inherited fields
*/
Field[] declaredFields = sourceClass.getDeclaredFields();
if(declaredFields != null && declaredFields.length > 0)
for(Field field : declaredFields)
/*
Set the accessible flag for this object to the indicated boolean value.
*/
field.setAccessible(true);
fieldMap.put(field.getName(),field);
//设置为不变Map
fieldMap = Collections.unmodifiableMap(fieldMap);
softReference = new SoftReference<>(fieldMap);
/*
更新缓存,将解析后的数据加入到缓存里面去
*/
resolvedClassCache.put(sourceClass,softReference);
return fieldMap;
/*
运行到这里来的时候要么早就存在,要么就是已经被其他的线程给初始化了
*/
return softReference.get();
public static Map<String,Field> getClassFieldMapWithCache(Class<?> sourceClass)
//查看缓存里面有没有已经解析完毕的现成的数据
SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);
//确保classFieldMap的正确初始化和缓存
if(softReference == null || softReference.get() == null)
//解析字节码对象
return resolveClassFieldMap(sourceClass);
else
//从缓存里面正确的取出数据
return softReference.get();
public static void copyObjectProperties(以上是关于Java反射小练之手写BeanUtils的copyProperties(Upgrade)的主要内容,如果未能解决你的问题,请参考以下文章
Hibernate的BeanUtils.copyProperties方法,怎么不能拷贝对象
java学习--基础知识进阶第十三天--反射机制的概述和字节码对象的获取方式反射操作构造方法成员方法成员属性JavaBean的概述&BeanUtils的使用自定义BeanUtils工
JAVA中反射机制五(JavaBean的内省与BeanUtils库)