Apache-BeanUtils VS SpringBean-Utils

目录

一、前言

二、对象拷贝

三、什么是浅拷贝和深拷贝

四、BeanUtils

[1. Apache 的 BeanUtils](#1. Apache 的 BeanUtils)

[2. Spring 的 BeanUtils](#2. Spring 的 BeanUtils)

五、性能比较

六、推荐使用

七、深拷贝需求

[1. SerializationUtils.clone()](#1. SerializationUtils.clone())

2.SerializableFunction+Iterables.transform()


一、前言

在我们实际项目开发过程中,我们经常需要将不同的两个对象实例进行属性复制,从而基于源对象的属性信息进行后续操作,而不改变源对象的属性信息,比如DTO数据传输对象和数据对象DO,我们需要将DO对象进行属性复制到DTO,但是对象格式又不一样,所以我们需要编写映射代码将对象中的属性值从一种类型转换成另一种类型。

二、对象拷贝

在具体介绍两种 BeanUtils 之前,先来补充一些基础知识。它们两种工具本质上就是对象拷贝工具,而对象拷贝又分为深拷贝和浅拷贝,下面进行详细解释。

三、什么是浅拷贝和深拷贝

在Java中,除了 基本数据类型 之外,还存在 类的实例对象这个引用数据类型,而一般使用 "="号做赋值操作的时候,对于基本数据类型,实际上是拷贝的它的值,但是对于对象而言,其实赋值的只是这个对象的引用,将原对象的引用传递过去,他们实际还是指向的同一个对象。

而浅拷贝和深拷贝就是在这个基础上做的区分。

  • 如果在拷贝这个对象的时候,只对基本数据类型进行了拷贝,而对引用数据类型只是进行引用的传递,而没有真实的创建一个新的对象,则认为是浅拷贝

  • 反之,在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且复制其内的成员变量,则认为是深拷贝

简单来说:

  • 浅拷贝:浅拷贝只复制了对象的引用,使得新旧对象指向同一块内存区域。

  • 深拷贝: 对深拷贝则复制了对象的所有数据,包括引用类型的数据,创建了完全独立的新对象。

四、BeanUtils

前面简单讲了一下对象拷贝的一些知识,下面就来具体看下两种 BeanUtils 工具

1. Apache 的 BeanUtils

Apache BeanUtils 是Apache Commons库的一部分,它提供了一系列工具方法来简化JavaBean的操作,包括属性读取、设置以及属性拷贝。当使用BeanUtils.copyProperties()方法时,它执行的是浅拷贝,即对于引用类型的属性,拷贝的是引用本身,而不是引用的对象。这意味着如果源对象和目标对象有相同的引用类型属性,它们会指向同一个对象实例。

首先来看一个非常简单的BeanUtils的例子

java 复制代码
public class PersonSource  {
    private Integer id;
    private String username;
    private String password;
    private Integer age;
    // getters/setters omiited
}
public class PersonDest {
    private Integer id;
    private String username;
    private Integer age;
    // getters/setters omiited
}
public class TestApacheBeanUtils {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
       //下面只是用于单独测试
        PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
        PersonDest personDest = new PersonDest();
        BeanUtils.copyProperties(personDest,personSource);
        System.out.println("persondest: "+personDest);
    }
}
persondest: PersonDest{id=1, username='pjmike', age=21}

从上面的例子可以看出,对象拷贝非常简单,BeanUtils最常用的方法就是:

java 复制代码
//将源对象中的值拷贝到目标对象
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

默认情况下,使用org.apache.commons.beanutils.BeanUtils对复杂对象的复制是引用,这是一种浅拷贝

但是由于 Apache下的BeanUtils对象拷贝性能太差,不建议使用,而且在阿里巴巴Java开发规约插件上也明确指出:

Ali-Check | 避免用Apache Beanutils进行属性的copy。

commons-beantutils 对于对象拷贝加了很多的检验,包括类型的转换,甚至还会检验对象所属的类的可访问性,可谓相当复杂,这也造就了它的差劲的性能,具体实现代码如下:

java 复制代码
/**
 * 将一个JavaBean的所有属性复制到另一个JavaBean。
 * 此方法将从`orig`对象复制属性到`dest`对象,并在必要时进行类型转换。
 * 它支持从DynaBeans、Maps和标准JavaBeans复制属性。
 *
 * @param dest 目标bean,属性将被复制到此处。
 * @param orig 源bean,属性将从此处复制。
 * @throws IllegalAccessException 如果访问字段或方法时出现问题。
 * @throws InvocationTargetException 如果底层方法抛出异常。
 */
public void copyProperties(final Object dest, final Object orig)
        throws IllegalAccessException, InvocationTargetException {

    // 验证目标bean和源bean都不为null
    if (dest == null) {
        throw new IllegalArgumentException("未指定目标bean");
    }
    if (orig == null) {
        throw new IllegalArgumentException("未指定源bean");
    }
    if (log.isDebugEnabled()) {
        log.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")");
    }

    // 根据源bean的类型复制属性
    if (orig instanceof DynaBean) {
        // 当源bean是DynaBean时的处理
        final DynaProperty[] origDescriptors = ((DynaBean) orig).getDynaClass().getDynaProperties();
        for (DynaProperty origDescriptor : origDescriptors) {
            final String name = origDescriptor.getName();
            // 检查是否可以从源读取属性并且可以写入目标
            if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) {
                final Object value = ((DynaBean) orig).get(name);
                copyProperty(dest, name, value);
            }
        }
    } else if (orig instanceof Map) {
        // 当源bean是Map时的处理
        @SuppressWarnings("unchecked")
        final Map<String, Object> propMap = (Map<String, Object>) orig;
        for (final Map.Entry<String, Object> entry : propMap.entrySet()) {
            final String name = entry.getKey();
            if (getPropertyUtils().isWriteable(dest, name)) {
                copyProperty(dest, name, entry.getValue());
            }
        }
    } else { // 当源bean是标准JavaBean时的处理
        final PropertyDescriptor[] origDescriptors = getPropertyUtils().getPropertyDescriptors(orig);
        for (PropertyDescriptor origDescriptor : origDescriptors) {
            final String name = origDescriptor.getName();
            if ("class".equals(name)) {
                continue; // 跳过复制'class'属性,因为它没有意义
            }
            if (getPropertyUtils().isReadable(orig, name) && getPropertyUtils().isWriteable(dest, name)) {
                try {
                    final Object value = getPropertyUtils().getSimpleProperty(orig, name);
                    copyProperty(dest, name, value);
                } catch (final NoSuchMethodException e) {
                    // 这不应该发生,因为我们之前检查了可读性
                }
            }
        }
    }
}

