• 浅析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);
    	}
    
    • 1
    • 2
    • 3
    • 4

    继续看下去

    	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);
    						}
    					}
    				}
    			}
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50

    此处简单讲解下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;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    public class BeanTwo {
        private String test2;
    
        public String getTest() {
            return test2;
        }
    
        public void setTest(String test2) {
            this.test2 = test2;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    此时可以将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();
    	}
    
    • 1
    • 2
    • 3
    • 4

    继续看下去

    //简单来说就是该类会缓存一份,如果没有就构造。
    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);
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    继续看

    在这里插入代码片
    	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);
    		}
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    接下来我们看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));
    	}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    再来看beanInfoFactory

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

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

    org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory
    
    • 1

    观看上述对应的源码

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

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

  • 相关阅读:
    Nacos集群和持久化配置(重要)
    原生小程序 自定义组件
    实现strStr题解补充
    K8S之pod(十二)
    UML测试题(用例图基础b)
    【C/C++】宏定义中的#和##
    TorchVision Transforms API 大升级,支持目标检测、实例/语义分割及视频类任务
    基于腾讯云的OTA远程升级
    产品经理专业知识50篇(三)-如何寻找用户增长的根本动因
    P4068 [SDOI2016]数字配对
  • 原文地址:https://blog.csdn.net/qq_43527397/article/details/126238564