1. Introducción
La creación de grandes aplicaciones Java compuestas por múltiples capas requiere el uso de múltiples modelos, como el modelo de persistencia, el modelo de dominio o los llamados DTO. El uso de múltiples modelos para diferentes capas de aplicación requerirá que proporcionemos una forma de mapeo entre beans.
Hacer esto manualmente puede crear rápidamente mucho código repetitivo y consumir mucho tiempo. Afortunadamente para nosotros, existen múltiples marcos de mapeo de objetos para Java.
En este tutorial, vamos a comparar el rendimiento de los marcos de mapeo de Java más populares.
2. Mapeo de marcos
2.1. Topadora
Dozer es un marco de trabajo de mapeo que usa la recursividad para copiar datos de un objeto a otro . El marco no solo puede copiar propiedades entre los beans, sino que también puede convertir automáticamente entre diferentes tipos.
Para utilizar el marco de trabajo de Dozer, debemos agregar dicha dependencia a nuestro proyecto:
com.github.dozermapper dozer-core 6.5.0
Puede encontrar más información sobre el uso del marco de Dozer en este artículo.
La documentación del marco se puede encontrar aquí.
2.2. Orika
Orika es un marco de mapeo de bean a bean que copia de forma recursiva datos de un objeto a otro .
El principio general de trabajo del Orika es similar al Dozer. La principal diferencia entre los dos es el hecho de que Orika utiliza la generación de códigos de bytes . Esto permite generar mapeadores más rápidos con una sobrecarga mínima.
Para usarlo,necesitamos agregar dicha dependencia a nuestro proyecto:
ma.glasnost.orika orika-core 1.5.4
En este artículo se puede encontrar información más detallada sobre el uso de Orika.
La documentación real del marco se puede encontrar aquí.
2.3. MapStruct
MapStruct es un generador de código que genera automáticamente clases de mapeadores de beans .
MapStruct también tiene la capacidad de convertir entre diferentes tipos de datos. Puede encontrar más información sobre cómo usarlo en este artículo.
Para agregar MapStructa nuestro proyecto necesitamos incluir la siguiente dependencia:
org.mapstruct mapstruct 1.3.1.Final
La documentación del marco se puede encontrar aquí.
2.4. ModelMapper
ModelMapper es un marco que tiene como objetivo simplificar el mapeo de objetos, determinando cómo los objetos se mapean entre sí según las convenciones. Proporciona una API segura para la refactorización y el tipo.
Puede encontrar más información sobre el marco en la documentación.
Para incluir ModelMapper en nuestro proyecto, necesitamos agregar la siguiente dependencia:
org.modelmapper modelmapper 2.3.8
2.5. JMapper
JMapper es el marco de trabajo de mapeo que tiene como objetivo proporcionar un mapeo fácil de usar y de alto rendimiento entre Java Beans.
El marco tiene como objetivo aplicar el principio DRY utilizando anotaciones y mapeo relacional.
El marco permite diferentes formas de configuración: basada en anotaciones, XML o basada en API.
Se puede encontrar más información sobre el marco en su documentación.
Para incluir JMapper en nuestro proyecto, necesitamos agregar su dependencia:
com.googlecode.jmapper-framework jmapper-core 1.6.1.CR2
3. Modelo de prueba
Para poder probar el mapeo correctamente, necesitamos tener modelos de origen y destino. Hemos creado dos modelos de prueba.
El primero es solo un POJO simple con un campo de cadena , esto nos permitió comparar marcos en casos más simples y verificar si algo cambia si usamos beans más complicados.
El modelo de fuente simple se ve a continuación:
public class SourceCode { String code; // getter and setter }
Y su destino es bastante parecido:
public class DestinationCode { String code; // getter and setter }
El ejemplo de la vida real del bean fuente se ve así:
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 }
Y la clase objetivo se ve a continuación:
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 }
La estructura completa del modelo se puede encontrar aquí.
4. Convertidores
Para simplificar el diseño de la configuración de prueba, hemos creado la interfaz del convertidor :
public interface Converter { Order convert(SourceOrder sourceOrder); DestinationCode convert(SourceCode sourceCode); }
Y todos nuestros mapeadores personalizados implementarán esta interfaz.
4.1. OrikaConverter
Orika permite la implementación completa de la API, esto simplifica enormemente la creación del mapeador:
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 requiere un archivo de mapeo XML, con las siguientes secciones:
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.
Como siempre, los ejemplos de código se pueden encontrar en GitHub.