5種常見(jiàn)Bean映射工具的性能比對(duì)

本文由 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é)果(越少越好):

AverageTime

這個(gè)基準(zhǔn)測(cè)試清楚地表明,MapStruct 和 JMapper 都有最佳的平均工作時(shí)間。

5.2. 吞吐量

在這種模式下,基準(zhǔn)測(cè)試返回每秒的操作數(shù)。我們收到以下結(jié)果(越多越好):

Throughput

在吞吐量模式中,MapStruct 是測(cè)試框架中最快的,JMapper 緊隨其后。

5.3. SingleShotTime

這種模式允許測(cè)量單個(gè)操作從開(kāi)始到結(jié)束的時(shí)間?;鶞?zhǔn)給出了以下結(jié)果(越少越好):

SingleShotTime

這里,我們看到 JMapper 返回的結(jié)果比 MapStruct 好得多。

5.4. 采樣時(shí)間

這種模式允許對(duì)每個(gè)操作的時(shí)間進(jìn)行采樣。三個(gè)不同百分位數(shù)的結(jié)果如下:

SampleTime

所有的基準(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é)果(越少越好):

平均時(shí)間

該基準(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é)果如下:

SampleTime

盡管簡(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ú)套路獲取。

我的公眾號(hào)
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容