Javaマッピングフレームワークのパフォーマンス

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は、平均実行時間について次の結果を返しました(少ないほど良い):

image

このベンチマークは、MapStructとJMapperの両方の平均作業時間が最適であることを明確に示しています。

5.2. Throughput

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

image

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

5.3. SingleShotTime

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

image

ここでは、JMapperがMapStructよりもかなり良い結果を返すことがわかります。

5.4. SampleTime

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

image

MapStructはSingleShotTime.に対して大幅に悪い結果をもたらしましたが、すべてのベンチマークは、MapStructとJMapperの両方がシナリオに応じて適切な選択であることを示しています。

6. 実生活モデルのテスト

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

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

6.1. AverageTime

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

image

6.2. Throughput

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

image

6.3. SingleShotTime

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

image

6.4. SampleTime

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

image

単純な例と実際の例の正確な結果は明らかに異なっていましたが、同じ傾向に従っています。 どちらの例でも、どのアルゴリズムが最も速く、どのアルゴリズムが最も遅いという点で、同様の結果が得られました。

6.5. 結論

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

7. Summary

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

いつものように、コードサンプルはover on GitHubで見つけることができます。