Javaマッピングフレームワークのパフォーマンス
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のにあります。