背景

某项目某个功能点是接受前端传参,将其存入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. 【Linux】5.3 Shell字符串

    Shell 字符串 字符串是shell编程中最常用最有用的数据类型(除了数字和字符串,也没啥其它类型好用了),字符串可以用单引号,也可以用双引号,也可以不用引号.单双引号的区别跟PHP类似. 1. 单 ...

  2. 移动应用APP购物车(店铺系列二)

    今天还是说移动app开发,店铺系列文章,我们经常去超市使用购物车,即一个临时的储物空间,用完清空回收.我大兄弟说, 平时很忙,录入订单的目录很多,临时有事回来要可以继续填写,提交订单后才算结束,这就是 ...

  3. golang实现命令行程序的使用帮助

    通过flag包我们可以很方便的实现命令行程序的参数标志, 接下来我们来看看如何实现命令行程序的使用帮助, 通常以参数标志-h或--help的形式来使用. 自动生成使用帮助 我们只需要声明其他参数标志, ...

  4. AR 智能生态鱼缸组态远控平台 | 图扑软件

    在工业 4.0 和物联网技术的推动下,万物互联正重塑行业管理模式.组态远控系统作为高效管控的核心,打破了设备孤立状态,实现数据实时交互.以智能生态鱼缸为例,图扑软件低代码数字孪生平台通过集成前沿技术, ...

  5. 子图,生成子图(Spanning Subgraph),导出子图(Induced Subgraph)的定义

    原图G用\(G=(V,E)\)表示,\(V\)是\(G\)中的所有顶点的集合:\(E\)是\(G\)中所有边的集合. 子图 定义:子图\(G '\)中的所有顶点和边均包含于原图\(G\).即\(E'∈ ...

  6. springboot将vo生成文件到目录

    依赖 org.springframework spring-mock 2.0.8 com.alibaba fastjson 1.2.62 service实现 public RestResponseBo ...

  7. AQS的release(int)方法底层源码

    一.定义 release(int) 是 AQS(AbstractQueuedSynchronizer)中的一个核心方法,用于在独占模式下释放同步状态.如果释放成功,则会唤醒等待队列中的后继节点,使其有 ...

  8. 线程,yield()

    一.定义:暂停当前正在执行的线程对象,并执行其他线程 yield()应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会. 因此,使用yield()的目的是让相同优先级的 ...

  9. java基础之String类、Math类、Arrays类、Collections类

    一.String类 概述:程序中所有的双引号字符串,都是String类的对象.(就算没有new,照样算是) 特点: 1.字符串的内容用不可变[重点] 2.因为字符串[String对象]是不可变的,所以 ...

  10. Python3多线程

    一.进程和线程 进程:是程序的一次执行,每个进程都有自己的地址空间.内存.数据栈及其他记录运行轨迹的辅助数据. 线程:所有的线程都运行在同一个进程当中,共享相同的运行环境.线程有开始.顺序执行和结束三 ...