«1. Введение
Создание больших приложений Java, состоящих из нескольких уровней, требует использования нескольких моделей, таких как модель сохраняемости, модель предметной области или так называемые DTO. Использование нескольких моделей для разных уровней приложения потребует от нас предоставления способа отображения между bean-компонентами.
Выполнение этого вручную может быстро создать много стандартного кода и занять много времени. К счастью для нас, для Java существует несколько фреймворков отображения объектов.
В этом уроке мы сравним производительность самых популярных фреймворков Java.
2. Платформы сопоставления
2.1. Dozer
Dozer — это фреймворк отображения, использующий рекурсию для копирования данных из одного объекта в другой. Фреймворк может не только копировать свойства между bean-компонентами, но и автоматически преобразовывать их между разными типами.
Чтобы использовать фреймворк Dozer, нам нужно добавить в наш проект такую зависимость:
<dependency>
<groupId>com.github.dozermapper</groupId>
<artifactId>dozer-core</artifactId>
<version>6.5.0</version>
</dependency>
Дополнительную информацию об использовании фреймворка Dozer можно найти в этой статье.
Документацию по фреймворку можно найти здесь.
2.2. Orika
Orika — это фреймворк сопоставления компонентов, который рекурсивно копирует данные из одного объекта в другой.
Общий принцип работы Орика аналогичен Дозеру. Основное различие между ними заключается в том, что Orika использует генерацию байт-кода. Это позволяет создавать более быстрые преобразователи с минимальными накладными расходами.
Чтобы использовать его, нам нужно добавить в наш проект такую зависимость:
<dependency>
<groupId>ma.glasnost.orika</groupId>
<artifactId>orika-core</artifactId>
<version>1.5.4</version>
</dependency>
Более подробную информацию об использовании Orika можно найти в этой статье.
Актуальную документацию по фреймворку можно найти здесь.
2.3. MapStruct
MapStruct — это генератор кода, автоматически генерирующий классы сопоставления компонентов.
MapStruct также может преобразовывать различные типы данных. Более подробную информацию о том, как его использовать, можно найти в этой статье.
Чтобы добавить MapStruct в наш проект, нам нужно включить следующую зависимость:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
Документацию по фреймворку можно найти здесь.
2.4. ModelMapper
ModelMapper — это фреймворк, цель которого — упростить сопоставление объектов, определяя, как объекты сопоставляются друг с другом на основе соглашений. Он предоставляет типобезопасный и безопасный для рефакторинга API.
Более подробную информацию о фреймворке можно найти в документации.
Чтобы включить ModelMapper в наш проект, нам нужно добавить следующую зависимость:
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.8</version>
</dependency>
2.5. JMapper
JMapper — это инфраструктура сопоставления, целью которой является обеспечение простого в использовании и высокопроизводительного сопоставления между Java Beans.
Фреймворк направлен на применение принципа DRY с использованием аннотаций и реляционного отображения.
Фреймворк допускает различные способы настройки: на основе аннотаций, на основе XML или API.
Более подробную информацию о фреймворке можно найти в его документации.
Чтобы включить JMapper в наш проект, нам нужно добавить его зависимость:
<dependency>
<groupId>com.googlecode.jmapper-framework</groupId>
<artifactId>jmapper-core</artifactId>
<version>1.6.1.CR2</version>
</dependency>
3. Тестирование модели
Чтобы правильно протестировать сопоставление, нам нужны исходная и целевая модели. Мы создали две модели тестирования.
Первый — это просто простой POJO с одним строковым полем, это позволило нам сравнить фреймворки в более простых случаях и проверить, изменится ли что-нибудь, если мы используем более сложные bean-компоненты.
Простая исходная модель выглядит следующим образом:
public class SourceCode {
String code;
// getter and setter
}
И ее назначение очень похоже:
public class DestinationCode {
String code;
// getter and setter
}
Реальный пример исходного компонента выглядит так:
public class SourceOrder {
private String orderFinishDate;
private PaymentType paymentType;
private Discount discount;
private DeliveryData deliveryData;
private User orderingUser;
private List<Product> 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<Product> 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
}
Всю структуру модели можно найти здесь.
4. Преобразователи
Чтобы упростить дизайн тестовой установки, мы создали интерфейс преобразователя:
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-файл отображения со следующими разделами:
<mappings xmlns="http://dozermapper.github.io/schema/bean-mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://dozermapper.github.io/schema/bean-mapping
https://dozermapper.github.io/schema/bean-mapping.xsd">
<mapping>
<class-a>com.baeldung.performancetests.model.source.SourceOrder</class-a>
<class-b>com.baeldung.performancetests.model.destination.Order</class-b>
<field>
<a>status</a>
<b>orderStatus</b>
</field>
</mapping>
<mapping>
<class-a>com.baeldung.performancetests.model.source.SourceCode</class-a>
<class-b>com.baeldung.performancetests.model.destination.DestinationCode</class-b>
</mapping>
</mappings>
После определения отображения XML мы можем использовать его из кода:
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 довольно простое, так как оно полностью основано на генерации кода:
@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 к каждому полю целевого класса. Кроме того, JMapper не может самостоятельно выполнять преобразование между типами перечислений и требует от нас создания пользовательских функций сопоставления:
@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 требует, чтобы мы предоставили только те классы, которые мы хотим отобразить:
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, подробнее о том, как использовать его можно найти в этой статье.
Мы создали отдельный бенчмарк для каждого преобразователя, указав BenchmarkMode в Mode.All.
5.1. AverageTime
JMH вернул следующие результаты для среднего времени работы (чем меньше, тем лучше):
Framework Name | Average running time (in ms per operation) |
---|---|
MapStruct | 10 -5 |
JMapper | 10 -5 |
Orika | 0.001 |
ModelMapper | 0.001 |
Dozer | 0.002 |
Этот тест ясно показывает, что и MapStruct, и JMapper имеют лучшее среднее время работы.
5.2. Пропускная способность
В этом режиме тест возвращает количество операций в секунду. Мы получили следующие результаты (чем больше, тем лучше):
В режиме пропускной способности MapStruct оказался самым быстрым из протестированных фреймворков, а JMapper занял второе место.
Framework Name | Throughput (in operations per ms) |
---|---|
MapStruct | 133719 |
JMapper | 106978 |
Orika | 1800 |
ModelMapper | 978 |
Dozer | 471 |
5.3. SingleShotTime
Этот режим позволяет измерять время одной операции от ее начала до конца. Бенчмарк дал следующий результат (чем меньше, тем лучше):
Здесь мы видим, что JMapper возвращает лучший результат, чем MapStruct.
5.4. SampleTime
Framework Name | Single Shot Time (in ms per operation) |
---|---|
JMapper | 0.015 |
MapStruct | 0.450 |
Dozer | 2.094 |
Orika | 2.898 |
ModelMapper | 4.837 |
Этот режим позволяет производить выборку времени каждой операции. Результаты для трех различных процентилей выглядят следующим образом:
Все тесты показали, что MapStruct и JMapper являются хорошим выбором в зависимости от сценария.
6. Тестирование моделей в реальных условиях
Для тестирования производительности мы можем использовать Java Microbenchmark Harness, дополнительную информацию о том, как его использовать, можно найти в этой статье.
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 |
Мы создали отдельный бенчмарк для каждого конвертера с указанием BenchmarkMode в Mode.All.
6.1. Среднее время
JMH вернул следующие результаты для среднего времени работы (чем меньше, тем лучше):
6.2. Пропускная способность
В этом режиме тест возвращает количество операций в секунду. Для каждого из мапперов мы получили следующие результаты (чем больше, тем лучше):
6.3. SingleShotTime
Этот режим позволяет измерять время одной операции от ее начала до конца. Бенчмарк дал следующие результаты (чем меньше, тем лучше):
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.4. SampleTime
Этот режим позволяет производить выборку времени каждой операции. Результаты выборки разделены на процентили, мы представим результаты для трех разных процентилей p0,90, p0,999 и p1,00:
Framework Name | Throughput (in operations per ms) |
---|---|
JMapper | 7691 |
MapStruct | 7120 |
Orika | 281 |
ModelMapper | 19 |
Dozer | 10 |
Хотя точные результаты простого примера и примера из реальной жизни явно различались, но они следуют более или менее той же тенденции. В обоих примерах мы видели напряженную борьбу между JMapper и MapStruct за первое место.
6.5. Заключение
Framework Name | Single Shot Time (in ms per operation) |
---|---|
JMapper | 0.253 |
MapStruct | 0.532 |
Dozer | 9.495 |
ModelMapper | 16.288 |
Orika | 18.081 |
Основываясь на реальном тестировании модели, которое мы провели в этом разделе, мы видим, что лучшая производительность явно принадлежит JMapper, хотя MapStruct занимает второе место. В тех же тестах мы видим, что Dozer стабильно находится внизу нашей таблицы результатов, за исключением SingleShotTime.
7. Резюме
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 |
В этой статье мы провели тесты производительности пяти популярных фреймворков отображения Java-бинов: ModelMapper, MapStruct, Orika, Dozer и JMapper.
Как всегда, образцы кода можно найти на GitHub.
«
7. Summary
In this article, we’ve conducted performance tests of five popular Java bean mapping frameworks: ModelMapper, MapStruct, Orika, Dozer, and JMapper.
As always, code samples can be found over on GitHub.