背景

某项目某个功能点是接受前端传参,将其存入MongoDB。这个传参的核心数据是一个二维数组List<List<Object>>,可以放字符串、整型,也可以放null。

在测试时发现,前端明明传的是整数,查出来却变成了字符串,比如1234变成了"1234"。经过排查发现,问题出在公司内部使用的一个Bean复制工具类,这个工具类的功能是将一个Bean复制成一个新的Bean,并且允许这两个Bean的Class不同,从而完成各种类型转换,如:VO <-> Model、Model <-> DO、DO <-> DTO等。

为了快速修复问题从而不影响项目进度,我手写了前端传参和MongoDB的Entity类的转换逻辑,规避了这个问题。这个工具类在公司内部的代码中大量使用,问题的根因是什么?为了搞明白,我写了一个简单的demo,通过debug这部分代码来一探究竟。

关于DozerMapper

公司的Bean复制工具类实际是对DozerMapper的简单封装。DozerMapper有一些高级用法和对应的传参,但是日常中仅仅用到DozerBeanMapperBuilder.buildDefault()来处理。

DozerMapper的官方github,在mvnrepository上可以看到它的最新版本是7.0.0。

公司的工具类用的是6.5.2,也就是6.x的最后一个版本。经验证:

  • 7.0.0和6.5.2都有这个bug
  • 6.5.2可以运行在JDK8,7.0.0必须运行在JDK11及以上

本文基于JDK8+DozerMapper6.5.2分析。

问题简化和复现

将实际的传参简化如下。该类必须有无参数构造器,否则DozerMapper创建Bean时会报错。

public  class ListObjectWrapper {
private List<Object> list; public ListObjectWrapper() {
} public ListObjectWrapper(List<Object> list) {
this.list = list;
} public List<Object> getList() {
return list;
} public void setList(List<Object> list) {
this.list = list;
}
}

对应的测试代码:

public class Test {
public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
List<Object> list = new ArrayList<Object>();
list.add("123");
list.add(456);
list.add(null);
list.add(new Date());
ListObjectWrapper wrapper1 = new ListObjectWrapper(list);
ListObjectWrapper wrapper2 = mapper.map(wrapper1, ListObjectWrapper.class);
for (Object value : wrapper2.getList()) {
if(value == null) {
System.out.println(value);
continue;
}
System.out.println("type:" + value.getClass() + ", value=" + value);
}
}
}

可见,wrapper2的list里的元素全部变成了String:

问题定位

进行debug时,发现在对456调用primitiveConverter.convert()时,此时是知道该元素类型是Integer,调用的返回值却成了字符串:

深入一层,可以看到convert()做了两件事:先确认使用哪个Converter,然后由这个Converter进行实际的转换。这里暗藏了问题:取Converter时,没有用原始数据的实际类型信息,而是取的是Object(这里为什么是Object,接下来会继续深入探讨):

Object类型取不到对应的Converter,就由以下的分支判断,最后还是取不到,就是使用了StringConstructorConverter:



StringConstructorConverter的内部调用了StringConverter,实际上做的只不过是调用了toString(),因此456变成了"456"

寻根究底

Ojbect类型是从哪里取的?

取Converter时,destFieldType=java.lang.Object,是怎么来的?直觉上,我认为是从List的类型参数上取的。再次从头debug,可以看到是addOrUpdateToList()设置的:

深入进去,可以看到在这个场景下取的是目标对象的Hint,而非原始值的类型:

目标对象的Hint是怎么生成的?

再次重新debug,回到相对上层的位置,可以看到这里设置的destHintContainer,genericType.getName()就是java.lang.Object:

跟着getGenericType()及后续的propertyDescriptor.genericType()再深入两层就可以看到,是从目标对象的写方法的入参上取到泛型的实际类型也就是java.lang.Ojbect的:



至此,原因已完整呈现。

引申1———给List褪去Bean的外衣

