![](http://static.javashuo.com/static/loading.gif)
警告:本文代碼較多,請耐心觀看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/
最後,感謝贊和轉發。
![](http://static.javashuo.com/static/loading.gif)
本文分享自微信公衆號 - 代碼潔癖患者(Jackeyzhe2018)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。