使用ModelMapper的一次踩坑经历

警告:本文代码较多,请耐心观看java

在实际项目中,咱们经常须要把两个类似的对象相互转换,其目的是在对外提供数据时须要将一部分敏感数据(例如:密码、加密token等)隐藏起来。最普通的方法是,新建一个对象,将须要的值逐个set进去。若是有多组须要这样转换的对象,那么就须要作不少只是get/set这样无心义的工做。web

在这样的背景下,ModelMapper诞生了,它是一个简单高效、智能的对象映射工具。它的使用很是简单,首先添加maven依赖
微信


<dependency>
 <groupId>org.modelmapper</groupId>
 <artifactId>modelmapper</artifactId>
 <version>2.1.1</version>
</dependency>


而后就能够直接new出一个ModelMapper对象,而且调用其map方法将指定对象的值映射到另外一个对象上了。app


使用方法今天不作过多介绍,你们能够自行Google,找到ModelMapper的相关文档进行学习。今天要分享的时前几天无心间踩到的一个坑。maven


我有两个类,PostDO和PostVO(这里只截取了部分字段,所以两个类的含义也不作解释了):编辑器

public class PostDO {
   private Long id;
   private String commentId;
   private Long postId;
   private int likeNum;
}


public class PostVO {
   private Long id;
   private boolean like;
   private int likeNum;
}


在一个方法中,我试图将PostDO的一个对象映射到PostVO,所以我进行以下操做:ide


public class Application {
   public static void main(String[] args) {
       ModelMapper modelMapper = new ModelMapper();
PostDO postDO = PostDO.builder().id(3L).likeNum(0).build();
       PostVO postVO = modelMapper.map(postDO, PostVO.class);
       System.out.println(postVO);
   }
}


执行结果是这样的:工具


PostVO(id=3, like=false, likeNum=0)


无异常,项目中likeNum字段的值是随着项目的进行递增的。当likeNum增长到2时,异常出现了:post


Exception in thread "main" org.modelmapper.MappingException: ModelMapper mapping errors:

1) Converter org.modelmapper.internal.converter.BooleanConverter@497470ed failed to convert int to boolean.

1 error
 at org.modelmapper.internal.Errors.throwMappingExceptionIfErrorsExist(Errors.java:380)
 at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:79)
 at org.modelmapper.ModelMapper.mapInternal(ModelMapper.java:554)
 at org.modelmapper.ModelMapper.map(ModelMapper.java:387)
 at Application.main(Application.java:7)
Caused by: org.modelmapper.MappingException: ModelMapper mapping errors:

1) Error mapping 2 to boolean

1 error
 at org.modelmapper.internal.Errors.toMappingException(Errors.java:258)
 at org.modelmapper.internal.converter.BooleanConverter.convert(BooleanConverter.java:49)
 at org.modelmapper.internal.converter.BooleanConverter.convert(BooleanConverter.java:27)
 at org.modelmapper.internal.MappingEngineImpl.convert(MappingEngineImpl.java:298)
 at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:108)
 at org.modelmapper.internal.MappingEngineImpl.setDestinationValue(MappingEngineImpl.java:238)
 at org.modelmapper.internal.MappingEngineImpl.propertyMap(MappingEngineImpl.java:184)
 at org.modelmapper.internal.MappingEngineImpl.typeMap(MappingEngineImpl.java:148)
 at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:113)
 at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:70)
 ... 3 more


提示int类型不能转换成boolean型,很明显。ModelMapper是将like字段映射到likeNum了。那么ModelMapper到底是怎样进行映射的呢,咱们一块儿来看一下ModelMapper的源码。学习

ModelMapper利用反射机制,获取到目标类的字段,并生成指望匹配的键值对,相似于这样。

接着对这些键值对进行遍历,逐个寻找源类中能够匹配的字段。首先会根据目标字段判断是否存在对应的映射,


Mapping existingMapping = this.typeMap.mappingFor(destPath);
if (existingMapping == null) {
   this.matchSource(this.sourceTypeInfo, mutator);
   this.propertyNameInfo.clearSource();
   this.sourceTypes.clear();
}


若是不存在,就调用matchSource方法,在源类中根据匹配规则寻找能够匹配的字段。匹配过程当中,首先会判断目标字段的类型是否在类型列表中存在,若是存在,则能够根据名称,加入匹配的mappings中;若是不存在,则须要判断converterStore中是否存在可以应用于该字段的转换器。