根据上面的分析,List如果直接做复制,应该也是有问题的?验证一把发现,确实是这样,依然有错误:

public class Test1 {
public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
List<Object> list = new ArrayList<Object>();
list.add("123");
list.add(456);
list.add(null);
list.add(new Date());
List<Object> list2 = mapper.map(list, List.class);
for (Object value : list2) {
if(value == null) {
System.out.println(value);
continue;
}
System.out.println("type:" + value.getClass() + ", value=" + value);
}
}
}

公司的工具类单独写了一个List的复制方法mapList(),对List里的元素逐项调用DozerMapper。不过这个工具类里的方法仍然无法正确复制List,并且遇到null元素会报错。由于是内部的工具类,就不展开讨论了。

那么一开始为什么不直接用List做测试呢?潜意识中我给List套了一层,作为bean的成员变量复制的,回想起来可能是在上家公司养成的编程习惯。为什么这么说?可以看看后面的“替代方案调研1——BeanUtils”章节。

引申2——类的继承如何处理?

既然Object是Java里一切类的基类,一个存放了基类对象和继承类对象的容器是否能正确处理呢?根据直觉,应该是不能,实际情况也和直觉一样。读者可以用下面的代码自行验证:

public class Test3 {

    public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
List<Parent> list1 = new ArrayList<>();
list1.add(new Parent("张三"));
list1.add(new Child("张四", "张三"));
List<Parent> list2 = mapper.map(list1, List.class);
for (Parent p : list2) {
System.out.println("type:" + p.getClass() + ", name=" + p.getName());
}
} public static class Parent {
private String name; public Parent(String name) {
this.name = name;
} public Parent() {
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
}
} public static class Child extends Parent {
private String parentName; public Child(String name, String parentName) {
super(name);
this.parentName = parentName;
} public Child() {
} public String getParentName() {
return parentName;
} public void setParentName(String parentName) {
this.parentName = parentName;
}
}
}

执行结果提示,list2的两个对象都是Parent类型。

替代方案调研1——BeanUtils

我的前司有同事是用BeanUtils做对象复制的,同名工具类很多,这里的完整类名是org.apache.commons.beanutils.BeanUtils。

按照之前的讨论,继续做测试。BeanUtils有个麻烦的地方在于,你要手动编写它的异常处理代码:

点击查看代码

```java
ListObjectWrapper wrapper3 = new ListObjectWrapper();
try {
BeanUtils.copyProperties(wrapper3, wrapper1);
} catch (Exception e) {
e.printStackTrace();
}
// 正确复制
System.out.println(wrapper3);
List list4 = new ArrayList<>();
try {
BeanUtils.copyProperties(list4, list);
} catch (Exception e) {
e.printStackTrace();
}
// 复制完list4是空的
System.out.println(list4);
```

结果是,List本身不能直接被复制,调用后仍然是空的。但是如果它是一个bean的成员变量,就可以正确复制了。很神奇,这正好解释了我为什么在最初简化场景时要把List放在一个类中,或许是在前司工作的习惯使然?

替代方案调研2——MapStruct

在研究DozerMapper的问题和解决方案时,我看到有的文章提到MapStruct是DozerMapper的替代方案,并且速度也更快一些,因此做了一个简单的调研。

依赖处理

MapStruct官网上有一个简单的Demo,直接照搬是运行不起来的,要处理一些依赖。以maven为例,pom.xml要添加以下内容:

  • MapStruct依赖
  • 注解处理配置中添加mapstruct-processor
  • 如果项目使用了Lombok,还需要在注解处理配置中增加lombok的配置,否则可能Build失败

综合后如下:

pom.xml片段
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
</dependencies> <build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<source>17</source>
<target>17</target>
<annotationProcessorPaths>
<!-- 如果使用 Lombok -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</path>
<!-- MapStruct 处理器 -->
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>

