«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.