Leistung von Java Mapping Frameworks

Leistung von Java Mapping Frameworks

1. Introduction

Für die Erstellung großer Java-Anwendungen, die aus mehreren Ebenen bestehen, müssen mehrere Modelle verwendet werden, z. B. ein Persistenzmodell, ein Domänenmodell oder sogenannte DTOs. Wenn Sie mehrere Modelle für verschiedene Anwendungsebenen verwenden, müssen Sie eine Möglichkeit zum Mappen zwischen Beans bereitstellen.

Wenn Sie dies manuell tun, können Sie schnell viel Code erstellen und viel Zeit verbrauchen. Zum Glück gibt es für Java mehrere Object Mapping Frameworks.

In diesem Tutorial werden wir die Leistung der beliebtesten Java-Mapping-Frameworks vergleichen.

2. Mapping-Frameworks

2.1. Dozer

Dozer is a mapping framework that uses recursion to copy data from one object to another. Das Framework kann nicht nur Eigenschaften zwischen den Beans kopieren, sondern auch automatisch zwischen verschiedenen Typen konvertieren.

Um das Dozer-Framework verwenden zu können, müssen wir unserem Projekt eine solche Abhängigkeit hinzufügen:


    net.sf.dozer
    dozer
    5.5.1

Weitere Informationen zur Verwendung des Dozer-Frameworks finden Sie inarticle.

Die Dokumentation des Frameworks finden Sie inhere.

2.2. Orika

Orika is a bean to bean mapping framework that recursively copies data from one object to another.

Das allgemeine Arbeitsprinzip der Orika ähnelt dem von Dozer. Der Hauptunterschied zwischen den beiden ist die Tatsache, dassOrika uses bytecode generation. Dadurch können schnellere Mapper mit minimalem Overhead generiert werden.

Um es zu verwenden, muss we unserem Projekt eine solche Abhängigkeit hinzufügen:


    ma.glasnost.orika
    orika-core
    1.5.2

Weitere Informationen zur Verwendung des Orika finden Sie inarticle.

Die eigentliche Dokumentation des Frameworks finden Sie inhere.

2.3. MapStruct

MapStruct ist ein Codegenerator, der automatisch Bean-Mapper-Klassen generiert.

MapStruct kann auch zwischen verschiedenen Datentypen konvertieren. Weitere Informationen zur Verwendung finden Sie inarticle.

Um MapStruct zu unserem Projekt hinzuzufügen, müssen wir die folgende Abhängigkeit einbeziehen:

3
    org.mapstruct
    mapstruct-processor
    1.2.0.Final

Die Dokumentation des Frameworks finden Sie inhere.

2.4. ModelMapper

ModelMapper ist ein Framework, das darauf abzielt, die Objektzuordnung zu vereinfachen, indem anhand von Konventionen festgelegt wird, wie Objekte einander zugeordnet werden. Es bietet eine typsichere und refactoring-sichere API.

Weitere Informationen zum Framework finden Sie indocumentation.

Um den ModelMapper in unser Projekt aufzunehmen, müssen wir die folgende Abhängigkeit hinzufügen:


  org.modelmapper
  modelmapper
  1.1.0

2.5. JMapper

JMapper ist das Mapping-Framework, das ein benutzerfreundliches und leistungsstarkes Mapping zwischen Java-Beans ermöglichen soll.

Das Framework zielt darauf ab, das DRY-Prinzip mithilfe von Anmerkungen und relationaler Zuordnung anzuwenden.

Das Framework bietet verschiedene Konfigurationsmöglichkeiten: annotationsbasiert, XML- oder API-basiert.

Weitere Informationen zum Framework finden Sie indocumentation.

Um den JMapper in unser Projekt aufzunehmen, müssen wir seine Abhängigkeit hinzufügen:


    com.googlecode.jmapper-framework
    jmapper-core
    1.6.0.1

3. Testen Modell

Um das Mapping richtig testen zu können, benötigen wir ein Quell- und ein Zielmodell. Wir haben zwei Testmodelle erstellt.

Das erste ist nur ein einfaches POJO mit einemString-Feld. Dies ermöglichte es uns, Frameworks in einfacheren Fällen zu vergleichen und zu überprüfen, ob sich etwas ändert, wenn wir kompliziertere Beans verwenden.

Das einfache Quellmodell sieht wie folgt aus:

public class SourceCode {
    String code;
    // getter and setter
}

Und sein Ziel ist ziemlich ähnlich:

public class DestinationCode {
    String code;
    // getter and setter
}

Das reale Beispiel für Source Bean sieht so aus:

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
}

Und die Zielklasse sieht wie folgt aus:

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
}

Die gesamte Modellstruktur kann inhere gefunden werden.

4. Konverter

Um das Design des Testaufbaus zu vereinfachen, haben wir dieConverter-Schnittstelle erstellt, die wie folgt aussieht:

