Javaマッピングフレームワークのパフォーマンス
1. Introduction
複数のレイヤーで構成される大規模なJavaアプリケーションを作成するには、永続性モデル、ドメインモデル、またはいわゆるDTOなどの複数のモデルを使用する必要があります。 異なるアプリケーション層に複数のモデルを使用するには、Bean間のマッピング方法を提供する必要があります。
これを手動で行うと、多くの定型コードがすばやく作成され、多くの時間が消費されます。 幸いなことに、Javaには複数のオブジェクトマッピングフレームワークがあります。
このチュートリアルでは、最も人気のあるJavaマッピングフレームワークのパフォーマンスを比較します。
2. マッピングフレームワーク
2.1. Dozer
Dozer is a mapping framework that uses recursion to copy data from one object to another。 フレームワークは、Bean間でプロパティをコピーできるだけでなく、異なるタイプ間で自動的に変換することもできます。
Dozerフレームワークを使用するには、プロジェクトにこのような依存関係を追加する必要があります。
net.sf.dozer
dozer
5.5.1
Dozerフレームワークの使用法の詳細については、このarticleを参照してください。
フレームワークのドキュメントはhereにあります。
2.2. オリカ
Orika is a bean to bean mapping framework that recursively copies data from one object to another。
オリカの仕事の一般原則はドーザーに似ています。 2つの主な違いは、Orika uses bytecode generationであるという事実です。 これにより、最小限のオーバーヘッドでより高速なマッパーを生成できます。
これを使用するには、 weでこのような依存関係をプロジェクトに追加する必要があります。
ma.glasnost.orika
orika-core
1.5.2
オリカの使用法の詳細については、このarticleをご覧ください。
フレームワークの実際のドキュメントはhereにあります。
2.3. MapStruct
MapStructは、Beanマッパークラスを自動的に生成するコードジェネレーターです。
MapStructには、異なるデータ型間で変換する機能もあります。 使用方法の詳細については、このarticleを参照してください。
MapStruct をプロジェクトに追加するには、次の依存関係を含める必要があります。
3
org.mapstruct
mapstruct-processor
1.2.0.Final
フレームワークのドキュメントはhere.にあります。
2.4. ModelMapper
ModelMapperは、規則に基づいてオブジェクトが相互にマップする方法を決定することにより、オブジェクトのマッピングを簡素化することを目的としたフレームワークです。 タイプセーフおよびリファクタリングセーフなAPIを提供します。
フレームワークの詳細については、documentationを参照してください。
ModelMapperをプロジェクトに含めるには、次の依存関係を追加する必要があります。
org.modelmapper
modelmapper
1.1.0
2.5. JMapper
JMapperは、Java Bean間の使いやすく高性能なマッピングを提供することを目的としたマッピングフレームワークです。
このフレームワークは、アノテーションとリレーショナルマッピングを使用してDRYの原則を適用することを目的としています。
このフレームワークでは、注釈ベース、XMLまたはAPIベースのさまざまな構成方法が可能です。
フレームワークの詳細については、そのdocumentationを参照してください。
プロジェクトに「JMapper」を含めるには、その依存関係を追加する必要があります。
com.googlecode.jmapper-framework
jmapper-core
1.6.0.1
3. テスト モデル
マッピングを適切にテストできるようにするには、ソースモデルとターゲットモデルが必要です。 2つのテストモデルを作成しました。
1つ目は1つのStringフィールドを持つ単純な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 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 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
}
モデル構造全体はhereで見つけることができます。
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マッピングファイルが必要です。
com.example.performancetests.model.source.SourceOrder
com.example.performancetests.model.destination.Order
status
orderStatus
com.example.performancetests.model.source.SourceCode
com.example.performancetests.model.destination.DestinationCode
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
マップ構造の定義は、コード生成に完全に基づいているため、非常に単純です。
@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 annotationsを追加する必要があります。 また、JMapperはそれ自体で列挙型間で変換することはできず、カスタムマッピング関数を作成する必要があります。
@JMapConversion(from = "paymentType", to = "paymentType")
public PaymentType conversion(com.example.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を使用できます。使用方法の詳細については、このarticleを参照してください。
BenchmarkMode toMode.Allを指定して、Converterごとに個別のベンチマークを作成しました。
5.1. AverageTime
JMHは、平均実行時間について次の結果を返しました(少ないほど良い):
このベンチマークは、MapStructとJMapperの両方の平均作業時間が最適であることを明確に示しています。
5.2. Throughput
このモードでは、ベンチマークは1秒あたりの操作数を返します。 次の結果が得られました(more is better):
スループットモードでは、MapStructがテストされたフレームワークの中で最速で、JMapperが2番目に近いものでした。
5.3. SingleShotTime
このモードでは、1回の操作の開始から終了までの時間を測定できます。 ベンチマークの結果は次のとおりです(少ないほど良い):
ここでは、JMapperがMapStructよりもかなり良い結果を返すことがわかります。
6. 実生活モデルのテスト
パフォーマンステストには、Java Microbenchmark Harnessを使用できます。使用方法の詳細については、このarticleを参照してください。
BenchmarkMode toMode.Allを指定して、Converterごとに個別のベンチマークを作成しました。
6.4. SampleTime
このモードでは、各操作の時間をサンプリングできます。 サンプリング結果はパーセンタイルに分割されます。3つの異なるパーセンタイルp0.90、p0.999,、およびp1.00の結果を示します。
単純な例と実際の例の正確な結果は明らかに異なっていましたが、同じ傾向に従っています。 どちらの例でも、どのアルゴリズムが最も速く、どのアルゴリズムが最も遅いという点で、同様の結果が得られました。
6.5. 結論
このセクションで実行した実際のモデルテストに基づいて、最高のパフォーマンスは明らかにMapStructに属していることがわかります。 同じテストで、Dozerが常に結果テーブルの一番下にあることがわかります。
7. Summary
この記事では、5つの一般的なJava Beanマッピングフレームワーク(ModelMapper, MapStruct, Orika, Dozer、およびJMapper)のパフォーマンステストを実施しました。
いつものように、コードサンプルはover on GitHubで見つけることができます。