Java 映射框架的性能
一、简介
创建由多层组成的大型 Java 应用程序需要使用多种模型,例如持久性模型、域模型或所谓的 DTO。对于不同的应用程序层使用多个模型将需要我们提供一种 bean 之间的映射方式。
手动执行此操作会快速创建大量样板代码并消耗大量时间。幸运的是,Java 有多个对象映射框架。
在本教程中,我们将比较最流行的 Java 映射框架的性能。
2. 映射框架
2.1. 推土机
Dozer 是一种映射框架,它使用递归将数据从一个对象复制到另一个对象。该框架不仅能够在bean之间复制属性,而且还可以在不同类型之间自动转换。
要使用 Dozer 框架,我们需要将此类依赖项添加到我们的项目中:
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>6.5.2</version>
</dependency>复制
有关 Dozer 框架的使用的更多信息可以在这篇文章中找到。
该框架的文档可以在这里找到,最新版本可以在这里找到。
2.2. 奥里卡
Orika 是一种 bean 到 bean 映射框架,它将数据从一个对象递归复制到另一个对象。
Orika 的一般工作原理与 Dozer 类似。两者之间的主要区别在于 Orika 使用字节码生成。这允许以最小的开销生成更快的映射器。
要使用它, 我们需要将这样的依赖项添加到我们的项目中:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>复制
有关 Orika 的使用的更多详细信息可以在本文中找到。
该框架的实际文档可以在这里找到,最新版本可以在这里找到。
警告:从Java 16开始,默认情况下会拒绝非法反射访问。Orika 的 1.5.4 版本使用了这种反射访问,因此 Orika 目前无法与 Java 16 结合使用。据称,随着 1.6.0 版本的发布,这个问题将在未来得到解决。
2.3. 映射结构
MapStruct 是 一个代码生成器,可以自动生成 bean 映射器类。
MapStruct还具有在不同数据类型之间进行转换的能力。有关如何使用它的更多信息可以在本文中找到。
要将 MapStruct 添加 到我们的项目中,我们需要包含以下依赖项:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.0.Beta1</version>
</dependency>复制
该框架的文档可以在这里找到,最新版本可以在这里找到。
2.4. 模型映射器
ModelMapper 是一个旨在通过根据约定确定对象如何相互映射来简化对象映射的框架。它提供类型安全和重构安全的 API。
有关该框架的更多信息可以在文档中找到。
要将 ModelMapper 包含在我们的项目中,我们需要添加以下依赖项:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>复制
可以在此处找到该框架的最新版本。
2.5. JMapper
JMapper 是一个映射框架,旨在提供 Java Bean 之间易于使用、高性能的映射。
该框架旨在使用注释和关系映射来应用 DRY 原则。
该框架允许不同的配置方式:基于注释、XML 或基于 API。
有关该框架的更多信息可以在其文档中找到。
要将 JMapper 包含在我们的项目中,我们需要添加它的依赖项:
<dependency>
<groupId>com.googlecode.jmapper-framework</groupId>
<artifactId>jmapper-core</artifactId>
<version>1.6.1.CR2</version>
</dependency>复制
可以在此处找到该框架的最新版本。
3. 测试 模型
为了能够正确测试映射,我们需要有源模型和目标模型。我们创建了两个测试模型。
第一个只是一个带有一个字符串字段的简单 POJO,这使我们能够在更简单的情况下比较框架,并检查如果我们使用更复杂的 bean 是否会发生任何变化。
简单的源模型如下所示:
public class SourceCode {
String code;
// getter and setter
}复制
它的目的地非常相似:
public class DestinationCode {
String code;
// getter and setter
}复制
真实生活中的源 bean 示例如下所示:
public class SourceOrder {
private String orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private DeliveryData deliveryData;
private User orderingUser;
private List<Product> orderedProducts;
private Shop offeringShop;
private int orderId;
private OrderStatus status;
private LocalDate orderDate;
// standard getters and setters
}复制
目标类如下所示:
public class Order {
private User orderingUser;
private List<Product> orderedProducts;
private OrderStatus orderStatus;
private LocalDate orderDate;
private LocalDate orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private int shopId;
private DeliveryData deliveryData;
private Shop offeringShop;
// standard getters and setters
}复制
整个模型结构可以在这里找到。
4.转换器
为了简化测试设置的设计,我们创建了 Converter接口:
public interface Converter {
Order convert(SourceOrder sourceOrder);
DestinationCode convert(SourceCode sourceCode);
}复制
我们所有的自定义映射器都将实现这个接口。
4.1. Orika转换器
Orika 允许完整的 API 实现,这极大地简化了映射器的创建:
public class OrikaConverter implements Converter{
private MapperFacade mapperFacade;
public OrikaConverter() {
MapperFactory mapperFactory = new DefaultMapperFactory
.Builder().build();
mapperFactory.classMap(Order.class, SourceOrder.class)
.field("orderStatus", "status").byDefault().register();
mapperFacade = mapperFactory.getMapperFacade();
}
@Override
public Order convert(SourceOrder sourceOrder) {
return mapperFacade.map(sourceOrder, Order.class);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return mapperFacade.map(sourceCode, DestinationCode.class);
}
}复制
4.2. 推土机转换器
Dozer 需要 XML 映射文件,包含以下部分:
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping
https://dozermapper.github.io/schema/bean-mapping.xsd">
<mapping>
<class-a>com.baeldung.performancetests.model.source.SourceOrder</class-a>
<class-b>com.baeldung.performancetests.model.destination.Order</class-b>
<field>
<a>status</a>
<b>orderStatus</b>
</field>
</mapping>
<mapping>
<class-a>com.baeldung.performancetests.model.source.SourceCode</class-a>
<class-b>com.baeldung.performancetests.model.destination.DestinationCode</class-b>
</mapping>
</mappings>复制
定义 XML 映射后,我们可以在代码中使用它:
public class DozerConverter implements Converter {
private final Mapper mapper;
public DozerConverter() {
this.mapper = DozerBeanMapperBuilder.create()
.withMappingFiles("dozer-mapping.xml")
.build();
}
@Override
public Order convert(SourceOrder sourceOrder) {
return mapper.map(sourceOrder,Order.class);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return mapper.map(sourceCode, DestinationCode.class);
}
}复制
4.3. 映射结构转换器
MapStruct 定义非常简单,因为它完全基于代码生成:
@Mapper
public interface MapStructConverter extends Converter {
MapStructConverter MAPPER = Mappers.getMapper(MapStructConverter.class);
@Mapping(source = "status", target = "orderStatus")
@Override
Order convert(SourceOrder sourceOrder);
@Override
DestinationCode convert(SourceCode sourceCode);
}复制
4.4. JMapper转换器
JMapperConverter需要做更多的工作。实现接口后:
public class JMapperConverter implements Converter {
JMapper realLifeMapper;
JMapper simpleMapper;
public JMapperConverter() {
JMapperAPI api = new JMapperAPI()
.add(JMapperAPI.mappedClass(Order.class));
realLifeMapper = new JMapper(Order.class, SourceOrder.class, api);
JMapperAPI simpleApi = new JMapperAPI()
.add(JMapperAPI.mappedClass(DestinationCode.class));
simpleMapper = new JMapper(
DestinationCode.class, SourceCode.class, simpleApi);
}
@Override
public Order convert(SourceOrder sourceOrder) {
return (Order) realLifeMapper.getDestination(sourceOrder);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return (DestinationCode) simpleMapper.getDestination(sourceCode);
}
}复制
我们还需要 为目标类的每个字段添加@JMap 注释。此外,JMapper 无法自行在枚举类型之间进行转换,它需要我们创建自定义映射函数:
@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType conversion(com.baeldung.performancetests.model.source.PaymentType type) {
PaymentType paymentType = null;
switch(type) {
case CARD:
paymentType = PaymentType.CARD;
break;
case CASH:
paymentType = PaymentType.CASH;
break;
case TRANSFER:
paymentType = PaymentType.TRANSFER;
break;
}
return paymentType;
}复制
4.5. 模型映射器转换器
ModelMapperConverter要求我们只提供我们想要映射的类:
public class ModelMapperConverter implements Converter {
private ModelMapper modelMapper;
public ModelMapperConverter() {
modelMapper = new ModelMapper();
}
@Override
public Order convert(SourceOrder sourceOrder) {
return modelMapper.map(sourceOrder, Order.class);
}
@Override
public DestinationCode convert(SourceCode sourceCode) {
return modelMapper.map(sourceCode, DestinationCode.class);
}
}复制
5. 简单模型测试
对于性能测试,我们可以使用Java Microbenchmark Harness,有关如何使用它的更多信息可以在这篇文章中找到。
我们为每个 转换器创建了一个单独的基准测试,并将 BenchmarkMode 指定为 Mode.All。
5.1. 平均时间
JMH 返回以下平均运行时间结果(越短越好):
框架名称 | 平均运行时间(每次操作的毫秒数) |
---|---|
映射结构 | 10 -5 |
JMapper | 10 -5 |
奥里卡 | 0.001 |
模型映射器 | 0.002 |
推土机 | 0.004 |
该基准测试清楚地表明 MapStruct 和 JMapper 都具有最佳的平均工作时间。
5.2. 吞吐量
在此模式下,基准测试返回每秒的操作数。我们收到了以下结果(越多越好):
框架名称 | 吞吐量(每毫秒操作数) |
---|---|
映射结构 | 58101 |
JMapper | 53667 |
奥里卡 | 1195 |
模型映射器 | 第379章 |
推土机 | 230 |
在吞吐量模式下,MapStruct 是测试框架中最快的,JMapper 紧随其后。
5.3. 单次时间
该模式允许测量单次操作从开始到结束的时间。基准测试给出了以下结果(越少越好):
框架名称 | 单次时间(每次操作以毫秒为单位) |
---|---|
JMapper | 0.016 |
映射结构 | 1.904 |
推土机 | 3.864 |
奥里卡 | 6.593 |
模型映射器 | 8.788 |
在这里,我们看到 JMapper 返回比 MapStruct 更好的结果。
5.4. 采样时间
该模式允许对每个操作的时间进行采样。三个不同百分位数的结果如下所示:
采样时间(每次操作以毫秒为单位) | |||
---|---|---|---|
框架名称 | p0.90 | p0.999 | p1.0 |
JMapper | 10 -4 | 0.001 | 1.526 |
映射结构 | 10 -4 | 10 -4 | 1.948 |
奥里卡 | 0.001 | 0.018 | 2.327 |
模型映射器 | 0.002 | 0.044 | 3.604 |
推土机 | 0.003 | 0.088 | 5.382 |
所有基准测试都表明,根据具体情况,MapStruct 和 JMapper 都是不错的选择。
6. 真实模型测试
对于性能测试,我们可以使用Java Microbenchmark Harness,有关如何使用它的更多信息可以在这篇文章中找到。
我们为每个转换器创建了一个单独的基准测试 ,并将 BenchmarkMode 指定为 Mode.All。
6.1. 平均时间
JMH 返回以下平均运行时间结果(越少越好):
框架名称 | 平均运行时间(每次操作的毫秒数) |
---|---|
映射结构 | 10 -4 |
JMapper | 10 -4 |
奥里卡 | 0.007 |
模型映射器 | 0.137 |
推土机 | 0.145 |
6.2. 吞吐量
在此模式下,基准测试返回每秒的操作数。对于每个映射器,我们收到了以下结果(越多越好):
框架名称 | 吞吐量(每毫秒操作数) |
---|---|
JMapper | 3205 |
映射结构 | 3467 |
奥里卡 | 121 |
模型映射器 | 7 |
推土机 | 6.342 |
6.3. 单次时间
该模式允许测量单次操作从开始到结束的时间。基准测试给出了以下结果(越少越好):
框架名称 | 单次时间(每次操作以毫秒为单位) |
---|---|
JMapper | 0.722 |
映射结构 | 2.111 |
推土机 | 16.311 |
模型映射器 | 22.342 |
奥里卡 | 32.473 |
6.4. 采样时间
该模式允许对每个操作的时间进行采样。采样结果分为百分位数,我们将呈现三个不同百分位数 p0.90、p0.999 和 p1.00 的结果:
采样时间(每次操作以毫秒为单位) | |||
---|---|---|---|
框架名称 | p0.90 | p0.999 | p1.0 |
JMapper | 10 -3 | 0.006 | 3 |
映射结构 | 10 -3 | 0.006 | 8 |
奥里卡 | 0.007 | 0.143 | 14 |
模型映射器 | 0.138 | 0.991 | 15 |
推土机 | 0.131 | 0.954 | 7 |
虽然简单示例和现实生活示例的确切结果明显不同,但它们确实遵循或多或少相同的趋势。在这两个示例中,我们看到 JMapper 和 MapStruct 之间为争夺第一名展开了激烈的竞争。
6.5. 结论
根据我们在本节中执行的实际模型测试,我们可以看到最佳性能显然属于 JMapper,尽管 MapStruct 紧随其后。在相同的测试中,我们看到 Dozer 始终位于结果表的底部,除了SingleShotTime。
七、总结
在本文中,我们对五个流行的 Java bean 映射框架进行了性能测试:ModelMapper 、 MapStruct 、 Orika 、 Dozer 和 JMapper。
与往常一样,可以在 GitHub 上找到代码示例。