浅析Spring浅拷贝 BeanUtils.copyProperties方法

最近在接口重构,因此涉及解耦,但是目前业务对象字段大多相同,因此为了节省时间,考虑使用BeanUtils.copyProperties减轻工作量,因此了解了下此方法。
大多数文章讲的是BeanUtils.copyProperties方法会拷贝相同字段属性和类型的数据,但此情况是在用户遵循Java Bean规范的前提下,但是如果用户不遵循Java Bean命名规范,或者故意利用BeanUtils.copyProperties的原理,就可以达到不一样的映射关系。
直接看源码BeanUtils.copyProperties(Object source, Object target)方法

	public static void copyProperties(Object source, Object target) throws BeansException {
			//第一个参数为源数据,第二个参数为拷贝后的数据
		copyProperties(source, target, null, (String[]) null);
	}

继续看下去

	private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
			@Nullable String... ignoreProperties) throws BeansException {
		//source和target必须自己生成
		Assert.notNull(source, "Source must not be null");
		Assert.notNull(target, "Target must not be null");
		
		Class<?> actualEditable = target.getClass();
		//如果指定了editable,则会判断target是否为editable的子类,如果不是则抛出异常。如果是,则按照父类来拷贝字段。
		if (editable != null) {
			if (!editable.isInstance(target)) {
				throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
						"] not assignable to Editable class [" + editable.getName() + "]");
			}
			actualEditable = editable;
		}
		//获取Class的get、set方法
		PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);
		List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
		//下面的方法其实就是获取readMethod和writeMethod,即getXX方法和setXX方法,先getXX值,再setXX。
		for (PropertyDescriptor targetPd : targetPds) {
			Method writeMethod = targetPd.getWriteMethod();
			//判断该属性是否在不被copy的属性集合中
			if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
				PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
				if (sourcePd != null) {
					Method readMethod = sourcePd.getReadMethod();
					//判断目标对象的set方法所需的参数类型和源对象get方法的返回类型是否一致
					if (readMethod != null &&
							ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
						try {
						   //如果get方法不是public,则取消访问检查,允许调用方法。
							if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
								readMethod.setAccessible(true);
							}
							Object value = readMethod.invoke(source);
							//如果set方法不是public,则取消访问检查,允许调用方法。
							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);
						}
					}
				}
			}
		}
	}

此处简单讲解下PropertyDescriptor ,其实就是读取一个类的setXX方法和getXX方法,具体可以看这篇博客 。因此实际上BeanUtils的方法是根据比较源Class和目标Class的get和set方法。因此以下两个类也是能完成拷贝的。

public class BeanOne {
    private String test;

    public String getTest() {
        return test;
    }

    public void setTest(String test) {
        this.test = test;
    }
}

public class BeanTwo {
    private String test2;

    public String getTest() {
        return test2;
    }

    public void setTest(String test2) {
        this.test2 = test2;
    }
}

此时可以将test拷贝到test2,也可以将test2拷贝到test,但是这其实违反了Java Bean的规范的。
我们再看下Spring是如何获取PropertyDescriptor的,实际上是通过PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable)获取的。
继续看源码:

	public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
		CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
		return cr.getPropertyDescriptors();
	}

继续看下去

//简单来说就是该类会缓存一份,如果没有就构造。
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的构造。
		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);
	}

继续看

在这里插入代码片
	private CachedIntrospectionResults(Class<?> beanClass) throws BeansException {
		try {
			if (logger.isTraceEnabled()) {
				logger.trace("Getting BeanInfo for class [" + beanClass.getName() + "]");
			}
			//beanInfo对象即为BeanInfo,JavaBean内省,即上述引用博客中提到的,具体可以看上面博客。
			//
			this.beanInfo = getBeanInfo(beanClass);

			if (logger.isTraceEnabled()) {
				logger.trace("Caching PropertyDescriptors for class [" + beanClass.getName() + "]");
			}
			this.propertyDescriptorCache = new LinkedHashMap<>();

			// This call is slow so we do it once.
			//其实就是通过beanInfo.getPropertyDescriptors方法获取
			PropertyDescriptor[] pds = this.beanInfo.getPropertyDescriptors();
			for (PropertyDescriptor pd : pds) {
				if (Class.class == beanClass &&
						("classLoader".equals(pd.getName()) ||  "protectionDomain".equals(pd.getName()))) {
					// Ignore Class.getClassLoader() and getProtectionDomain() methods - nobody needs to bind to those
					continue;
				}
				if (logger.isTraceEnabled()) {
					logger.trace("Found bean property '" + pd.getName() + "'" +
							(pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") +
							(pd.getPropertyEditorClass() != null ?
									"; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
				}
				pd = buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
				this.propertyDescriptorCache.put(pd.getName(), pd);
			}

			// Explicitly check implemented interfaces for setter/getter methods as well,
			// in particular for Java 8 default methods...
			Class<?> currClass = beanClass;
			while (currClass != null && currClass != Object.class) {
				introspectInterfaces(beanClass, currClass);
				currClass = currClass.getSuperclass();
			}

			this.typeDescriptorCache = new ConcurrentReferenceHashMap<>();
		}
		catch (IntrospectionException ex) {
			throw new FatalBeanException("Failed to obtain BeanInfo for class [" + beanClass.getName() + "]", ex);
		}
	}

接下来我们看beanInfo怎么获得,因此BeanInfo是接口,需要有具体实现。继续看 this.beanInfo = getBeanInfo(beanClass);

	private static BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
		for (BeanInfoFactory beanInfoFactory : beanInfoFactories) {
		 	//即遍历beanInfoFactories,如果有对应的能够加载该类的,则返回BeanInfo。
			BeanInfo beanInfo = beanInfoFactory.getBeanInfo(beanClass);
			if (beanInfo != null) {
				return beanInfo;
			}
		}
		return (shouldIntrospectorIgnoreBeaninfoClasses ?
				Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) :
				Introspector.getBeanInfo(beanClass));
	}

再来看beanInfoFactory

	//即读取spring.factories的BeanInfoFactory的实现类
	private static List<BeanInfoFactory> beanInfoFactories = SpringFactoriesLoader.loadFactories(
			BeanInfoFactory.class, CachedIntrospectionResults.class.getClassLoader());

点击看spring-beans的jar包下的META-INF中spring.factories可知,即获取到的是ExtendedBeanInfoFactory

org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory

观看上述对应的源码

	//即实际上还是调用JAVA BEAN 内省工具Introspector,相关描述在上述引用的博客中有提到。
	public BeanInfo getBeanInfo(Class<?> beanClass) throws IntrospectionException {
		return (supports(beanClass) ? new ExtendedBeanInfo(Introspector.getBeanInfo(beanClass)) : null);
	}

总结: BeanUtils.copyProperties是浅拷贝的一种,通过调用set和get方法实现拷贝,他们的拷贝并不是根据字段名和字段类型来判断的,而是根据set、get名和属性判断。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值