public interface Converter {
    Order convert(SourceOrder sourceOrder);
    DestinationCode convert(SourceCode sourceCode);
}

Alle unsere benutzerdefinierten Mapper implementieren diese Schnittstelle.

4.1. OrikaConverter

Orika ermöglicht die vollständige API-Implementierung. Dies vereinfacht die Erstellung des Mapper erheblich:

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 erfordert eine XML-Zuordnungsdatei mit den folgenden Abschnitten:



    
        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
    

Nachdem wir das XML-Mapping definiert haben, können wir es aus dem Code verwenden:

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

Die Definition der Kartenstruktur ist recht einfach, da sie vollständig auf der Codegenerierung basiert:

@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 erfordert mehr Arbeit. Nach der Implementierung der Schnittstelle:

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);
    }
}

Wir müssen außerdem jedem Feld der Zielklasse@JMap annotationen hinzufügen. Außerdem kann JMapper nicht alleine zwischen Aufzählungstypen konvertieren und muss benutzerdefinierte Zuordnungsfunktionen erstellen:

@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 erfordert nur die Bereitstellung der Klassen, die wir zuordnen möchten:

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. Einfache Modellprüfung

Für die Leistungstests können wir Java Microbenchmark Harness verwenden. Weitere Informationen zur Verwendung finden Sie inarticle.

Wir haben für jedesConverter einen separaten Benchmark erstellt, in demBenchmarkMode toMode.All angegeben sind.

5.1. AverageTime

JMH gab die folgenden Ergebnisse für die durchschnittliche Laufzeit zurück (je weniger desto besser):

image

Dieser Benchmark zeigt deutlich, dass sowohl MapStruct als auch JMapper die besten durchschnittlichen Arbeitszeiten haben.

5.2. Throughput

In diesem Modus gibt der Benchmark die Anzahl der Operationen pro Sekunde zurück. Wir haben folgende Ergebnisse erhalten (more is better):

image

Im Durchsatzmodus war MapStruct das schnellste der getesteten Frameworks, wobei JMapper knapp dahinter lag.

5.3. SingleShotTime

In diesem Modus kann die Zeit eines einzelnen Vorgangs vom Anfang bis zum Ende gemessen werden. Der Benchmark ergab folgendes Ergebnis (weniger ist besser):

image

Hier sehen wir, dass JMapper ein deutlich besseres Ergebnis liefert als MapStruct.

5.4. SampleTime

In diesem Modus kann die Zeit jedes Vorgangs abgetastet werden. Die Ergebnisse für drei verschiedene Perzentile sehen wie folgt aus:

image

Alle Benchmarks haben gezeigt, dass MapStruct und JMapper je nach Szenario eine gute Wahl sind, obwohl MapStruct fürSingleShotTime. signifikant schlechtere Ergebnisse lieferte

6. Modellversuche im realen Leben

Für die Leistungstests können wir Java Microbenchmark Harness verwenden. Weitere Informationen zur Verwendung finden Sie inarticle.

Wir haben für jedesConverter einen separaten Benchmark erstellt, in demBenchmarkMode toMode.All angegeben sind.

6.1. AverageTime

JMH lieferte die folgenden Ergebnisse für die durchschnittliche Laufzeit (weniger ist besser):

image

6.2. Throughput

In diesem Modus gibt der Benchmark die Anzahl der Operationen pro Sekunde zurück. Für jeden der Mapper haben wir die folgenden Ergebnisse erhalten (mehr ist besser):

image

6.3. SingleShotTime

In diesem Modus kann die Zeit eines einzelnen Vorgangs vom Anfang bis zum Ende gemessen werden. Der Benchmark ergab folgende Ergebnisse (weniger ist besser):

image

6.4. SampleTime

In diesem Modus kann die Zeit jedes Vorgangs abgetastet werden. Die Stichprobenergebnisse sind in Perzentile unterteilt. Wir präsentieren die Ergebnisse für drei verschiedene Perzentile p0.90, p0.999, und p1.00:

image

Die genauen Ergebnisse des einfachen Beispiels und des realen Beispiels waren zwar deutlich unterschiedlich, sie folgen jedoch dem gleichen Trend. Beide Beispiele ergaben ähnliche Ergebnisse hinsichtlich des schnellsten und des langsamsten Algorithmus.

6.5. Fazit

Anhand der in diesem Abschnitt durchgeführten realen Modelltests können wir erkennen, dass die beste Leistung eindeutig MapStruct zuzuordnen ist. In den gleichen Tests stellen wir fest, dass Dozer stets am Ende unserer Ergebnistabelle steht.

7. Summary

In diesem Artikel haben wir Leistungstests für fünf gängige Java-Bean-Mapping-Frameworks durchgeführt: ModelMapper, MapStruct, Orika, Dozer und JMapper.

Wie immer finden sich Codebeispiele inover on GitHub.