某项目某个功能点是接受前端传参,将其存入MongoDB。这个传参的核心数据是一个二维数组List
,可以放字符串、整型,也可以放null。>
在测试时发现,前端明明传的是整数,查出来却变成了字符串,比如1234
变成了"1234"
。经过排查发现,问题出在公司内部使用的一个Bean复制工具类,这个工具类简单封装了DozerMapper,主要功能是将一个Bean复制成一个新的Bean,并且允许这两个Bean的Class不同,从而完成各种类型转换,如:VO Model、Model DO、DO DTO等。
为了快速修复问题从而不影响项目进度,我手写了前端传参和MongoDB的Entity类的转换逻辑,规避了这个问题。这个工具类在公司内部的代码中大量使用,问题的根因是什么?为了搞明白,我写了一个简单的demo,通过debug这部分代码来一探究竟。
DozerMapper有一些高级用法和对应的传参,但是日常中仅仅用到DozerBeanMapperBuilder.buildDefault()来处理。
DozerMapper的官方github,在mvnrepository上可以看到它的最新版本是7.0.0。
公司的工具类用的是6.5.2,也就是6.x的最后一个版本。经验证:
本文基于JDK8+DozerMapper6.5.2分析。
将实际的传参简化如下。该类必须有无参数构造器,否则DozerMapper创建Bean时会报错。
public class ListObjectWrapper {
private List
对应的测试代码:
public class Test {
public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
List list = new ArrayList();
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"
。
取Converter时,destFieldType=java.lang.Object,是怎么来的?直觉上,我认为是从List 深入进去,可以看到在这个场景下取的是目标对象的Hint,而非原始值的类型: 再次重新debug,回到相对上层的位置,可以看到这里设置的destHintContainer,genericType.getName()就是java.lang.Object: 跟着getGenericType()及后续的propertyDescriptor.genericType()再深入两层就可以看到,是从目标对象的写方法的入参上取到泛型的实际类型也就是java.lang.Ojbect的: 根据上面的分析,List 公司的工具类单独写了一个List的复制方法mapList(),对List里的元素逐项调用DozerMapper。不过这个工具类里的方法仍然无法正确复制List 那么一开始为什么不直接用List 既然Object是Java里一切类的基类,一个存放了基类对象和继承类对象的容器是否能正确处理呢?根据直觉,应该是不能,实际情况也和直觉一样。读者可以用下面的代码自行验证: 执行结果提示,list2的两个对象都是Parent类型。 我的前司有同事是用BeanUtils做对象复制的,同名工具类很多,这里的完整类名是org.apache.commons.beanutils.BeanUtils。 结果是,List 在研究DozerMapper的问题和解决方案时,我看到有的文章提到MapStruct是DozerMapper的替代方案,并且速度也更快一些,因此做了一个简单的调研。 MapStruct官网上有一个简单的Demo,直接照搬是运行不起来的,要处理一些依赖。以maven为例,pom.xml要添加以下内容: 综合后如下: 依赖是否配置正确,可以通过以下两步验证: 由于被测的类比较简单,不需要做转换前后的字段映射,因此对应Mapper也很简单: 对应地,测试代码如下: 运行时可见,List 如果转换前后的类,字段不同名,可以用 那么,参考“引申2————类的继承如何处理?”这一节,使用MapStruct是否能正确映射呢?答案是肯定的,新的List里两个元素类型分别是Parent和Child:addOrUpdateToList()
设置的:
目标对象的Hint是怎么生成的?
至此,原因已完整呈现。引申1———给List褪去Bean的外衣
public class Test1 {
public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
List
引申2——类的继承如何处理?
public class Test3 {
public static void main(String[] args) {
Mapper mapper = DozerBeanMapperBuilder.buildDefault();
List
替代方案调研1——BeanUtils
按照之前的讨论,继续做测试。BeanUtils有个麻烦的地方在于,你要手动编写它的异常处理代码:点击查看代码
ListObjectWrapper wrapper3 = new ListObjectWrapper();
try {
BeanUtils.copyProperties(wrapper3, wrapper1);
} catch (Exception e) {
e.printStackTrace();
}
// 正确复制
System.out.println(wrapper3);
List
替代方案调研2——MapStruct
依赖处理
pom.xml片段
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
@Mapping
来指定。MapStruct的编程接口是比较丰富且强大的,读者可以自行研究。
小结
作者:五岳
出处:http://www.cnblogs.com/wuyuegb2312
对于标题未标注为“转载”的文章均为原创,其版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
参与评论
手机查看
返回顶部