2. Spring 的 BeanUtils

Spring框架中的BeanUtils提供了与Apache BeanUtils类似的功能,但它通常被认为在性能上优于Apache BeanUtils。Spring的BeanUtils.copyProperties()同样执行浅拷贝,但是它的实现更为简洁和高效,因为它直接调用getter和setter方法来进行属性的拷贝,避免了Apache BeanUtils中的一些额外的类型转换和访问性检查。

使用spring的BeanUtils进行对象拷贝:

java 复制代码
public class TestSpringBeanUtils {
    public static void main(String[] args) throws InvocationTargetException, IllegalAccessException {
​
       //下面只是用于单独测试
        PersonSource personSource = new PersonSource(1, "pjmike", "12345", 21);
        PersonDest personDest = new PersonDest();
        BeanUtils.copyProperties(personSource,personDest);
        System.out.println("persondest: "+personDest);
    }
}

spring下的BeanUtils也是使用 copyProperties方法进行拷贝,只不过它的实现方式非常简单,就是对两个对象中相同名字的属性进行简单的get/set,仅检查属性的可访问性。具体实现如下:

java 复制代码
/**
 * 复制源对象的属性值到目标对象中。
 * <p>注意:源和目标类不需要匹配或相互继承,只要属性名和类型匹配即可。
 * 不论源对象中是否存在而目标对象中不存在的属性,都将被忽略。
 * <p>自Spring Framework 5.3起,此方法在匹配源和目标对象的属性时会考虑泛型信息。
 * 请参阅{@link #copyProperties(Object, Object)}的文档以了解详细信息。
 * 
 * @param source 源对象,其属性值将被复制。
 * @param target 目标对象,将接收源对象的属性值。
 * @param editable 限制属性设置的目标类(或接口)。如果为null,则不限制。
 * @param ignoreProperties 要忽略的属性名数组。如果为null,则不忽略任何属性。
 * @throws BeansException 如果复制过程中发生错误。
 * @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");

    // 如果提供了editable类,则检查目标对象是否是其实例。
    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);

    // 遍历目标对象的属性,尝试从源对象复制值。
    for (PropertyDescriptor targetPd : targetPds) {
        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) {
                    // 使用ResolvableType检查源和目标属性类型的兼容性。
                    ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
                    ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);

                    // 检查类型是否兼容,如果存在未解析的泛型则退回到原始的Class.isAssignableFrom检查。
                    boolean isAssignable =
                            (sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ?
                                    ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) :
                                    targetResolvableType.isAssignableFrom(sourceResolvableType));

                    // 如果类型兼容,则尝试复制属性值。
                    if (isAssignable) {
                        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);
                        }
                        // 抛出BeansException封装复制过程中的任何异常。
                        catch (Throwable ex) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                        }
                    }
                }
            }
        }
    }
}

可以看到,成员变量赋值是基于目标对象的成员列表,并且会跳过ignore的以及在源对象中不存在,所以这个方法是安全的,不会因为两个对象之间的结构差异导致错误,但是必须保证同名的两个成员变量类型相同

五、性能比较

根据历史数据,Spring BeanUtils在性能测试中表现得更好,特别是在处理大量对象拷贝时。这是因为Spring BeanUtils的实现更为直接,而Apache BeanUtils在拷贝时会进行更多的检查和转换,这增加了开销。

六、推荐使用

鉴于Spring BeanUtils的性能优势,以及阿里巴巴Java开发规约中明确建议避免使用Apache BeanUtils,通常更推荐使用Spring BeanUtils进行对象属性的拷贝。此外,Spring BeanUtils与Spring框架的集成更加紧密,如果你的应用正在使用Spring,那么使用Spring BeanUtils会更加自然和高效。

七、深拷贝需求

如果你需要深拷贝(即复制引用类型属性所指向的对象),那么Apache BeanUtils和Spring BeanUtils都不是最佳选择。对于深拷贝的需求,可以考虑以下方式:

1. SerializationUtils.clone()

Apache Commons Lang Apache Commons Lang库提供了一个CloneUtils类,可以用于深拷贝对象。但是请注意,CloneUtils在Apache Commons Lang 3.5版本之后被标记为已弃用,建议使用SerializationUtils.clone()方法,该方法使用序列化实现深拷贝。

java 复制代码
   import org.apache.commons.lang3.SerializationUtils;
   
   T clonedObject = SerializationUtils.clone(originalObject);
​
    //示例
    List<AutoCalculationRule> rules = SerializationUtils.clone((ArrayList<AutoCalculationRule>) autoCalculationDefinition.getRules());
复制代码
   

2.SerializableFunction+Iterables.transform()

Google Guava Google Guava库提供了SerializableFunction和Iterables.transform()方法,可以结合使用实现深拷贝,但通常也需要对象实现Serializable接口。

实现:利用SerializableFunction来封装序列化和反序列化的逻辑,然后使用Iterables.transform()来应用这个函数到集合的每个元素上。

java 复制代码
    import com.google.common.base.Function;
    import com.google.common.collect.Iterables;
    import java.io.ByteArrayInputStream;
    import java.io.ByteArrayOutputStream;
    import java.io.ObjectInputStream;
    import java.io.ObjectOutputStream;
public class DeepCopyWithGuava {
    
     /**
     * 使用Java序列化机制实现单个对象的深拷贝。
     * 
     * @param original 原始对象,必须实现Serializable接口。
     * @param <T>      泛型类型参数,限定为实现了Serializable的类型。
     * @return 返回深拷贝后的对象。
     */
    public static <T extends Serializable> T deepCopy(T original) {
        try {
            // 创建一个字节数组输出流,用于存储序列化后的对象数据。
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            // 创建一个对象输出流,用于将对象写入字节数组输出流。
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            // 将原始对象写入输出流,完成序列化。
            oos.writeObject(original);
            // 刷新输出流,确保所有数据都被写出。
            oos.flush();
            
            // 使用字节数组输入流读取序列化后的对象数据。
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            // 创建一个对象输入流,用于从字节数组输入流中读取对象。
            ObjectInputStream ois = new ObjectInputStream(bais);
            // 从输入流中读取对象,完成反序列化,得到深拷贝后的对象。
            return (T) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            // 如果在序列化或反序列化过程中发生错误,抛出运行时异常。
            throw new RuntimeException("Error during deep copy", e);
        }
    }
