Что такое MapStruct и зачем он нужен?
MapStruct — это генератор кода для Java, который автоматически создает реализации мапперов (преобразователей) между объектами разных типов. В современной Java-разработке часто возникает необходимость преобразовывать один объект в другой: Entity в DTO, DTO в VO, один API-ответ в другую модель и так далее. Ручное написание такого кода — это скучная, трудоемкая и чреватая ошибками работа. MapStruct решает эту проблему, генерируя весь boilerplate-код на этапе компиляции.
Основные преимущества MapStruct:
- Производительность: Генерация кода на этапе компиляции (compile-time). Нет рефлексии, нет потерь производительности в runtime. MapStruct работает так же быстро, как если бы вы написали маппинг вручную.
- Безопасность типов: Все ошибки (несоответствие типов, отсутствие полей) обнаруживаются на этапе компиляции, а не в runtime.
- Простота: Минимум конфигурации. Достаточно определить интерфейс или абстрактный класс с методами маппинга, и MapStruct сам сгенерирует реализацию.
- Гибкость: Поддержка сложных маппингов, преобразования типов, использования выражений, вызова методов и интеграции с другими библиотеками (Lombok, Spring и т.д.).
- Чистота кода: Код маппинга отделен от бизнес-логики, что делает его более читаемым и поддерживаемым.
MapStruct особенно полезен в многослойных архитектурах (Controller-Service-Repository), где часто требуется преобразовывать объекты между слоями.
Установка MapStruct
MapStruct не устанавливается через pip или npm, так как это библиотека для Java. Для подключения MapStruct к вашему проекту используется система сборки Maven или Gradle.
Установка через Maven
Добавьте следующие зависимости в ваш файл pom.xml:
<dependencies>
<!-- Основная библиотека MapStruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<!-- Процессор аннотаций (для генерации кода) -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
<scope>provided</scope>
</dependency>
</dependencies>
Установка через Gradle
Для Gradle добавьте в файл build.gradle:
dependencies {
implementation 'org.mapstruct:mapstruct:1.5.5.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final'
}
Важно: Убедитесь, что версия mapstruct-processor совпадает с версией mapstruct. После добавления зависимостей выполните сборку проекта (например, mvn clean compile), чтобы MapStruct сгенерировал реализации мапперов.
Быстрый старт с примером кода
Давайте создадим простой маппер, который преобразует объект Car (Entity) в объект CarDto (DTO).
Шаг 1: Определите исходный и целевой классы
// Car.java
public class Car {
private String make;
private int numberOfSeats;
private CarType type;
// конструкторы, геттеры, сеттеры
public Car() {}
public Car(String make, int numberOfSeats, CarType type) {
this.make = make;
this.numberOfSeats = numberOfSeats;
this.type = type;
}
public String getMake() { return make; }
public void setMake(String make) { this.make = make; }
public int getNumberOfSeats() { return numberOfSeats; }
public void setNumberOfSeats(int numberOfSeats) { this.numberOfSeats = numberOfSeats; }
public CarType getType() { return type; }
public void setType(CarType type) { this.type = type; }
}
// CarType.java
public enum CarType {
SEDAN, SUV, SPORTS, HATCHBACK
}
// CarDto.java
public class CarDto {
private String make;
private int seatCount;
private String type;
// конструкторы, геттеры, сеттеры
public CarDto() {}
public CarDto(String make, int seatCount, String type) {
this.make = make;
this.seatCount = seatCount;
this.type = type;
}
public String getMake() { return make; }
public void setMake(String make) { this.make = make; }
public int getSeatCount() { return seatCount; }
public void setSeatCount(int seatCount) { this.seatCount = seatCount; }
public String getType() { return type; }
public void setType(String type) { this.type = type; }
}
Шаг 2: Создайте интерфейс маппера
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface CarMapper {
CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);
@Mapping(source = "numberOfSeats", target = "seatCount")
@Mapping(source = "type", target = "type")
CarDto carToCarDto(Car car);
}
Шаг 3: Используйте маппер
public class Main {
public static void main(String[] args) {
Car car = new Car("Toyota", 5, CarType.SEDAN);
CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);
System.out.println("Make: " + carDto.getMake());
System.out.println("Seat Count: " + carDto.getSeatCount());
System.out.println("Type: " + carDto.getType());
}
}
Результат:
Make: Toyota
Seat Count: 5
Type: SEDAN
Обратите внимание, что MapStruct автоматически сопоставил поле make (имена совпадают), преобразовал numberOfSeats в seatCount (благодаря аннотации @Mapping) и преобразовал enum CarType в строку type.
Основные методы и аннотации MapStruct
MapStruct предоставляет богатый набор аннотаций и конфигураций для настройки маппинга. Рассмотрим 10 ключевых из них.
1. @Mapper
Что делает: Помечает интерфейс или абстрактный класс как маппер. MapStruct сгенерирует реализацию для всех методов, объявленных в этом интерфейсе.
Параметры:
componentModel— модель компонента (например,"spring"для интеграции со Spring).uses— классы других мапперов, которые могут быть использованы.unmappedTargetPolicy— политика обработки несопоставленных полей (ERROR, WARN, IGNORE).
Пример:
@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface UserMapper {
UserDto userToUserDto(User user);
}
2. @Mapping
Что делает: Настраивает сопоставление между полями источника и цели. Позволяет указать, как преобразовывать одно поле в другое.
Параметры:
source— имя поля в исходном объекте.target— имя поля в целевом объекте.expression— Java-выражение для преобразования.defaultValue— значение по умолчанию, если поле источника равноnull.qualifiedByName— имя квалификатора для вызова кастомного метода.
Пример:
@Mapping(source = "fullName", target = "name")
@Mapping(source = "emailAddress", target = "email")
@Mapping(target = "creationDate", expression = "java(new java.util.Date())")
UserDto userToUserDto(User user);
3. @Mappings
Что делает: Контейнер для нескольких аннотаций @Mapping. Используется, когда нужно применить более одного маппинга к одному методу.
Параметры: Не имеет собственных параметров, содержит массив @Mapping.
Пример:
@Mappings({
@Mapping(source = "firstName", target = "givenName"),
@Mapping(source = "lastName", target = "familyName"),
@Mapping(source = "age", target = "yearsOld")
})
PersonDto personToPersonDto(Person person);
4. @MappingTarget
Что делает: Используется для обновления существующего объекта, а не создания нового. Параметр метода, помеченный этой аннотацией, будет тем объектом, в который будут записаны значения.
Параметры: Не имеет.
Пример:
@Mapper
public interface CarMapper {
void updateCarFromDto(CarDto carDto, @MappingTarget Car car);
}
// Использование:
Car car = new Car("Toyota", 5, CarType.SEDAN);
CarDto carDto = new CarDto("Honda", 4, "SUV");
carMapper.updateCarFromDto(carDto, car);
// Теперь car имеет значения из carDto: make="Honda", seatCount=4, type=SUV
5. @IterableMapping
Что делает: Позволяет настроить маппинг для коллекций (List, Set и т.д.). Полезно, когда нужно преобразовать каждый элемент коллекции.
Параметры:
elementTargetType— тип элемента целевой коллекции.dateFormat— формат даты для преобразования.
Пример:
@Mapper
public interface CarMapper {
@IterableMapping(elementTargetType = CarDto.class)
List<CarDto> carsToCarDtos(List<Car> cars);
}
// MapStruct сгенерирует код, который пройдется по списку и преобразует каждый Car в CarDto.
6. @BeanMapping
Что делает: Позволяет настроить общие правила маппинга для всего метода. Например, можно игнорировать все поля, которые не были явно сопоставлены.
Параметры:
ignoreByDefault— еслиtrue, то будут сопоставлены только поля, явно указанные в@Mapping.nullValueMappingStrategy— стратегия обработки null значений.
Пример:
@BeanMapping(ignoreByDefault = true)
@Mapping(source = "id", target = "userId")
@Mapping(source = "name", target = "userName")
UserDto userToUserDto(User user);
// Будут сопоставлены только id и name, остальные поля в UserDto останутся null.
7. @AfterMapping / @BeforeMapping
Что делает: Позволяют выполнить кастомную логику до или после основного маппинга. Методы с этими аннотациями должны быть определены в том же маппере (как default-методы).
Параметры: Не имеют.
Пример:
@Mapper
public interface OrderMapper {
@Mapping(source = "totalPrice", target = "price")
OrderDto orderToOrderDto(Order order);
@AfterMapping
default void setFormattedDate(Order order, @MappingTarget OrderDto order