耗尽 發表於 2025-7-4 15:55:00

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

<h1 id="背景">背景</h1>
<p>某项目某个功能点是接受前端传参,将其存入MongoDB。这个传参的核心数据是一个二维数组<code>List&lt;List&lt;Object&gt;&gt;</code>,可以放字符串、整型,也可以放null。</p>
<p>在测试时发现,前端明明传的是整数,查出来却变成了字符串,比如<code>1234</code>变成了<code>"1234"</code>。经过排查发现,问题出在公司内部使用的一个Bean复制工具类,这个工具类简单封装了DozerMapper,主要功能是将一个Bean复制成一个新的Bean,并且允许这两个Bean的Class不同,从而完成各种类型转换,如:VO &lt;-&gt; Model、Model &lt;-&gt; DO、DO &lt;-&gt; DTO等。</p>
<p>为了快速修复问题从而不影响项目进度,我手写了前端传参和MongoDB的Entity类的转换逻辑,规避了这个问题。这个工具类在公司内部的代码中大量使用,问题的根因是什么?为了搞明白,我写了一个简单的demo,通过debug这部分代码来一探究竟。</p>
<h2 id="关于dozermapper">关于DozerMapper</h2>
<p>DozerMapper有一些高级用法和对应的传参,但是日常中仅仅用到DozerBeanMapperBuilder.buildDefault()来处理。</p>
<p>DozerMapper的官方github,在mvnrepository上可以看到它的最新版本是7.0.0。</p>
<p>公司的工具类用的是6.5.2,也就是6.x的最后一个版本。经验证:</p>
<ul>
<li>7.0.0和6.5.2都有这个bug</li>
<li>6.5.2可以运行在JDK8,7.0.0必须运行在JDK11及以上</li>
</ul>
<p><strong>本文基于JDK8+DozerMapper6.5.2分析。</strong></p>
<h1 id="问题简化和复现">问题简化和复现</h1>
<p>将实际的传参简化如下。该类必须有无参数构造器,否则DozerMapper创建Bean时会报错。</p>
<pre><code class="language-java">publicclass ListObjectWrapper {
    private List&lt;Object&gt; list;

    public ListObjectWrapper() {
    }

    public ListObjectWrapper(List&lt;Object&gt; list) {
      this.list = list;
    }

    public List&lt;Object&gt; getList() {
      return list;
    }

    public void setList(List&lt;Object&gt; list) {
      this.list = list;
    }
}
</code></pre>
<p>对应的测试代码:</p>
<pre><code class="language-java">public class Test {
    public static void main(String[] args) {
      Mapper mapper = DozerBeanMapperBuilder.buildDefault();
      List&lt;Object&gt; list = new ArrayList&lt;Object&gt;();
      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);
      }
    }
}
</code></pre>
<p>可见,wrapper2的list里的元素全部变成了String:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703164150015-920737364.png" alt="image" loading="lazy"></p>
<h1 id="问题定位">问题定位</h1>
<p>进行debug时,发现在对<code>456</code>调用<code>primitiveConverter.convert()</code>时,此时是知道该元素类型是Integer,调用的返回值却成了字符串:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703165604767-278908127.png" alt="image" loading="lazy"></p>
<p>深入一层,可以看到convert()做了两件事:先确认使用哪个Converter,然后由这个Converter进行实际的转换。这里暗藏了问题:取Converter时,没有用原始数据的实际类型信息,而是取的是Object(这里为什么是Object,接下来会继续深入探讨):<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703170738102-475261105.png" alt="image" loading="lazy"></p>
<p>Object类型取不到对应的Converter,就由以下的分支判断,最后还是取不到,就是使用了StringConstructorConverter:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703171216811-913530903.png" alt="image" loading="lazy"><br>
StringConstructorConverter的内部调用了StringConverter,实际上做的只不过是调用了toString(),因此<code>456</code>变成了<code>"456"</code>。</p>
<h2 id="寻根究底">寻根究底</h2>
<h3 id="ojbect类型是从哪里取的">Ojbect类型是从哪里取的?</h3>
<p>取Converter时,destFieldType=java.lang.Object,是怎么来的?直觉上,我认为是从List<object>的类型参数上取的。再次从头debug,可以看到是<code>addOrUpdateToList()</code>设置的:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703172514831-304394168.png" alt="image" loading="lazy"><p></p>
<p>深入进去,可以看到在这个场景下取的是目标对象的Hint,而非原始值的类型:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703172909943-856653546.png" alt="image" loading="lazy"></p>
<h3 id="目标对象的hint是怎么生成的">目标对象的Hint是怎么生成的?</h3>
<p>再次重新debug,回到相对上层的位置,可以看到这里设置的destHintContainer,genericType.getName()就是java.lang.Object:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703173615021-1304346726.png" alt="image" loading="lazy"></p>
<p>跟着getGenericType()及后续的propertyDescriptor.genericType()再深入两层就可以看到,是从目标对象的写方法的入参上取到泛型的实际类型也就是java.lang.Ojbect的:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703174241910-951462949.png" alt="image" loading="lazy"><br>
至此,原因已完整呈现。</p>
<h1 id="引申1给list褪去bean的外衣">引申1———给List褪去Bean的外衣</h1>
<p>根据上面的分析,List<object>如果直接做复制,应该也是有问题的?验证一把发现,确实是这样,依然有错误:<p></p>
<pre><code class="language-java">public class Test1 {
    public static void main(String[] args) {
      Mapper mapper = DozerBeanMapperBuilder.buildDefault();
      List&lt;Object&gt; list = new ArrayList&lt;Object&gt;();
      list.add("123");
      list.add(456);
      list.add(null);
      list.add(new Date());
      List&lt;Object&gt; 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);
      }
    }
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250703174652269-2146760769.png" alt="image" loading="lazy"></p>
<p>公司的工具类单独写了一个List的复制方法mapList(),对List里的元素逐项调用DozerMapper。不过这个工具类里的方法仍然无法正确复制List<object>,并且遇到null元素会报错。由于是内部的工具类,就不展开讨论了。<p></p>
<p>那么一开始为什么不直接用List<object>做测试呢?潜意识中我给List<object>套了一层,作为bean的成员变量复制的,回想起来可能是在上家公司养成的编程习惯。为什么这么说?可以看看后面的“替代方案调研1——BeanUtils”章节。<p></p>
<h1 id="引申2类的继承如何处理">引申2——类的继承如何处理?</h1>
<p>既然Object是Java里一切类的基类,一个存放了基类对象和继承类对象的容器是否能正确处理呢?根据直觉,应该是不能,实际情况也和直觉一样。读者可以用下面的代码自行验证:</p>
<pre><code class="language-java">public class Test3 {

    public static void main(String[] args) {
      Mapper mapper = DozerBeanMapperBuilder.buildDefault();
      List&lt;Parent&gt; list1 = new ArrayList&lt;&gt;();
      list1.add(new Parent("张三"));
      list1.add(new Child("张四", "张三"));
      List&lt;Parent&gt; 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;
      }
    }
}
</code></pre>
<p>执行结果提示,list2的两个对象都是Parent类型。</p>
<h1 id="替代方案调研1beanutils">替代方案调研1——BeanUtils</h1>
<p>我的前司有同事是用BeanUtils做对象复制的,同名工具类很多,这里的完整类名是org.apache.commons.beanutils.BeanUtils。<br>
按照之前的讨论,继续做测试。BeanUtils有个麻烦的地方在于,你要手动编写它的异常处理代码:</p>
<details>
<summary>点击查看代码</summary>
<pre><code>      ListObjectWrapper wrapper3 = new ListObjectWrapper();
      try {
            BeanUtils.copyProperties(wrapper3, wrapper1);
      } catch (Exception e) {
            e.printStackTrace();
      }
      // 正确复制
      System.out.println(wrapper3);
      List&lt;Object&gt; list4 = new ArrayList&lt;&gt;();
      try {
            BeanUtils.copyProperties(list4, list);
      } catch (Exception e) {
            e.printStackTrace();
      }
      // 复制完list4是空的
      System.out.println(list4);
