1. はじめに

複数のレイヤーで構成される大規模なJavaアプリケーションを作成するには、永続性モデル、ドメインモデル、またはいわゆるDTOなどの複数のモデルを使用する必要があります。 異なるアプリケーション層に複数のモデルを使用するには、Bean間のマッピング方法を提供する必要があります。

これを手動で行うと、多くの定型コードがすばやく作成され、多くの時間が消費されます。 幸いなことに、Javaには複数のオブジェクトマッピングフレームワークがあります。

このチュートリアルでは、最も人気のあるJavaマッピングフレームワークのパフォーマンスを比較します。

2. マッピングフレームワーク

2.1. ドーザー

Dozerは、再帰を使用して1つのオブジェクトから別のオブジェクトにデータをコピーするマッピングフレームワークです。  フレームワークは、Bean間でプロパティをコピーできるだけでなく、異なるタイプ間で自動的に変換することもできます。

Dozerフレームワークを使用するには、プロジェクトにそのような依存関係を追加する必要があります。

<dependency>
    <groupId>com.github.dozermapper</groupId>
    <artifactId>dozer-core</artifactId>
    <version>6.5.0</version>
</dependency>

Dozerフレームワークの使用法の詳細については、この記事を参照してください。

フレームワークのドキュメントはここにあります。

2.2. オリカ

Orikaは、あるオブジェクトから別のオブジェクトにデータを再帰的にコピーするBeanからBeanへのマッピングフレームワークです

Orikaの仕事の一般的な原理はDozerに似ています。 2つの主な違いは、Orikaがバイトコード生成を使用するという事実です。 これにより、最小限のオーバーヘッドでより高速なマッパーを生成できます。

これを使用するには、このような依存関係をプロジェクトに追加する必要があります。

<dependency>
    <groupId>ma.glasnost.orika</groupId>
    <artifactId>orika-core</artifactId>
    <version>1.5.4</version>
</dependency>

Orikaの使用法の詳細については、この記事を参照してください。

フレームワークの実際のドキュメントはここにあります。

2.3. MapStruct

MapStructは Beanマッパークラスを自動的に生成するコードジェネレーター。

MapStructには、異なるデータ型間で変換する機能もあります。 使用方法の詳細については、この記事を参照してください。

MapStruct をプロジェクトに追加するには、次の依存関係を含める必要があります。

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.3.1.Final</version>
</dependency>

フレームワークのドキュメントはここにあります。

2.4. ModelMapper

ModelMapperは、規則に基づいてオブジェクトが相互にマップする方法を決定することにより、オブジェクトのマッピングを簡素化することを目的としたフレームワークです。 タイプセーフおよびリファクタリングセーフなAPIを提供します。

フレームワークの詳細については、ドキュメントを参照してください。

ModelMapperをプロジェクトに含めるには、次の依存関係を追加する必要があります。

<dependency>
  <groupId>org.modelmapper</groupId>
  <artifactId>modelmapper</artifactId>
  <version>2.3.8</version>
</dependency>

2.5. JMapper

JMapperは、JavaBean間で使いやすく高性能なマッピングを提供することを目的としたマッピングフレームワークです。

このフレームワークは、アノテーションとリレーショナルマッピングを使用してDRYの原則を適用することを目的としています。

フレームワークでは、注釈ベース、XML、またはAPIベースのさまざまな構成方法が可能です。

フレームワークの詳細については、ドキュメントを参照してください。

プロジェクトにJMapperを含めるには、その依存関係を追加する必要があります。

<dependency>
    <groupId>com.googlecode.jmapper-framework</groupId>
    <artifactId>jmapper-core</artifactId>
    <version>1.6.1.CR2</version>
</dependency>

3. テスト モデル

マッピングを適切にテストできるようにするには、ソースモデルとターゲットモデルが必要です。 2つのテストモデルを作成しました。

1つ目はStringフィールドが1つある単純な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. OrikaConverter

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. DozerConverter

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. MapStructConverter

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. JMapperConverter

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

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を指定して、Converterごとに個別のベンチマークを作成しました。

5.1.  AverageTime

JMHは、平均実行時間について次の結果を返しました(少ないほど良い):

フレームワーク名 平均実行時間(操作あたりのミリ秒単位)
MapStruct 10 -5
JMapper 10 -5
オリカ 0.001
ModelMapper 0.001
ブルドーザー 0.002

 

