提升76%的关键-在ModelMapper中实现性能提升的几种方法
目录
前言
一、ModelMapper基础知识
1、深入ModelMapper
2、深入Configuration配置
3、深入MappingEngineImpl
二、默认加载模式
1、基础测试代码
三、持续优化,慢慢提升
1、增加忽略字段
2、设置忽略空值模式
3、设置命名模式
4、采用精准匹配模式
四、总结
前言
关于在Java当中把HashMap键值属性转换成JavaBean的关键技术有很多实现方式,在之前的博客中介绍了一种实现方式在Java中使用ModelMapper简化Shapefile属性转JavaBean实战。博客仅介绍了ModelMapper这个框架,它本身是通过对象的反射来进行属性的绑定和赋值的。因此属性的对应寻找上就会耗费一定的时间。后面有朋友联系,说ModelMapper的数据转换性能太差,接口的查询效率大大降低了。原来很快的速度,现在使用了ModelMapper之后,反而不如之前的手动调用SetXXX方法来的快。后来经过交流和现场的复盘,得知在对象转换的过程中,这位朋友一直采用的是ModelMapper的默认转换,设置也是采用默认的配置。因此整个属性的映射过程非常慢。从而导致了整体接口的响应超出了预期。
那么,如何来优化转换速度,提升转换性能呢?这是本文创作的初衷,很多朋友对ModelMapper对象转换的策略和相关配置不是很熟悉。不会设置转换策略,其实通过一些策略的设置,可以很好的进行转换性能的提升。本文即以解析全球主要城市数据为例,重点介绍如何一步一步的优化转换策略和逻辑,将对象的转换时间性能提升76%的过程。希望通过本文的介绍,让您对掌握和优化ModelMapper有更深的了解。
一、ModelMapper基础知识
为了更清晰的了解ModelMapper对象,掌握其转换的思想和原理。这里对ModelMapper的核心类、核心类的属性和方法将进行简单的介绍。
1、深入ModelMapper
为了让大家对ModelMaper有更深入的了解,这里我们对ModelMapper的相关属性和方法进行简单介绍。ModelMapper这个类中有两个最核心的属性,即InheritingConfiguration和MappingEngineImpl,一个是配置器;另外一个是MappingEngineImpl,映射引擎。大家可以在ModelMapper的源码中来查看其实现。
public class ModelMapper {
private final InheritingConfiguration config;
private final MappingEngineImpl engine;
/**
* Creates a new ModelMapper.
*/
public ModelMapper() {
config = new InheritingConfiguration();
engine = new MappingEngineImpl(config);
}
xxx
}
在ModelMapper的构造方法中,可以看到。在每次创建ModelMapper对象的实例时,都会创建配置器实例和映射引擎。可以请大家留意这里的实现方式。除了这两个关键的参数外,还有其它的重要方法,总体来说有以下的一些重要方法(说明,为减少重复,这里将类中重载的部分进行删减):
序号 | 方法名 | 说明 |
1 | public <S, D> void addConverter(Converter<S, D> converter) | 注册一个用于将S 类型转换为D 类型的转换器。 |
2 | public <S, D> TypeMap<S, D> addMappings(PropertyMap<S, D> propertyMap) | 添加从PropertyMap 中定义的显式映射。 |
3 | public <S, D> TypeMap<S, D> createTypeMap(Class<S> sourceType, Class<D> destinationType) | 创建一个新的类型映射,支持通过类类型指定源和目标类型。 |
4 | public <S, D> TypeMap<S, D> getTypeMap(Class<S> sourceType, Class<D> destinationType) | 获取源类型和目标类型之间的类型映射。 |
5 | public <S, D> TypeMap<S, D> typeMap(Class<S> sourceType, Class<D> destinationType) | 获取或创建源类型和目标类型之间的类型映射。 |
6 | public Collection<TypeMap<?, ?>> getTypeMaps() | 获取所有的类型映射 |
7 | public <D> D map(Object source, Class<D> destinationType) | 将源对象映射到目标类型的新实例,支持通过类类型指定目标类型。 |
8 | public void map(Object source, Object destination) | 将源对象的属性映射到目标对象,支持直接操作对象。 |
9 | public ModelMapper registerModule(Module module) | 注册一个模块以扩展ModelMapper 的功能。 |
在ModelMapper中进行参数转换的重要方法就是map方法,这里只是将方法列出,最终的映射方法在后面进行讲解。接下来看一下,在配置属性对象和映射引擎对象的作用分别是什么?
2、深入Configuration配置
这里首先来介绍一下配置对象。俗话说,没有规矩不成方圆,因此规矩(即方圆)的设置非常有必要。正确的配置也是参数转换的前提条件。InheritingConfiguration
类中的属性主要负责存储和管理模型映射过程中的各种配置和策略。以下是该类中一些关键属性的介绍:
序号 | 属性 | 说明 |
1 | private final Configuration parent; | 父配置,使得当前配置可以从父配置继承设置 |
2 | public final TypeMapStore typeMapStore; | TypeMapStore 实例,用于存储和管理类型映射配置。 |
3 | public final ConverterStore converterStore; | ConverterStore 实例,用于存储和管理转换器,这些转换器用于在不同类型之间转换数据。 |
4 | public final ValueAccessStore valueAccessStore; | ValueAccessStore 实例,用于存储和管理值读取器,这些读取器用于访问对象的属性值。 |
5 | public final ValueMutateStore valueMutateStore; | ValueMutateStore 实例,用于存储和管理值写入器,这些写入器用于修改对象的属性值。 |
6 | private NameTokenizer destinationNameTokenizer; | 定义如何分割目标对象名称的 NameTokenizer 。 |
7 | private NameTransformer destinationNameTransformer; | 转换目标对象名称的 NameTransformer 。 |
8 | private NamingConvention destinationNamingConvention; | 定义目标对象命名约定的 NamingConvention 。 |
9 | private MatchingStrategy matchingStrategy; | 匹配策略的 MatchingStrategy ,用于确定如何匹配源和目标属性。 |
10 | private Condition<?, ?> propertyCondition; | 定义属性条件的 Condition 。 |
11 | private NameTokenizer sourceNameTokenizer; | 定义如何分割源对象名称的 NameTokenizer 。 |
12 | private Boolean fieldMatchingEnabled; | 布尔值,用于启用或禁用字段匹配。 |
13 | private Boolean ambiguityIgnored; | 布尔值,用于处理属性匹配中的歧义情况。 |
14 | private Boolean fullTypeMatchingRequired; | 布尔值,用于确定是否需要完全匹配类型。 |
15 | private Boolean implicitMatchingEnabled; | 布尔值,用于启用或禁用隐式匹配 |
16 | private Boolean preferNestedProperties; | 布尔值,用于确定是否优先匹配嵌套属性。 |
17 | private Boolean skipNullEnabled; | 布尔值,用于确定是否跳过 null 值 |
18 | private Boolean collectionsMergeEnabled; | 布尔值,用于启用或禁用集合合并。 |
19 | private Boolean useOSGiClassLoaderBridging; | 布尔值,用于确定是否使用 OSGi 类加载器桥接。 |
上面的这些配置非常重要,默认的配置一定是最宽泛的,为了兼容绝大多数的情况。同时效率应该不是最高的,因为很多没有做优化。因此优化上面的话,就涉及到配置的调优等。在这里面,比较重要的就是分割器,命名分割器对于提高源对象和目标对象的属性匹配度而对其进行分割,求解相似度,经过打分,从而确定一个最优的参数映射关系。这就是NameTokenizer的用法,于此同时,匹配策略也是非常重要,还有一些数据的属性条件等都能提高一定的转换性能。
来看一下它的默认构造方法,如下代码所示:
/**
* Creates an initial InheritingConfiguration.
*/
public InheritingConfiguration() {
parent = null;
typeMapStore = new TypeMapStore(this);
converterStore = new ConverterStore();
valueAccessStore = new ValueAccessStore();
valueMutateStore = new ValueMutateStore();
sourceNameTokenizer = NameTokenizers.CAMEL_CASE;
destinationNameTokenizer = NameTokenizers.CAMEL_CASE;
sourceNamingConvention = NamingConventions.JAVABEANS_ACCESSOR;
destinationNamingConvention = NamingConventions.JAVABEANS_MUTATOR;
sourceNameTransformer = NameTransformers.JAVABEANS_ACCESSOR;
destinationNameTransformer = NameTransformers.JAVABEANS_MUTATOR;
matchingStrategy = MatchingStrategies.STANDARD;
fieldAccessLevel = AccessLevel.PUBLIC;
methodAccessLevel = AccessLevel.PUBLIC;
fieldMatchingEnabled = Boolean.FALSE;
ambiguityIgnored = Boolean.FALSE;
fullTypeMatchingRequired = Boolean.FALSE;
implicitMatchingEnabled = Boolean.TRUE;
preferNestedProperties = Boolean.TRUE;
skipNullEnabled = Boolean.FALSE;
useOSGiClassLoaderBridging = Boolean.FALSE;
collectionsMergeEnabled = Boolean.FALSE;
}
从代码中可以看到,默认的配置中,NameTokenizer默认采用的驼峰的命名方法,这也与Java的命名规范息息相关;默认的匹配策略是标准匹配模式;默认的属性访问级别是public,方法的访问级别也是public,忽略空值默认是false等等。感兴趣的朋友可以细细查看。介绍完配置后,下面我们来介绍一下映射引擎。
3、深入MappingEngineImpl
MappingEngineImpl
的类,它实现了 MappingEngine
接口,提供了模型映射的具体实现。这个类是模型映射框架中的核心实现类,负责具体的映射逻辑和策略应用。通过继承和配置,它提供了强大的自定义能力,以适应不同的映射需求。首先来介绍一下它的属性列表:
序号 | 参数 | 说明 |
1 | private final Map<TypePair<?, ?>, Converter<?, ?>> converterCache | 一个用于存储基于源类型和目标类型对的有条件的转换器缓存。 |
2 | private final InheritingConfiguration configuration | 继承自配置,用于获取模型映射的配 |
3 | private final TypeMapStore typeMapStore; | 存储和管理类型映射的存储。 |
4 | private final ConverterStore converterStore; | 存储和管理转换器的存储。 |
在构建ModelMapper的构造方法中,将配置对象传入到映射引擎的对象中,即完成映射引擎与配置的关系,从而可以在进行参数转换进行处理。关于映射引擎如何工作的原理,后面有机会再来进行深入的说明。这里简单先说明一下map方法的调用。调用参数转换时会自动调用map方法,代码如下:
/**
* Initial entry point.
*/
public <S, D> D map(S source, Class<S> sourceType, D destination,
TypeToken<D> destinationTypeToken, String typeMapName) {
MappingContextImpl<S, D> context = new MappingContextImpl<S, D>(source, sourceType,
destination, destinationTypeToken.getRawType(), destinationTypeToken.getType(),
typeMapName, this);
D result = null;
try {
result = map(context);
} catch (ConfigurationException e) {
throw e;
} catch (ErrorsException e) {
throw context.errors.toMappingException();
} catch (Throwable t) {
context.errors.errorMapping(sourceType, destinationTypeToken.getType(), t);
}
context.errors.throwMappingExceptionIfErrorsExist();
return result;
}
最后调用属性的转换方法,即convert方法实现具体的转换逻辑,关键代码如下所示:
以上就是在ModelMapper的转换过程中比较重要的类的属性和方法的简单介绍。以及具体的应用,当然它后面的各种策略还没有细说。大家可以在官网中进行深度学习。 在了解这些基础的对象之后,我们来进行不同模式下的转换对比。
二、默认加载模式
首先我们先来看一下在使用默认的配置和策略设置的情况下,对于加载和转换数据的相关耗时情况。在进行默认的信息读取前,对加载的数据属性字段也有一个特定的说明。为了测试转换时数据的属性列的列数对转换的影响,这里我们选取之前的城市信息shapefile文件。这个shapefile文件有7342条数据,属性列个数是137个。
1、基础测试代码
这里我们采取从7342条数据库中循环读取和转换。最开始在默认的参数配置和映射引擎配置下运行。为了详细记录在读取Shapefile的各个过程中的性能损耗。这里分两个时间点介绍,第一个时间点是解析shp成hashMap集合的耗时,第二是将hashMap集合转换成目标集合的耗时。通过不同的设置模式求解耗时,来得到一个比较好的处理模式。默认情况下,modelMapper没有做任何优化和设置,全部采用默认的配置。数据处理的代码如下所示:
public void convertShp2BeanWithLooseModel() throws Exception {
Long startTime = System.currentTimeMillis();
File file = new File(SHP_FILE);
if (!file.exists()) {
System.out.println("文件不存在");
}
ShapefileDataStore store = new ShapefileDataStore(file.toURI().toURL());
store.setCharset(Charset.defaultCharset());// 设置中文字符编码
// 获取特征类型
SimpleFeatureType featureType = store.getSchema(store.getTypeNames()[0]);
CoordinateReferenceSystem crs = featureType.getGeometryDescriptor().getCoordinateReferenceSystem();
Integer epsgCode = CRS.lookupEpsgCode(crs, true);
List<HashMap<String, Object>> mapList = new ArrayList<HashMap<String,Object>>();
ModelMapper modelMapper = new ModelMapper();
Long s1 = System.currentTimeMillis();
List<Ne10mPopulatedPlaces> dataList = new ArrayList<Ne10mPopulatedPlaces>();
SimpleFeatureSource featureSource = store.getFeatureSource();
// 执行查询
SimpleFeatureCollection simpleFeatureCollection = featureSource.getFeatures();
SimpleFeatureIterator itertor = simpleFeatureCollection.features();
// 遍历featurecollection
while (itertor.hasNext()) {
HashMap<String, Object> map = new HashMap<String, Object>();
SimpleFeature feature = itertor.next();
Collection<Property> p = feature.getProperties();
Iterator<Property> it = p.iterator();
// 遍历feature的properties
while (it.hasNext()) {
Property pro = it.next();
if (null != pro && null != pro.getValue()) {
String field = pro.getName().toString();
String value = pro.getValue().toString();
map.put(field, value);
}
}
// 获取空间字段
org.locationtech.jts.geom.Geometry geometry = (org.locationtech.jts.geom.Geometry) feature.getDefaultGeometry();
// 创建WKTWriter对象
WKTWriter wktWriter = new WKTWriter();
// 将Geometry对象转换为WKT格式的字符串
String wkt = wktWriter.write(geometry);
String geom = "SRID=" + epsgCode +";" + wkt;//拼接srid,实现动态写入
map.put("geom", geom);
mapList.add(map);
}
Long e1 = System.currentTimeMillis();
System.out.println("解析shp:"+ (e1 - s1) + "毫秒");
Long s2 = System.currentTimeMillis();
for(HashMap<String, Object> map : mapList) {
Ne10mPopulatedPlaces places = modelMapper.map(map, Ne10mPopulatedPlaces.class);
dataList.add(places);
}
Long e2 = System.currentTimeMillis();
System.out.println("转化shp:"+ (e2 - s2) + "毫秒");
System.out.println(dataList.size());
Long endTime = System.currentTimeMillis();
Long time = endTime - startTime;
System.out.println("程序运行耗时:"+ time + "毫秒");
store.dispose();
}
使用Junit单元测试套件来测试以上代码,运行过后的输出结果如下:
默认模式转换耗时:解析1769毫秒 转换66759毫秒
可以看到一共7342条数据,解析这些数据的时间很快,1769毫秒,也就是两秒钟不到的时间就解析完成。但是在转换的时候耗时很长,花了将近66秒,将近一分钟了。如果有73420条数据,可能处理的速度会更慢,有没有更好的方式来解决呢?
三、持续优化,慢慢提升
既然使用默认的配置无法有效提高性能,那么要想提高处理能力呢?本节将结合转换配置器来重点讲解一些优化的步骤。通过使用有效的匹配策略和模式,加速参数转换,提高程序性能。本节将围绕速度和效率,来分享这些方法。所谓优化配置,就是根据配置表类设置不同的策略,因地制宜的进行参数的配置和模式的选择,选择最适合当前对象的转换模式。下面将分别给出设置代码和执行的时间。
1、增加忽略字段
在之前阐述configuration对象时,我们曾将讲过很多的属性。优化第一步,在实际情况中,比如我们的业务主键pkId或者ID等主键,可能是不需要转换的。在进行映射时,如果要转换的列较多就可以提升一定的处理性能。关键代码如下:
//设置忽略字段
PropertyMap<HashMap<String,Object>, Ne10mPopulatedPlaces> propertyMap = new PropertyMap<HashMap<String,Object>, Ne10mPopulatedPlaces>() {
protected void configure() {
skip(destination.getPkId());
skip(destination.getGeomJson());
}
};
modelMapper.addMappings(propertyMap);
请注意,这里的忽略字段可以根据项目的需求进行灵活的增减。最后在设置完propertyMap后不要忘了使用addMappings来添加。随后运行程序,经过设置忽略字段后,程序有稍微一点的提僧。处理和转换消耗的时间如下:
解析1807毫秒 转换58988毫秒
从运行结果可以看到,shapefile的解析时间不多,转换实现有了些许的提升,7771毫秒,将近7秒钟。感觉前途似乎看到了一些希望。
2、设置忽略空值模式
除了一些指定的字段值可以不用设置之外,还有一些属性是空值的,在转换时其实也是可以不用转换的。因此,第二个优化方法就是忽略空值。关键代码如下:
//设置忽略空值模式
modelMapper.getConfiguration().setSkipNullEnabled(true);
设置之后再来看一下程序的运行耗时情况。
设置忽略空值模式后耗时: 解析1906毫秒 转换57520毫秒
来对比一下时间,解析程序的耗时差不多,参数转换的时间又降低了。时间又提升了1468毫秒。提升不是很明显。
3、设置命名模式
大家知道使用java进行程序开发时,命名方法是有一定的规则的。但是,我们的源对象,比如是HashMap,它的命名可不一定是符合java的命名规范的。比如在map有中一个参数,名字是USER_NAME,而javaBean的属性是userName,所谓驼峰命名方法。因此,通过设置预知的命名模式,也是可以提升一部分性能的。来看一下设置的代码:
//设置命名约定,将下划线转换为驼峰
modelMapper.getConfiguration().setSourceNameTokenizer(NameTokenizers.UNDERSCORE)
.setDestinationNameTokenizer(NameTokenizers.CAMEL_CASE);
UnderscoreNameTokenizer这里是表示带下划线的转换成驼峰的命名方法,下面来看一下设置命名模式后的转换耗时:
命名模式设置后耗时:解析1895毫秒 转换52895毫秒
时间消耗又提升了4625毫秒。
4、采用精准匹配模式
经过之前的几步优化,我们的转换程序依然提升不是很明显,原因是参数和属性进行映射时,还是采用打分然后选取最高分的标准处理模式,那么我们有没有可能使用精准的匹配模式呢?这样就能减少很多的时间消耗。精准匹配模式的设置方法为:
//精准模式
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
我们来看一下精准匹配对象类的源码:
相比于标准的处理处理模式如下图所示:
可以看到,在match方法中,标准匹配模式比精准匹配模式的转换方法处理更简洁,而且对比匹配时也只是进行equalsIgnoreCase对比值,因此速度很快。下面我们来看一下最终的耗时。
开启精准匹配模式耗时:解析1940毫秒 转换14137毫秒
到这里,可以看到系统的转换性能得到了极大的提升,现在只需要14137毫秒了。从原来的一分多钟优化到14秒左右。由此可以看出,选用正确的匹配模式对于提升程序的处理时间有巨大的帮助。
四、总结
以上就是本文的主要内容,本文即以解析全球主要城市数据为例,重点介绍如何一步一步的优化转换策略和逻辑,将对象的转换时间性能提升76%的过程。希望通过本文的介绍,让您对掌握和优化ModelMapper有更深的了解。文章首先介绍ModelMapper的核心对象,然后介绍配置对象和映射引擎。接着分别介绍在默认模式、设置字段忽略、控制忽略、命名匹配、精准匹配模式下的不同转换耗时,通过这几个步骤和设置,就可以对我们的程序进行极大的性能优化。行文仓促,难免有许多不足之处,若有不足,还请各位专家和朋友在评论区留言指正,不胜感激。