依赖是否配置正确,可以通过以下两步验证:

  • 编译是否通过
  • 编译完成后,target/gnerated-souces/annotations下是否有编写的接口对应的实现

Mapper和测试代码

由于被测的类比较简单,不需要做转换前后的字段映射,因此对应Mapper也很简单:

点击查看ListObjectWrapperMapper
@Mapper
public interface ListObjectWrapperMapper {
ListObjectWrapperMapper INSTANCE = Mappers.getMapper(ListObjectWrapperMapper.class); ListObjectWrapper map(ListObjectWrapper wrapper);
}

对应地,测试代码如下:

点击查看代码
public class Test2 {
public static void main(String[] args) {
List<Object> list = new ArrayList<>();
list.add("123");
list.add(456);
list.add(null);
list.add(new Date());
ListObjectWrapper wrapper1 = new ListObjectWrapper(list);
ListObjectWrapper wrapper2 = ListObjectWrapperMapper.INSTANCE.map(wrapper1);
for (Object value : wrapper2.getList()) {
if(value == null) {
System.out.println(value);
continue;
}
System.out.println("type:" + value.getClass() + ", value=" + value);
}
}
}

运行时可见,List中的元素按照原本的类型被复制了过去:

如果转换前后的类,字段不同名,可以用@Mapping来指定。MapStruct的编程接口是比较丰富且强大的,读者可以自行研究。

那么,参考“引申2————类的继承如何处理?”这一节,使用MapStruct是否能正确映射呢?答案是肯定的,新的List里两个元素类型分别是Parent和Child:

小结

  • 当目标容器的泛型类型参数是Object类型时,或者当容器中存放了泛型类型参数的子类对象时,DozerMapper的默认用法无法按照元素的实际类型正确地处理
  • BeanUtils可以正确复制成员变量包括List的对象,但是不能直接复制List本身;此外还要做异常处理
  • MapStruct作为DozerMapper的替换时,可以正确处理第一种情况的转换,不过用法显然不如DozerMapper简单:必须编写转换接口、明确入参和返回值的转换方法。但优点是有丰富而强大的相关注解,可以通过注解指定不同名和类型的字段映射。