このベンチマークは、MapStructとJMapperの両方が最高の平均稼働時間を持っていることを明確に示しています。

5.2. スループット

このモードでは、ベンチマークは1秒あたりの操作数を返します。 次の結果が得られました(多いほど良い):

フレームワーク名 スループット(ミリ秒あたりの操作数)
MapStruct 133719
JMapper 106978
オリカ 1800
ModelMapper 978
ブルドーザー 471

 

スループットモードでは、MapStructがテストされたフレームワークの中で最速であり、JMapperが2番目に近いものでした。

5.3.  SingleShotTime

このモードでは、1回の操作の開始から終了までの時間を測定できます。 ベンチマークの結果は次のとおりです(少ないほど良い)。

フレームワーク名 シングルショット時間(操作あたりのミリ秒単位)
JMapper 0.015
MapStruct 0.450
ブルドーザー 2.094
オリカ 2.898
ModelMapper 4.837

 

ここでは、JMapperがMapStructよりも優れた結果を返すことがわかります。

5.4.  SampleTime

このモードでは、各操作の時間をサンプリングできます。  3つの異なるパーセンタイルの結果は次のようになります。

サンプル時間(操作あたりのミリ秒単位)
フレームワーク名 p0.90 p0.999 p1.0
JMapper 10 -4 0.001 2.6
MapStruct 10 -4 0.001 3
オリカ 0.001 0.010 4
ModelMapper 0.002 0.015 3.2
ブルドーザー 0.003 0.021 25

 

すべてのベンチマークは、シナリオに応じて、MapStructとJMapperの両方が適切な選択であることを示しています。

6. 実際のモデルテスト

パフォーマンステストには、Java Microbenchmark Harnessを使用できます。使用方法の詳細については、この記事を参照してください。

BenchmarkModeからMode.Allを指定して、Converterごとに個別のベンチマークを作成しました。

6.1.  AverageTime

JMHは、平均実行時間について次の結果を返しました(少ないほど良い):

フレームワーク名 平均実行時間(操作あたりのミリ秒単位)
MapStruct 10 -4
JMapper 10 -4
オリカ 0.004
ModelMapper 0.059
ブルドーザー 0.103

6.2. スループット

このモードでは、ベンチマークは1秒あたりの操作数を返します。 マッパーごとに、次の結果が得られました(多いほど良い):

フレームワーク名 スループット(ミリ秒あたりの操作数)
JMapper 7691
MapStruct 7120
オリカ 281
ModelMapper 19
ブルドーザー 10

6.3.  SingleShotTime

このモードでは、1回の操作の開始から終了までの時間を測定できます。 ベンチマークは次の結果をもたらしました(少ないほど良い):

フレームワーク名 シングルショット時間(操作あたりのミリ秒単位)
JMapper 0.253
MapStruct 0.532
ブルドーザー 9.495
ModelMapper 16.288
オリカ 18.081

6.4.  SampleTime

このモードでは、各操作の時間をサンプリングできます。 サンプリング結果はパーセンタイルに分割されます。3つの異なるパーセンタイルp0.90、p0.999 、およびp1.00の結果を示します。

サンプル時間(操作あたりのミリ秒単位)
フレームワーク名 p0.90 p0.999 p1.0
JMapper 10 -3 0.008 64
MapStruct 10 -3 0.010 68
オリカ 0.006 0.278 32
ModelMapper 0.083 2.398 97
ブルドーザー 0.146 4.526 118

単純な例と実際の例の正確な結果は明らかに異なっていましたが、それらは多かれ少なかれ同じ傾向に従います。 どちらの例でも、JMapperとMapStructの間でトップの座を争う緊密な競争が見られました。

6.5. 結論

このセクションで実行した実際のモデルテストに基づくと、MapStructはすぐ近くにありますが、最高のパフォーマンスは明らかにJMapperに属していることがわかります。 同じテストで、 SingleShotTime を除いて、Dozerが一貫して結果テーブルの一番下にあることがわかります。

7. 概要

この記事では、5つの一般的なJava beanマッピングフレームワーク、ModelMapper MapStruct Orika Dozer、およびJMapperのパフォーマンステストを実施しました。 。

いつものように、コードサンプルはGitHubにあります。