浅析BeanUtils

2,009 阅读5分钟

浅析BeanUtils

在聊聊BeanUtils之前,我们可以先了解一下PO VO BO DTO 。

  1. PO (persistant object持久对象)与数据库中对应的对象。
  2. BO (business object业务对象)处理业务逻辑肯定会涉及到多张表的数据,BO可以将多个PO对象整合一起进行业务处理。
  3. VO (view object表现层对象) 传递至前端的对象,可以将私密的数据隐藏掉。
  4. DTO (Data Transfer Object数据传输对象)可以简单理解为前端POST传递数据至后端,后端可以用@RequestBody ObjectDTO 对象进行接收处理。

那么在业务比较复杂的情况下,必然会出现各种DTO,BO,VO,PO相互转换的代码。在业务代码中出现大量的GET,SET方法不美观,容易出错而且耗费精力。由此出现了很多的开源工具,例如springframework的BeanUtils,apache的BeanUtils,dozer等。

本文讨论的是springframework包下的BeanUtils。

import org.springframework.beans.BeanUtils;
BeanUtils.copyProperties(Object source, Object target);

​ 就是把source对象的属性赋值给target对象,但是要注意的是BeanUtils并不会对null进行处理,而是会将其作为属性值直接赋值给target。

​ 例如:

ObjectDTO
@Data
@AllArgsConstructor
public class ObjectDTO {
    String name;
    int age;
    OtherProperty otherProperty;
}
ObjectBO
@Data
@AllArgsConstructor
public class ObjectBO {
    String name;
    int age;
    OtherProperty otherProperty;
    double value;
}
OtherProperty
@Data
@AllArgsConstructor
public class OtherProperty {
    String propertyName;
}
main
public class CopyMain {
    public static void main(String[] args) {
        ObjectDTO objectDTO = new ObjectDTO("DTO对象", 18, new OtherProperty("DTO其他属性"));
        ObjectBO objectBO = new ObjectBO("BO对象",20,new OtherProperty("BO其他属性"),1.0);
        BeanUtils.copyProperties(objectDTO,objectBO);
        //ObjectDTO(name=DTO对象, age=18, otherProperty=OtherProperty(propertyName=DTO其他属性))
        System.out.println(objectDTO);
        //ObjectBO(name=DTO对象, age=18, otherProperty=OtherProperty(propertyName=DTO其他属性), value=1.0)
        System.out.println(objectBO);
        //true
        System.out.println(objectDTO.getOtherProperty() == objectBO.getOtherProperty());
    }
}

由此可见source会将target所有符合条件的属性进行copy,如果属性是引用对象,则会共享,属于浅克隆。

如果将ObjectDTO的name属性变更为null,otherProperty 设置为null,是否会跨过这些属性进行复制呢?

请看:

public static void main(String[] args) {
   	ObjectDTO objectDTO = new ObjectDTO(null, 18, null);
        ObjectBO objectBO = new ObjectBO("BO对象",20,new OtherProperty("BO其他属性"),1.0);
        BeanUtils.copyProperties(objectDTO,objectBO);
        //ObjectDTO(name=null, age=18, otherProperty=null)
        System.out.println(objectDTO);
        //ObjectBO(name=null, age=18, otherProperty=null, value=1.0)
        System.out.println(objectBO);
        //true
        System.out.println(objectDTO.getOtherProperty() == objectBO.getOtherProperty());
}

并不会跨过null进行赋值,而是会进行覆盖。所以童鞋们要注意这一点哦,不然在修改操作的方法可能导致重要数据的丢失。:)

聊了这么多,我们看看BeanUtils的源码是如何实现的吧!

    /**
	 * Copy the property values of the given source bean into the given target bean.
	 * <p>Note: The source and target classes do not have to match or even be derived
	 * from each other, as long as the properties match. Any bean properties that the
	 * source bean exposes but the target bean does not will silently be ignored.
	 * @param source the source bean
	 * @param target the target bean
	 * @param editable the class (or interface) to restrict property setting to
	 * @param ignoreProperties array of property names to ignore
	 * @throws BeansException if the copying failed
	 * @see BeanWrapper
	 */
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);
                        }
                    }
                }
            }
        }

    }

细心的你,应该发现了这样的参数 @Nullable String... ignoreProperties,顾名思义BeanUtils支持跨过某些属性赋值。

public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
		copyProperties(source, target, null, ignoreProperties);
	}

想完成上面的跨过name,otherProperty,只需要

BeanUtils.copyProperties(objectDTO,objectBO,"name","otherProperty");

思考一下,如果一直通过反射来取值赋值,那么这个时间成本其实是比较大的,如果返回大量的数据转成VO对象,那响应的速度是非常慢的。先测试一下BeanUtils的效率。

	ObjectDTO objectDTO = new ObjectDTO("DTO", 18, new OtherProperty("property"));
        ObjectBO objectBO = new ObjectBO();
        ObjectBO objectBO1 = new ObjectBO();
        ObjectBO objectBO2 = new ObjectBO();



        long start = System.currentTimeMillis();
        BeanUtils.copyProperties(objectDTO,objectBO);
        long end1 = System.currentTimeMillis();
        BeanUtils.copyProperties(objectDTO,objectBO1);
        long end2 = System.currentTimeMillis();
        BeanUtils.copyProperties(objectDTO,objectBO2);
        long end3 = System.currentTimeMillis();

        System.out.println("第一次复制花费的时间:" + (end1 - start));
        System.out.println("第二次复制花费的时间:" + (end2 - end1));
        System.out.println("第三次复制花费的时间:" + (end3 - end2));

结果如下:

第一次复制花费的时间:679
第二次复制花费的时间:0
第三次复制花费的时间:0

为什么出现这样的情况呢?在正常的预期中应该是 679*n 才对吧,让我们看看BeanUtils是怎么优化的。

请注意上面源码的这一行

PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);

只是一个简单的获得所有属性的PropertyDescriptor集合的方法。

    /**
	 * Retrieve the JavaBeans {@code PropertyDescriptor}s of a given class.
	 * @param clazz the Class to retrieve the PropertyDescriptors for
	 * @return an array of {@code PropertyDescriptors} for the given class
	 * @throws BeansException if PropertyDescriptor look fails
	 */
	public static PropertyDescriptor[] getPropertyDescriptors(Class<?> clazz) throws BeansException {
		CachedIntrospectionResults cr = CachedIntrospectionResults.forClass(clazz);
		return cr.getPropertyDescriptors();
	}

继续深挖。

/**
	 * Create CachedIntrospectionResults for the given bean class.
	 * @param beanClass the bean class to analyze
	 * @return the corresponding CachedIntrospectionResults
	 * @throws BeansException in case of introspection failure
	 */
	@SuppressWarnings("unchecked")
	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);
	}

原来BeanUtils会从强缓存中查找是否有当前类的属性集合,没有就去弱缓存中查找。如果都没有,才会进行创建创建。strongClassCache,softClassCache本质上其实是Map,class作为key,将创建的CachedIntrospectionResults作为value进行存储。源类也一样将PropertyDescriptor缓存到CachedIntrospectionResults中。因此大大提升了性能.

  • 注意: 如果有父类的话,父类的属性也会缓存起来。

参考资料

参考CNblogs链接

CSDN