1. Einleitung
Das Erstellen großer Java-Anwendungen, die aus mehreren Ebenen bestehen, erfordert die Verwendung mehrerer Modelle wie Persistenzmodell, Domänenmodell oder sogenannte DTOs. Für die Verwendung mehrerer Modelle für verschiedene Anwendungsebenen müssen wir eine Möglichkeit zur Zuordnung zwischen Beans bereitstellen.
Wenn Sie dies manuell tun, können Sie schnell viel Code für das Boilerplate erstellen und viel Zeit in Anspruch nehmen. Zum Glück gibt es für Java mehrere Objektzuordnungs-Frameworks.
In diesem Tutorial werden wir die Leistung der beliebtesten Java-Mapping-Frameworks vergleichen.
2. Mapping Frameworks
2.1. Bulldozer
Dozer ist ein Mapping-Framework, das mithilfe der Rekursion Daten von einem Objekt in ein anderes kopiert . Das Framework kann nicht nur Eigenschaften zwischen den Beans kopieren, sondern auch automatisch zwischen verschiedenen Typen konvertieren.
Um das Dozer-Framework zu verwenden, müssen wir unserem Projekt eine solche Abhängigkeit hinzufügen:
com.github.dozermapper dozer-core 6.5.0
Weitere Informationen zur Verwendung des Dozer-Frameworks finden Sie in diesem Artikel.
Die Dokumentation des Frameworks finden Sie hier.
2.2. Orika
Orika ist ein Bean-to-Bean-Mapping-Framework, das Daten rekursiv von einem Objekt in ein anderes kopiert .
Das allgemeine Arbeitsprinzip der Orika ähnelt dem von Dozer. Der Hauptunterschied zwischen den beiden ist die Tatsache, dass Orika die Bytecode-Generierung verwendet . Dies ermöglicht die Erzeugung schnellerer Mapper mit minimalem Overhead.
Um es zu benutzen,Wir müssen unserem Projekt eine solche Abhängigkeit hinzufügen:
ma.glasnost.orika orika-core 1.5.4
Weitere Informationen zur Verwendung des Orika finden Sie in diesem Artikel.
Die eigentliche Dokumentation des Frameworks finden Sie hier.
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 in diesem Artikel.
So fügen Sie MapStruct hinzuIn unser Projekt müssen wir die folgende Abhängigkeit einbeziehen:
org.mapstruct mapstruct 1.3.1.Final
Die Dokumentation des Frameworks finden Sie hier.
2.4. ModelMapper
ModelMapper ist ein Framework, das die Objektzuordnung vereinfachen soll, 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 in der Dokumentation.
Um ModelMapper in unser Projekt aufzunehmen, müssen wir die folgende Abhängigkeit hinzufügen:
org.modelmapper modelmapper 2.3.8
2.5. JMapper
JMapper ist das Mapping-Framework, das eine benutzerfreundliche, leistungsstarke Mapping-Funktion zwischen Java Beans bereitstellen soll.
Das Framework zielt darauf ab, das DRY-Prinzip mithilfe von Anmerkungen und relationaler Zuordnung anzuwenden.
Das Framework ermöglicht verschiedene Konfigurationsmöglichkeiten: annotationsbasiert, XML- oder API-basiert.
Weitere Informationen zum Framework finden Sie in der Dokumentation.
Um JMapper in unser Projekt aufzunehmen, müssen wir seine Abhängigkeit hinzufügen:
com.googlecode.jmapper-framework jmapper-core 1.6.1.CR2
3. Modell testen
Um das Mapping richtig testen zu können, benötigen wir Quell- und Zielmodelle. Wir haben zwei Testmodelle erstellt.
Das erste ist nur ein einfaches POJO mit einem String- Feld. Dadurch konnten wir Frameworks in einfacheren Fällen vergleichen und prü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 folgendermaßen 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 finden Sie hier.
4. Konverter
Um das Design des Testaufbaus zu vereinfachen, haben wir die Converter- Schnittstelle erstellt:
public interface Converter { Order convert(SourceOrder sourceOrder); DestinationCode convert(SourceCode sourceCode); }
Und alle unsere benutzerdefinierten Mapper werden diese Schnittstelle implementieren.
4.1. OrikaConverter
Orika ermöglicht eine vollständige API-Implementierung. Dies vereinfacht die Erstellung des Mappers 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 benötigt eine XML-Zuordnungsdatei mit den folgenden Abschnitten:
com.baeldung.performancetests.model.source.SourceOrder com.baeldung.performancetests.model.destination.Order status orderStatus com.baeldung.performancetests.model.source.SourceCode com.baeldung.performancetests.model.destination.DestinationCode
After defining the XML mapping, we can use it from code:
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 definition is quite simple as it's entirely based on code generation:
@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 requires more work to do. After implementing the interface:
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); } }
We also need to add @JMap annotations to each field of the target class. Also, JMapper can't convert between enum types on its own and it requires us to create custom mapping functions :
@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 requires us to only provide the classes that we want to map:
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. Simple Model Testing
For the performance testing, we can use Java Microbenchmark Harness, more information about how to use it can be found in this article.
We've created a separate benchmark for each Converter with specifying BenchmarkMode to Mode.All.
5.1. AverageTime
JMH returned the following results for average running time (the lesser the better) :
Framework Name | Average running time (in ms per operation) |
---|---|
MapStruct | 10 -5 |
JMapper | 10 -5 |
Orika | 0.001 |
ModelMapper | 0.001 |
Dozer | 0.002 |
This benchmark shows clearly that both MapStruct and JMapper have the best average working times.
5.2. Throughput
In this mode, the benchmark returns the number of operations per second. We have received the following results (more is better) :
Framework Name | Throughput (in operations per ms) |
---|---|
MapStruct | 133719 |
JMapper | 106978 |
Orika | 1800 |
ModelMapper | 978 |
Dozer | 471 |
In throughput mode, MapStruct was the fastest of the tested frameworks, with JMapper a close second.
5.3. SingleShotTime
This mode allows measuring the time of single operation from it's beginning to the end. The benchmark gave the following result (less is better):
Framework Name | Single Shot Time (in ms per operation) |
---|---|
JMapper | 0.015 |
MapStruct | 0.450 |
Dozer | 2.094 |
Orika | 2.898 |
ModelMapper | 4.837 |
Here, we see that JMapper returns better result than MapStruct.
5.4. SampleTime
This mode allows sampling of the time of each operation. The results for three different percentiles look like below:
Sample Time (in milliseconds per operation) | |||
---|---|---|---|
Framework Name | p0.90 | p0.999 | p1.0 |
JMapper | 10-4 | 0.001 | 2.6 |
MapStruct | 10-4 | 0.001 | 3 |
Orika | 0.001 | 0.010 | 4 |
ModelMapper | 0.002 | 0.015 | 3.2 |
Dozer | 0.003 | 0.021 | 25 |
All benchmarks have shown that MapStruct and JMapper are both good choices depending on the scenario.
6. Real-Life Model Testing
For the performance testing, we can use Java Microbenchmark Harness, more information about how to use it can be found in this article.
We have created a separate benchmark for each Converter with specifying BenchmarkMode to Mode.All.
6.1. AverageTime
JMH returned the following results for average running time (less is better) :
Framework Name | Average running time (in ms per operation) |
---|---|
MapStruct | 10 -4 |
JMapper | 10 -4 |
Orika | 0.004 |
ModelMapper | 0.059 |
Dozer | 0.103 |
6.2. Throughput
In this mode, the benchmark returns the number of operations per second. For each of the mappers we've received the following results (more is better) :
Framework Name | Throughput (in operations per ms) |
---|---|
JMapper | 7691 |
MapStruct | 7120 |
Orika | 281 |
ModelMapper | 19 |
Dozer | 10 |
6.3. SingleShotTime
This mode allows measuring the time of single operation from it's beginning to the end. The benchmark gave the following results (less is better):
Framework Name | Single Shot Time (in ms per operation) |
---|---|
JMapper | 0.253 |
MapStruct | 0.532 |
Dozer | 9.495 |
ModelMapper | 16.288 |
Orika | 18.081 |
6.4. SampleTime
This mode allows sampling of the time of each operation. Sampling results are split into percentiles, we'll present results for three different percentiles p0.90, p0.999, and p1.00:
Sample Time (in milliseconds per operation) | |||
---|---|---|---|
Framework Name | p0.90 | p0.999 | p1.0 |
JMapper | 10-3 | 0.008 | 64 |
MapStruct | 10-3 | 0.010 | 68 |
Orika | 0.006 | 0.278 | 32 |
ModelMapper | 0.083 | 2.398 | 97 |
Dozer | 0.146 | 4.526 | 118 |
While the exact results of the simple example and the real-life example were clearly different, but they do follow more or less the same trend. In both examples, we saw a close contest between JMapper and MapStruct for the top spot.
6.5. Conclusion
Based on the real-life model testing we performed in this section, we can see that the best performance clearly belongs to JMapper, although MapStruct is a close second. In the same tests, we see that Dozer is consistently at the bottom of our results table, except for SingleShotTime.
7. Summary
In this article, we've conducted performance tests of five popular Java bean mapping frameworks: ModelMapper, MapStruct, Orika, Dozer, and JMapper.
Codebeispiele finden Sie wie immer auf GitHub.