</code></pre>
</details>
<p>结果是,List<object>本身不能直接被复制,调用后仍然是空的。但是如果它是一个bean的成员变量,就可以正确复制了。很神奇,这正好解释了我为什么在最初简化场景时要把List<object>放在一个类中,或许是在前司工作的习惯使然?<p></p>
<h1 id="替代方案调研2mapstruct">替代方案调研2——MapStruct</h1>
<p>在研究DozerMapper的问题和解决方案时,我看到有的文章提到MapStruct是DozerMapper的替代方案,并且速度也更快一些,因此做了一个简单的调研。</p>
<h2 id="依赖处理">依赖处理</h2>
<p>MapStruct官网上有一个简单的Demo,直接照搬是运行不起来的,要处理一些依赖。以maven为例,pom.xml要添加以下内容:</p>
<ul>
<li>MapStruct依赖</li>
<li>注解处理配置中添加mapstruct-processor</li>
<li>如果项目使用了Lombok,还需要在注解处理配置中增加lombok的配置,否则可能Build失败</li>
</ul>
<p>综合后如下:</p>
<details>
<summary>pom.xml片段</summary>
<pre><code class="language-xml">&lt;dependencies&gt;
    &lt;dependency&gt;
      &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
      &lt;artifactId&gt;mapstruct&lt;/artifactId&gt;
      &lt;version&gt;1.5.5.Final&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
    &lt;plugins&gt;
      &lt;plugin&gt;
            &lt;groupId&gt;org.apache.maven.plugins&lt;/groupId&gt;
            &lt;artifactId&gt;maven-compiler-plugin&lt;/artifactId&gt;
            &lt;version&gt;3.13.0&lt;/version&gt;
            &lt;configuration&gt;
                &lt;source&gt;17&lt;/source&gt;
                &lt;target&gt;17&lt;/target&gt;
                &lt;annotationProcessorPaths&gt;
                  &lt;!-- 如果使用 Lombok --&gt;
                  &lt;path&gt;
                        &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
                        &lt;artifactId&gt;lombok&lt;/artifactId&gt;
                        &lt;version&gt;1.18.32&lt;/version&gt;
                  &lt;/path&gt;
                  &lt;!-- MapStruct 处理器 --&gt;
                  &lt;path&gt;
                        &lt;groupId&gt;org.mapstruct&lt;/groupId&gt;
                        &lt;artifactId&gt;mapstruct-processor&lt;/artifactId&gt;
                        &lt;version&gt;1.5.5.Final&lt;/version&gt;
                  &lt;/path&gt;
                &lt;/annotationProcessorPaths&gt;
            &lt;/configuration&gt;
      &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;
