本文由 JavaGuide 翻譯自 https://www.baeldung.com/java-performance-mapping-frameworks 。轉(zhuǎn)載請(qǐng)注明原文地址以及翻譯作者。
1. 介紹
創(chuàng)建由多個(gè)層組成的大型 Java 應(yīng)用程序需要使用多種領(lǐng)域模型,如持久化模型、領(lǐng)域模型或者所謂的 DTO。為不同的應(yīng)用程序?qū)邮褂枚鄠€(gè)模型將要求我們提供 bean 之間的映射方法。手動(dòng)執(zhí)行此操作可以快速創(chuàng)建大量樣板代碼并消耗大量時(shí)間。幸運(yùn)的是,Java 有多個(gè)對(duì)象映射框架。在本教程中,我們將比較最流行的 Java 映射框架的性能。
綜合日常使用情況和相關(guān)測(cè)試數(shù)據(jù),個(gè)人感覺(jué) MapStruct、ModelMapper 這兩個(gè) Bean 映射框架是最佳選擇。
2. 常見(jiàn) Bean 映射框架概覽
2.1. Dozer
Dozer 是一個(gè)映射框架,它使用遞歸將數(shù)據(jù)從一個(gè)對(duì)象復(fù)制到另一個(gè)對(duì)象。框架不僅能夠在 bean 之間復(fù)制屬性,還能夠在不同類(lèi)型之間自動(dòng)轉(zhuǎn)換。
要使用 Dozer 框架,我們需要添加這樣的依賴(lài)到我們的項(xiàng)目:
<dependency>
<groupId>net.sf.dozer</groupId>
<artifactId>dozer</artifactId>
<version>5.5.1</version>
</dependency>
更多關(guān)于 Dozer 的內(nèi)容可以在官方文檔中找到: http://dozer.sourceforge.net/documentation/gettingstarted.html ,或者你也可以閱讀這篇文章:https://www.baeldung.com/dozer 。
2.2. Orika
Orika 是一個(gè) bean 到 bean 的映射框架,它遞歸地將數(shù)據(jù)從一個(gè)對(duì)象復(fù)制到另一個(gè)對(duì)象。
Orika 的工作原理與 Dozer 相似。兩者之間的主要區(qū)別是 Orika 使用字節(jié)碼生成。這允許以最小的開(kāi)銷(xiāo)生成更快的映射器。
要使用 Orika 框架,我們需要添加這樣的依賴(lài)到我們的項(xiàng)目:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.2</version>
</dependency>
更多關(guān)于 Orika 的內(nèi)容可以在官方文檔中找到:https://orika-mapper.github.io/orika-docs/,或者你也可以閱讀這篇文章:https://www.baeldung.com/orika-mapping。
2.3. MapStruct
MapStruct 是一個(gè)自動(dòng)生成 bean mapper 類(lèi)的代碼生成器。MapStruct 還能夠在不同的數(shù)據(jù)類(lèi)型之間進(jìn)行轉(zhuǎn)換。Github 地址:https://github.com/mapstruct/mapstruct。
要使用 MapStruct 框架,我們需要添加這樣的依賴(lài)到我們的項(xiàng)目:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>
更多關(guān)于 MapStruct 的內(nèi)容可以在官方文檔中找到:https://mapstruct.org/,或者你也可以閱讀這篇文章:https://www.baeldung.com/mapstruct。
要使用 MapStruct 框架,我們需要添加這樣的依賴(lài)到我們的項(xiàng)目:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.2.0.Final</version>
</dependency>
2.4. ModelMapper
ModelMapper 是一個(gè)旨在簡(jiǎn)化對(duì)象映射的框架,它根據(jù)約定確定對(duì)象之間的映射方式。它提供了類(lèi)型安全的和重構(gòu)安全的 API。
更多關(guān)于 ModelMapper 的內(nèi)容可以在官方文檔中找到:http://modelmapper.org/ 。
要使用 ModelMapper 框架,我們需要添加這樣的依賴(lài)到我們的項(xiàng)目:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>1.1.0</version>
</dependency>
2.5. JMapper
JMapper 是一個(gè)映射框架,旨在提供易于使用的、高性能的 Java bean 之間的映射。該框架旨在使用注釋和關(guān)系映射應(yīng)用 DRY 原則。該框架允許不同的配置方式:基于注釋、XML 或基于 api。
更多關(guān)于 JMapper 的內(nèi)容可以在官方文檔中找到:https://github.com/jmapper-framework/jmapper-core/wiki。
要使用 JMapper 框架,我們需要添加這樣的依賴(lài)到我們的項(xiàng)目:
<dependency>
<groupId>com.googlecode.jmapper-framework</groupId>
<artifactId>jmapper-core</artifactId>
<version>1.6.0.1</version>
</dependency>
3.測(cè)試模型
為了能夠正確地測(cè)試映射,我們需要有一個(gè)源和目標(biāo)模型。我們已經(jīng)創(chuàng)建了兩個(gè)測(cè)試模型。
第一個(gè)是一個(gè)只有一個(gè)字符串字段的簡(jiǎn)單 POJO,它允許我們?cè)诟?jiǎn)單的情況下比較框架,并檢查如果我們使用更復(fù)雜的 bean 是否會(huì)發(fā)生任何變化。
簡(jiǎn)單的源模型如下:
public class SourceCode {
String code;
// getter and setter
}
它的目標(biāo)也很相似:
public class DestinationCode {
String code;
// getter and setter
}
源 bean 的實(shí)際示例如下:
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
}
目標(biāo)類(lèi)如下圖所示:
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
}
整個(gè)模型結(jié)構(gòu)可以在這里找到:https://github.com/eugenp/tutorials/tree/master/performance-tests/src/main/java/com/baeldung/performancetests/model/source。
4. 轉(zhuǎn)換器
為了簡(jiǎn)化測(cè)試設(shè)置的設(shè)計(jì),我們創(chuàng)建了如下所示的轉(zhuǎn)換器接口:
public interface Converter {
Order convert(SourceOrder sourceOrder);
DestinationCode convert(SourceCode sourceCode);
}
我們所有的自定義映射器都將實(shí)現(xiàn)這個(gè)接口。
4.1. OrikaConverter
Orika 支持完整的 API 實(shí)現(xiàn),這大大簡(jiǎn)化了 mapper 的創(chuàng)建:
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. DozerConverter
Dozer 需要 XML 映射文件,有以下幾個(gè)部分:
<mappings xmlns="http://dozer.sourceforge.net"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozer.sourceforge.net
http://dozer.sourceforge.net/schema/beanmapping.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() {
DozerBeanMapper mapper = new DozerBeanMapper();
mapper.addMapping(
DozerConverter.class.getResourceAsStream("/dozer-mapping.xml"));
this.mapper = mapper;
}
@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. MapStructConverter
Map 結(jié)構(gòu)的定義非常簡(jiǎn)單,因?yàn)樗耆诖a生成:
@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. JMapperConverter
JMapperConverter 需要做更多的工作。接口實(shí)現(xiàn)后:
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);
}
}
我們還需要向目標(biāo)類(lèi)的每個(gè)字段添加@JMap
注釋。此外,JMapper 不能在 enum 類(lèi)型之間轉(zhuǎn)換,它需要我們創(chuàng)建自定義映射函數(shù):
@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
ModelMapperConverter 只需要提供我們想要映射的類(lèi):
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. 簡(jiǎn)單的模型測(cè)試
對(duì)于性能測(cè)試,我們可以使用 Java Microbenchmark Harness,關(guān)于如何使用它的更多信息可以在 這篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。
我們?yōu)槊總€(gè)轉(zhuǎn)換器創(chuàng)建了一個(gè)單獨(dú)的基準(zhǔn)測(cè)試,并將基準(zhǔn)測(cè)試模式指定為 Mode.All。
5.1. 平均時(shí)間
對(duì)于平均運(yùn)行時(shí)間,JMH 返回以下結(jié)果(越少越好):
這個(gè)基準(zhǔn)測(cè)試清楚地表明,MapStruct 和 JMapper 都有最佳的平均工作時(shí)間。
5.2. 吞吐量
在這種模式下,基準(zhǔn)測(cè)試返回每秒的操作數(shù)。我們收到以下結(jié)果(越多越好):
在吞吐量模式中,MapStruct 是測(cè)試框架中最快的,JMapper 緊隨其后。
5.3. SingleShotTime
這種模式允許測(cè)量單個(gè)操作從開(kāi)始到結(jié)束的時(shí)間?;鶞?zhǔn)給出了以下結(jié)果(越少越好):
這里,我們看到 JMapper 返回的結(jié)果比 MapStruct 好得多。
5.4. 采樣時(shí)間
這種模式允許對(duì)每個(gè)操作的時(shí)間進(jìn)行采樣。三個(gè)不同百分位數(shù)的結(jié)果如下:
所有的基準(zhǔn)測(cè)試都表明,根據(jù)場(chǎng)景的不同,MapStruct 和 JMapper 都是不錯(cuò)的選擇,盡管 MapStruct 對(duì) SingleShotTime 給出的結(jié)果要差得多。
6. 真實(shí)模型測(cè)試
對(duì)于性能測(cè)試,我們可以使用 Java Microbenchmark Harness,關(guān)于如何使用它的更多信息可以在 這篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。
我們?yōu)槊總€(gè)轉(zhuǎn)換器創(chuàng)建了一個(gè)單獨(dú)的基準(zhǔn)測(cè)試,并將基準(zhǔn)測(cè)試模式指定為 Mode.All。
6.1. 平均時(shí)間
JMH 返回以下平均運(yùn)行時(shí)間結(jié)果(越少越好):
該基準(zhǔn)清楚地表明,MapStruct 和 JMapper 均具有最佳的平均工作時(shí)間。
6.2. 吞吐量
在這種模式下,基準(zhǔn)測(cè)試返回每秒的操作數(shù)。我們收到以下結(jié)果(越多越好):
在吞吐量模式中,MapStruct 是測(cè)試框架中最快的,JMapper 緊隨其后。
6.3. SingleShotTime
這種模式允許測(cè)量單個(gè)操作從開(kāi)始到結(jié)束的時(shí)間?;鶞?zhǔn)給出了以下結(jié)果(越少越好):
6.4. 采樣時(shí)間
這種模式允許對(duì)每個(gè)操作的時(shí)間進(jìn)行采樣。三個(gè)不同百分位數(shù)的結(jié)果如下:
盡管簡(jiǎn)單示例和實(shí)際示例的確切結(jié)果明顯不同,但是它們的趨勢(shì)相同。在哪種算法最快和哪種算法最慢方面,兩個(gè)示例都給出了相似的結(jié)果。
6.5. 結(jié)論
根據(jù)我們?cè)诒竟?jié)中執(zhí)行的真實(shí)模型測(cè)試,我們可以看出,最佳性能顯然屬于 MapStruct。在相同的測(cè)試中,我們看到 Dozer 始終位于結(jié)果表的底部。
7. 總結(jié)
在這篇文章中,我們已經(jīng)進(jìn)行了五個(gè)流行的 Java Bean 映射框架性能測(cè)試:ModelMapper , MapStruct , Orika ,Dozer, JMapper。
示例代碼地址:https://github.com/eugenp/tutorials/tree/master/performance-tests。
公眾號(hào)
如果大家想要實(shí)時(shí)關(guān)注我更新的文章以及分享的干貨的話(huà),可以關(guān)注我的公眾號(hào)。
《Java面試突擊》: 由本文檔衍生的專(zhuān)為面試而生的《Java面試突擊》V2.0 PDF 版本公眾號(hào)后臺(tái)回復(fù) "Java面試突擊" 即可免費(fèi)領(lǐng)取!
Java工程師必備學(xué)習(xí)資源: 一些Java工程師常用學(xué)習(xí)資源公眾號(hào)后臺(tái)回復(fù)關(guān)鍵字 “1” 即可免費(fèi)無(wú)套路獲取。