最近遇到一个工具类的问题比较容易出错,问题比较诡异,因为是通用的工具类,因此还是要搞清楚根本原因,避免误用。
由于领域模型之间的转换手写getter setter代码会很冗余并且容易出错,然而复制对象属性值是高频操作,因此各种库有开源实现用以复制对象属性,常见的有spring和apache的实现,个人更推荐spring的版本,根据经验发现spring的BUG更少。这次在项目中用一个现成的封装工具类来复制属性,发现一个比较诡异的报错,查看其实现用的org.modelmapper实现复制属性,这次就来看一下它的实现到底是怎样的,首先报错代码demo如下:
/**
* 复制的目标model
*/
static class Target {
private Long id;
// getter setter略
}
/**
* 复制的来源model
*/
static class Source {
private Long skuId;
private Long itemId;
// getter setter略
}
public static void main(String[] args) {
Source source = new Source();
source.setItemId(1L);
source.setSkuId(1L);
Target target = new Target();
ModelMapper modelMapper = new ModelMapper();
// 复制model属性
modelMapper.map(source, target);
System.out.println(target);
}
报错堆栈:
Exception in thread "main" org.modelmapper.ConfigurationException: ModelMapper configuration errors:
1) The destination property Target.setId() matches multiple source property hierarchies:
Source.getSkuId()
Source.getItemId()
1 error
at org.modelmapper.internal.Errors.throwConfigurationExceptionIfErrorsExist(Errors.java:241)
at org.modelmapper.internal.ImplicitMappingBuilder.matchDestination(ImplicitMappingBuilder.java:150)
at org.modelmapper.internal.ImplicitMappingBuilder.build(ImplicitMappingBuilder.java:81)
at org.modelmapper.internal.TypeMapStore.getOrCreate(TypeMapStore.java:108)
at org.modelmapper.internal.TypeMapStore.getOrCreate(TypeMapStore.java:81)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:99)
at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:60)
at org.modelmapper.ModelMapper.mapInternal(ModelMapper.java:529)
at org.modelmapper.ModelMapper.map(ModelMapper.java:413)
我的预期不应该匹配到值,而这报错含义就是id匹配到source中Source.getSkuId(),Source.getItemId()两个属性值,这非常的奇怪。
其调用链路大概如下:
也就是说到底层其会调用一个Matcher去遍历属性匹配,而就是在这个匹配的过程中发生了问题。
大概分析下调用链路:
- 调用开始,map传递来源对象和目标对象。
- Types类反射获取到对象类型并调用modelmapper自己实现的一个Engine默认实现开始map
- 把需要进行mapper的上下文进行解析并封装
- 可能是解析对象匹配属性对的过程比较消耗资源,所以这里主要看当前mapper对象有没缓存的匹配转换工具等,如果有就可以直接用,否则创建。
- 创建匹配关系
- 这里终于开始目标字段的遍历匹配了
- 这里对目标对象id进行匹配
- 这里匹配的结果是itemId与id的匹配是true,问题的根本也就在这里
查看它的匹配策略有三种实现,这里使用的方式是标准匹配 - 这里属性对的匹配是根据驼峰拆分,统计匹配数量
itemId与id的匹配数量是1,skuId与id的匹配也是1,所以发现两个来源属性导致导致异常。
解决方案:
看源码分析其实可以看到关键的问题在于属性匹配的策略不符合我们的预期,竟然把标准匹配策略设计成这样的逻辑,至少我在大多数情况下不会依赖驼峰拆分匹配数量去复制属性,这样在属性多的情况下很难控制,更多情况应该是严格的一个匹配策略,它原生的实现的确也有一个严格匹配策略 StrictMatchingStrategy.
修改方法:
Source source = new Source();
source.setItemId(1L);
source.setSkuId(1L);
Target target = new Target();
ModelMapper modelMapper = new ModelMapper();
// 修改匹配策略为严格
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
modelMapper.map(source, target);
System.out.println(target);
这样就不会报错了。