深入研究使用DozerMapper复制List<Ojbect>前后元素类型不一致的问题的更多相关文章

  1. C++ - 复制容器(container)的元素至还有一个容器

    复制容器(container)的元素至还有一个容器 本文地址: http://blog.csdn.net/caroline_wendy C++复制容器(container)元素, 能够使用标准库(ST ...

  2. 一个高性能的对象属性复制类,支持不同类型对象间复制,支持Nullable<T>类型属性

    由于在实际应用中,需要对大量的对象属性进行复制,原来的方法是通过反射实现,在量大了以后,反射的性能问题就凸显出来了,必须用Emit来实现. 搜了一圈代码,没发现适合的,要么只能在相同类型对象间复制,要 ...

  3. 前端研究CSS之文字与特殊符号元素结合的浏览器兼容性总结

    页面布局里总是会有类似 “文字 | 文字” 的设计样式,不同的浏览器存在严重偏差. 有兼容问题就要解决,下面总结了3种解决方案,分享给大家: 一.系统默认的样式 1.元素换行的段落 <div c ...

  4. 【VBA研究】VBA自己定义函数參数类型不符的错误

    版权声明:本文为博主原创文章.未经博主同意不得转载. https://blog.csdn.net/iamlaosong/article/details/36871769 作者:iamlaosong 1 ...

  5. Revit二次开发 屏蔽复制构件产生的重复类型提示窗

    做了很久码农,也没个写博客的习惯,这次开始第一次写博客. 这个问题也是折腾了我接近一天时间,网上也没有任何的相关博文,于是决定分享一下,以供同样拥有此问题的小伙伴们参考. 内容源于目前在做的一个项目, ...

  6. jq的clone用第二次的时候为什么会复制clone出来的元素(即一变二,二变四)

    原因是clone得到的是一个数组吗,每次再clone的时候,相当于操作了这个数组,肯定就会出现重复,我们只需要取第一个值就可以了,用.first()的方法 jquery(‘item‘).first() ...

  7. c# datarow[] 转换成 datatable, List<T> 转datatable

      c# datarow[] 转换成 datatable, List<T> 转datatable DdataRow[]转成Datatable private DataTable ToDat ...

  8. C#常用处理数据类型转换、数据源转换、数制转换、编码转换相关的扩展

    public static class ConvertExtensions { #region 数据类型转换扩展方法 /// <summary> /// object 转换成string ...

  9. [No0000B9]C# 类型基础 值类型和引用类型 及其 对象复制 浅度复制vs深度复制 深入研究2

    接上[No0000B5]C# 类型基础 值类型和引用类型 及其 对象判等 深入研究1 对象复制 有的时候,创建一个对象可能会非常耗时,比如对象需要从远程数据库中获取数据来填充,又或者创建对象需要读取硬 ...

  10. 关于:1.指针与对象;2.深浅拷贝(复制);3.可变与不可变对象;4.copy与mutableCopy的一些理解

    最近对深浅拷贝(复制)做了一些研究,在此将自己的理解写下来,希望对大家有所帮助.本人尚处在摸索阶段,希望各位予以指正. 本文包括如下方向的探索: 1.指针与对象: 2.深/浅拷贝(复制): 3.可变/ ...

随机推荐

  1. Go初入武林之乘法表

    为统一管理源码, 请到gitee中查看. GoTimesTable

  2. .net WorkFlow 流程会签

    WikeFlow官网:www.wikesoft.com WikeFlow学习版演示地址:workflow.wikesoft.com WikeFlow学习版源代码下载:https://gitee.com ...

  3. Redis的底层数据结构-跳表

    跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的.具有如下性质: 1.由很多层结构组成: 2.每一层都是一个有序的链表,排列顺序为 ...

  4. 小程序组件使用全局样式app.wxss

    Component({ options: { addGlobalClass: true } })

  5. 如何在 Go 中解析 yaml 文件

    Go 语言没有内置解析 yaml 文件的功能,实现 yaml 的解析可以使用第三方库 gopkg.in/yaml.v2 和 gopkg.in/yaml.v3. 下面以解析 config.yml 文件为 ...

  6. 信息资源管理综合题之“ITSM(IT服务管理)和ITIL(基础架构标准库)内容”

    一.在百度百科中,关于IT服务管理有如下描述:专家的研究和大量企业时间表明,在IT项目的生命周期中,大约80%的时间与IT项目运营维护有关,而该阶段的投资仅占整个IT投资的20%,形成了典型的&quo ...

  7. Vim-从放弃到入门

    初识Vim Vim被称为神一样的编译器,人类历史上最好文本编辑器(^_^).学习成本很高,学习路线陡峭.下面列举一些入门的教程: 慕课网-玩转Vim 从放弃到爱不释手 新手必看 Vim实用技巧(第2版 ...

  8. 基于First Order Motion与TTS的AI虚拟主播系统全流程实现教程

    前言:多模态虚拟主播的技术革命 在AI内容生成领域,虚拟主播技术正经历从2D到3D.从固定模板到个性化定制的跨越式发展.本文将深入解析如何通过Python技术栈构建支持形象定制与声音克隆的AI虚拟主播 ...

  9. Evaluate Division——LeetCode进阶路

    原题链接https://leetcode.com/problems/evaluate-division/ 题目描述 Equations are given in the format A / B = ...

  10. 集合操作交并补的三种Java实现

    基本概念 为了便于理解,下面首先介绍集合的三个基本操作:并集.交集和补集.   并集:以属于A或属于B的元素为元素的集合称为A与B的并(集),记作A∪B(或B∪A),读作"A并B" ...