大彬大约 3 分钟

最近内部组件升级到spring5.3.x的时候对象拷贝内容不全,定位分析总结如下(可直接拉到最后看结论和解决办法):

1.现象:

源对象的类里有个内部类的成员变量,是List类型,,List的元素类型是自己的内部静态类

目标对象的类里有个内部类的成员变量,也是List类型,List的元素类型是自己的内部静态类

源对象的代码示例如下(省略了get set方法):

/**
 * 源对象
 * @author dabin
 *
 */
public class Rsp_07300240_01 {
    private int totals;
    private List<Contract> contracts;//合同列表
    static public class Contract{
        private String constractId;//合同编号
        private String constractName;//合同名称
        private String type;//合同类型
        private String fileId;//fps文件id
        private String fileHash;//fps文件hash
        @Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("Rsp_07300240_01.Contract [constractId=");
            builder.append(constractId);
            builder.append(", constractName=");
            builder.append(constractName);
            builder.append(", type=");
            builder.append(type);
            builder.append(", fileId=");
            builder.append(fileId);
            builder.append(", fileHash=");
            builder.append(fileHash);
            builder.append("]");
            return builder.toString();
        }
    }
}

目标对象的代码示例如下(省略了get set方法):

public class Rsp_04301099_01 {
    @RmbField(seq = 1, title = "总条数")
    private int totals;
    @RmbField(seq = 2, title = "合同列表")
    // 这里是自己的内部类
    private List<Contract> contracts;// 合同列表
    static public class Contract{
        private String constractId;//合同编号
        private String constractName;//合同名称
        private String type;//合同类型
        private String fileId;//fps文件id
        private String fileHash;//fps文件hash
@Override
        public String toString() {
            StringBuilder builder = new StringBuilder();
            builder.append("Rsp_04301099_01.Contract [constractId=");
            builder.append(constractId);
            builder.append(", constractName=");
            builder.append(constractName);
            builder.append(", type=");
            builder.append(type);
            builder.append(", fileId=");
            builder.append(fileId);
            builder.append(", fileHash=");
            builder.append(fileHash);
            builder.append("]");
            return builder.toString();
        }
    }
}

单元测试验证代码如下:

public class SpringBeanCopyUtilTest {
    @Test
    public void testBeanCopy() {
        Rsp_07300240_01 orgResp = new Rsp_07300240_01();
        orgResp.setTotals(100);
        List<Rsp_07300240_01.Contract> contracts = new ArrayList<>();
        Rsp_07300240_01.Contract cc = new Rsp_07300240_01.Contract();
        cc.setConstractId("aaa");
        contracts.add(cc);
        orgResp.setContracts(contracts);
        Rsp_04301099_01 destResp = new Rsp_04301099_01();
        System.out.println("源对象的值:" + orgResp);
        System.out.println("复制前的值:" + destResp);
        BeanUtils.copyProperties(orgResp, destResp);
        System.out.println("Spring版本" + BeanUtils.class.getPackage().getImplementationVersion());
        System.out.println("复制后的值:" + destResp);
//        if (destResp.getContracts() != null && destResp.getContracts().size() > 0) {
//            System.out.println("复制后的list成员类型是:" + destResp.getContracts().get(0));
//        }
    }
}

分别依赖spring 5.2.4和5.3.9版本,运行结果如下:

源对象的值:Rsp_07300240_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]]
复制前的值:Rsp_04301099_01 [totals=0, contracts=null]

Spring版本5.2.4.RELEASE
复制后的值:Rsp_04301099_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]]

Spring版本5.3.9
复制后的值:Rsp_04301099_01 [totals=100, contracts=null]

2.分析

可以看到在依赖spring 5.3.x的时候,contracts的值是没有复制过来的。

但是也可以看到在依赖spring 5.2.x的时候,contracts的值是直接设置的引用,List的成员变量类型是 Rsp_07300240_01.Contract,Rsp_04301099_01.Contract。

这个其实也是有问题的。但是为啥在业务逻辑中没有暴雷呢?

经核实,业务代码中,是在返回应答对象之前执行的 org.springframework.beans.BeanUtils.copyProperties 操作,执行完之后,立即返回了对象,然后内部使用的框架,直接使用jackson进行系列化,此时类型信息已经擦除,不涉及类型转换,所以正常生成了json字符串。

而上面示例中被注释的代码里,如果启用的话,测试的时候就会立即报错,提示类型转换异常。

对比代码可以发现:

经过一番搜索,原来是在2019年的时候就有人向Spring社区提了bug,然后spring增加了泛型判断逻辑,杜绝了错误的赋值,在5.3.x中修复了这个bug。

https://github.com/spring-projects/spring-framework/issues/24187open in new window

由于平时大部分使用场景都是执行BeanUtils.copyProperties后立即取出里面的对象进行操作,这种情况下,就会提前触发bug,然后调用方自己想办法规避掉spring bug。

恰好内部使用的xx框架在BeanUtils.copyProperties之后没有显式的操作成员对象,因此一直没有触发bug,直到升级到spring 5.3.x时才暴雷。

3.解决办法:

先回退版本,然后检视所有调用BeanUtils.copyProperties的地方,针对触发bug的这种场景优化代码,比如把两个内部静态class合并使用一个公共的class。

Loading...