if (this.destinationTypes.contains(destinationMutator.getType())) {
   this.mappings.add(new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), true));
} else {
   TypeMap<?, ?> propertyTypeMap = this.typeMapStore.get(accessor.getType(), destinationMutator.getType(), (String)null);
   PropertyMappingImpl mapping = null;
   if (propertyTypeMap != null) {
       Converter<?, ?> propertyConverter = propertyTypeMap.getConverter();
       if (propertyConverter == null) {
           this.mergeMappings(propertyTypeMap);
       } else {
           this.mappings.add(new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), propertyTypeMap.getProvider(), propertyConverter));
       }

       doneMatching = this.matchingStrategy.isExact();
   } else {
       Iterator var9 = this.converterStore.getConverters().iterator();

       while(var9.hasNext()) {
           ConditionalConverter<?, ?> converter = (ConditionalConverter)var9.next();
           MatchResult matchResult = converter.match(accessor.getType(), destinationMutator.getType());
           if (!MatchResult.NONE.equals(matchResult)) {
               mapping = new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), false);
               if (MatchResult.FULL.equals(matchResult)) {
                   this.mappings.add(mapping);
                   doneMatching = this.matchingStrategy.isExact();
                   break;
               }

               if (!this.configuration.isFullTypeMatchingRequired()) {
                   this.partiallyMatchedMappings.add(mapping);
                   break;
               }
           }
       }
   }

   if (mapping == null) {
       this.intermediateMappings.put(accessor, new PropertyMappingImpl(this.propertyNameInfo.getSourceProperties(), this.propertyNameInfo.getDestinationProperties(), false));
   }
}


默认的转换器有11中:

找到对应的converter后,converter的map方法返回一个MatchResult,MatchResult有三种结果:FULL、PARTIAL和NONE(即所有匹配,部分匹配和不匹配)。注意,这里有一个部分匹配,也就是我所踩到的坑,在对like进行匹配是,likeNum就被定义为部分匹配。所以,当likeNum大于2时,就不能被转换成boolean类型。


这里解决方法有两种,一种是在设置中,规定必须字段名彻底匹配;另外一种就是将匹配策略定义为严格。设置方法以下:


modelMapper.getConfiguration().setFullTypeMatchingRequired(true);
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);


到这里,ModelMapper会选出较为合适的源字段,可是若是匹配要求不高的话,ModelMapper可能会筛选出多个符合条件的字段,所以,还须要进一步过滤。


PropertyMappingImpl mapping;
if (this.mappings.size() == 1) {
   mapping = (PropertyMappingImpl)this.mappings.get(0);
} else {
   mapping = this.disambiguateMappings();
   if (mapping == null && !this.configuration.isAmbiguityIgnored()) {
       this.errors.ambiguousDestination(this.mappings);
   }
}


这里咱们看到,若是匹配到的结果只有1个,那么就返回这个结果;若是有多个,则会调用disambiguateMappings方法,去掉有歧义的结果。咱们来看一下这个方法。


private PropertyMappingImpl disambiguateMappings() {
   List<ImplicitMappingBuilder.WeightPropertyMappingImpl> weightMappings = new ArrayList(this.mappings.size());
   Iterator var2 = this.mappings.iterator();

   while(var2.hasNext()) {
       PropertyMappingImpl mapping = (PropertyMappingImpl)var2.next();
       ImplicitMappingBuilder.SourceTokensMatcher matcher = this.createSourceTokensMatcher(mapping);
       ImplicitMappingBuilder.DestTokenIterator destTokenIterator = new ImplicitMappingBuilder.DestTokenIterator(mapping);

       while(destTokenIterator.hasNext()) {
           matcher.match(destTokenIterator.next());
       }

       double matchRatio = (double)matcher.matches() / ((double)matcher.total() + (double)destTokenIterator.total());
       weightMappings.add(new ImplicitMappingBuilder.WeightPropertyMappingImpl(mapping, matchRatio));
   }

   Collections.sort(weightMappings);
   if (((ImplicitMappingBuilder.WeightPropertyMappingImpl)weightMappings.get(0)).ratio == ((ImplicitMappingBuilder.WeightPropertyMappingImpl)weightMappings.get(1)).ratio) {
       return null;
   } else {
       return ((ImplicitMappingBuilder.WeightPropertyMappingImpl)weightMappings.get(0)).mapping;
   }
}


ModelMapper定义了一个权重,来判断源字段是否有歧义,这里根据驼峰式的规则(也能够设置为下划线),将源和目标字段名称进行拆分,根据 匹配数量/源token数+目标token数,获得一个匹配的比率,比率越大,说明匹配度越高。最终取得匹配权重最大的那个字段。其余字段被认为是有歧义的。



截至目前,默认的ModelMapper的map方法的工做原理已经介绍完了,中间可能有些遗漏的细节,或者哪里有说的不明白的地方,欢迎你们和我一块儿讨论。你们在用到ModelMapper时必定要注意字段名,若是有相近的字段名,必须认真核对匹配是否正确,必要时就采用严格匹配策略。


对ModelMapper的基础使用方法不熟悉的同窗能够访问它的官网查看相关文档:

http://modelmapper.org/


最后,感谢发。

往期精彩回顾
代码洁癖系列(八):迭代的原则
实用的工具,有趣的人

本文分享自微信公众号 - 代码洁癖患者(Jackeyzhe2018)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。

相关文章
相关标签/搜索