关于Apache BeanUtils组件浅复制的原因分析以及源码修改

本文深入分析了Apache BeanUtils的浅复制原因,并通过源码修改实现了深复制功能。在浅复制测试中,展示了对象引用地址导致的一变俱变现象。接着探讨了BeanUtilsBean在处理引用类型时的行为,揭示了浅复制的根源。通过反射干预内存地址空间,成功改造源码以实现深复制。最后,提及了Apache BeanUtils的体积问题及Spring BeanUtils等替代方案的优缺点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1、浅复制测试

浅复制和深复制最本质的区别就是引用数据类型的地址指向问题,见下方图解。

 在本次测试中,在School类里定义Student类对象实现浅复制测试,School类代码定义如下:

public class School {
    private Student student; //通过Student类对象模拟引用数据类型的地址指向
    private String addr;

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        this.student = student;
    }

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    @Override
    public String toString() {
        return "School{" +
                "student=" + student +
                ", addr='" + addr + '\'' +
                '}';
    }
}

Student类定义如下:

public class Student {
    private String name;
    private Integer age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

测试Demo:

public void test02(){
        Student student = new Student();
        student.setAge(20);
        student.setName("java");

        School school = new School();
        School school02 = new School();
        try {
            BeanUtils.setProperty(school,"student",student);
            BeanUtils.setProperty(school,"addr","xxx路xxx号");
            BeanUtils.copyProperties(school02,school);
            System.out.println(school.getStudent() == school02.getStudent());
            school02.getStudent().setName("java123456");
            System.out.println(school);
            System.out.println(school02);
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
    }

最终运行结果:

 可以看到,当其中一个引用数据类型对象的addr属性发生变化时,另外一个引用数据类型对象的内容同样发生改变,一变俱变,符合浅复制的特点。

 2、浅复制原因分析

在BeanUtils工具类中,BeanUtilsBean类对于Bean对象的操作方式是通过获得Bean对象的存取器方法进而操作对象,但Bean对象里属性的数据类型不唯一,因此BeanUtils工具类还提供了多种类型的转换器以匹配不同Bean里不同的数据类型。但当找不到引用数据类型对象的转换器时,此时BeanUtilsBean对于该引用数据类型的操作方式是直接返回该引用数据类型。也即返回的引用数据类型的值和用户Bean对象里引用数据类型的值一致,因此返回的引用数据类型的内存地址和用户传入的引用数据类型对象的内存地址空间一致,故最终才会出现符合浅复制特点的测试结果。

对源码进行Debug也能发现,当找不到合适的converter时,BeanUtils组件采取的方式是直接return返回当前类对象,所以内存地址空间并没有发生任何变化,故才会导致浅复制。

3、修改源码实现深复制

需要强调一点,如果直接下载官网的jar包是无法获得修改源码权限的,下载source目录下的tar包解压后就可以修改底层代码了。

 在前面已经分析出组件浅复制的根本原因是当找不到合适的转换器时,返回的数据前后的内存地址空间一致,因此可以通过反射对返回的目标数据对象内存地址空间进行干预,通俗的话讲就是通过反射在JVM开辟的内存空间里使用反射构建一个新的对象即可。

//修改后的convert方法
protected Object convert(final Object value, final Class<?> type) {
        final Converter converter = getConvertUtils().lookup(type);
        if (converter != null) {
            log.trace("        USING CONVERTER " + converter);
            return converter.convert(type, value);
        } else {
            try {
                Object obj = type.getConstructor().newInstance();
                Field[] declaredFields = type.getDeclaredFields();
                String fieldName = null;
                for (Field declaredField : declaredFields) {
                    String name = declaredField.getName();
                    fieldName = name.substring(0,1).toUpperCase() + name.substring(1);
                    Method getMethod = type.getMethod("get"+fieldName);
                    Method setMethod = type.getMethod("set"+fieldName, declaredField.getType());
                    Object invoke = getMethod.invoke(value);
                    setMethod.invoke(obj, invoke);
                }
                if(obj != null){
                    return obj;
                }
            } catch (InstantiationException e1) {
                throw new RuntimeException(e1);
            } catch (IllegalAccessException e2) {
                throw new RuntimeException(e2);
            } catch (InvocationTargetException e3) {
                throw new RuntimeException(e3);
            } catch (NoSuchMethodException e4) {
                throw new RuntimeException(e4);
            }
            return value;
        }
    }

4、打包测试

通过maven进行打包后替换原来的jar包并再次进行测试,运行结果如下:

 可以看出==比较后的结果为false,==比较对于引用数据类型比较的是地址,结果为false显示出两个对象的内存地址空间不一致。

 5、番外

Apache Commons BeanUtils组件里需要加载的组件较多,比如内置的Log等等,因此导致该组件比较笨重,市面上也有很多其他的辅助操作Bean对象的组件库,如Spring BeanUtils和BeanCopies。较之Apache的BeanUtils,Spring的较为轻量级,BeanCopies操作字节码的方式速度更快。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值