​
    /**
     * 使用Guava的Iterables.transform()方法和自定义的SerializableFunction实现对象集合的深拷贝。
     * 
     * @param originalIterable 原始对象集合,其中的元素必须实现Serializable接口。
     * @param <T>              泛型类型参数,限定为实现了Serializable的类型。
     * @return 返回一个包含深拷贝后对象的集合。
     */
    public static <T extends Serializable> Iterable<T> deepCopyIterable(Iterable<T> originalIterable) {
        // 定义一个SerializableFunction,使用deepCopy方法将对象转换为其深拷贝版本。
        Function<T, T> deepCopyFunction = DeepCopyWithGuava::deepCopy;
        // 使用Iterables.transform()方法将deepCopyFunction应用于originalIterable中的每个元素,
        // 得到一个包含深拷贝后对象的新集合。
        return Iterables.transform(originalIterable, deepCopyFunction);
    }
    
    
    // 测试
     public static void main(String[] args) {
        // 示例对象
        class Example implements Serializable {
            int i;
            String s;
            Example(int i, String s) {
                this.i = i;
                this.s = s;
            }
        }
​
        // 创建一个Example对象的集合
        Iterable<Example> examples = new ArrayList<>();
        examples.add(new Example(1, "one"));
        examples.add(new Example(2, "two"));
​
        // 使用Guava进行深拷贝
        Iterable<Example> copiedExamples = deepCopyIterable(examples);
​
        // 输出验证
        for (Example example : copiedExamples) {
            System.out.println(example.i + ": " + example.s);
        }
    }
}
相关推荐
古月居GYH5 分钟前
在C++上实现反射用法
java·开发语言·c++
儿时可乖了1 小时前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol1 小时前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-13141 小时前
jdk各个版本介绍
java
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺2 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序2 小时前
vue3 封装request请求
java·前端·typescript·vue