</code></pre>
</details>
<p>依赖是否配置正确,可以通过以下两步验证:</p>
<ul>
<li>编译是否通过</li>
<li>编译完成后,target/gnerated-souces/annotations下是否有编写的接口对应的实现</li>
</ul>
<h2 id="mapper和测试代码">Mapper和测试代码</h2>
<p>由于被测的类比较简单,不需要做转换前后的字段映射,因此对应Mapper也很简单:</p>
<details>
<summary>点击查看ListObjectWrapperMapper</summary>
<pre><code class="language-java">@Mapper
public interface ListObjectWrapperMapper {
    ListObjectWrapperMapper INSTANCE = Mappers.getMapper(ListObjectWrapperMapper.class);

    ListObjectWrapper map(ListObjectWrapper wrapper);
}
</code></pre>
</details>
<p>对应地,测试代码如下:</p>
<details>
<summary>点击查看代码</summary>
<pre><code class="language-java">public class Test2 {
    public static void main(String[] args) {
      List&lt;Object&gt; list = new ArrayList&lt;&gt;();
      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);
      }
    }
}
</code></pre>
</details>
<p>运行时可见,List<object>中的元素按照原本的类型被复制了过去:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250704144525752-646372686.png" alt="image" loading="lazy"><p></p>
<p>如果转换前后的类,字段不同名,可以用<code>@Mapping</code>来指定。MapStruct的编程接口是比较丰富且强大的,读者可以自行研究。</p>
<p>那么,参考“引申2————类的继承如何处理?”这一节,使用MapStruct是否能正确映射呢?答案是肯定的,新的List里两个元素类型分别是Parent和Child:<br>
<img src="https://img2024.cnblogs.com/blog/228024/202507/228024-20250704151918552-1805039593.png" alt="image" loading="lazy"></p>
<h1 id="小结">小结</h1>
<ul>
<li>当目标容器的泛型类型参数是Object类型时,或者当容器中存放了泛型类型参数的子类对象时,DozerMapper的默认用法无法按照元素的实际类型正确地处理</li>
<li>BeanUtils可以正确复制成员变量包括List<object>的对象,但是不能直接复制List<object>本身;此外还要做异常处理
<li>MapStruct作为DozerMapper的替换时,可以正确处理第一种情况的转换,不过用法显然不如DozerMapper简单:必须编写转换接口、明确入参和返回值的转换方法。但优点是有丰富而强大的相关注解,可以通过注解指定不同名和类型的字段映射。</li>

</object></object></li></ul></object></p></object></object></p></object></object></p></object></p></object></p></object></p>

</div>
<div id="MySignature" role="contentinfo">
    <div id="AllanboltSignature">
      <p id="PSignature" style="border-top: #e0e0e0 1px dashed; border-right: #e0e0e0 1px dashed; border-bottom: #e0e0e0 1px dashed; border-left: #e0e0e0 1px dashed;
            padding-top: 10px;padding-right: 10px;padding-bottom: 10px;padding-left: 60px;
            font-family: 微软雅黑; font-size:11px;">
            <br />
            作者:五岳
            <br />
            出处:http://www.cnblogs.com/wuyuegb2312
            <br />
            对于标题未标注为“转载”的文章均为原创,其版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
      </p>
     </div><br><br>
来源:https://www.cnblogs.com/wuyuegb2312/p/18963780
頁: [1]
查看完整版本: 深入研究使用DozerMapper复制List<Ojbect>前后元素类型不一致的问题