ModelMapper для Java: Полное руководство по автоматическому маппингу объектов
1. Что такое ModelMapper и зачем он нужен?
ModelMapper — это мощная библиотека для Java, предназначенная для автоматического преобразования (маппинга) объектов одного типа в объекты другого типа. В современной разработке, особенно при использовании многослойной архитектуры (Controller → Service → Repository), разработчики постоянно сталкиваются с необходимостью копировать данные между DTO (Data Transfer Objects), Entity (сущностями базы данных), VO (Value Objects) и другими POJO. Ручное написание такого кода — это утомительная, подверженная ошибкам работа, которая приводит к раздуванию кода и снижению его читаемости.
Основная цель ModelMapper — избавить разработчика от написания шаблонного кода вида source.getField() → target.setField(). Библиотека анализирует структуру классов (поля, геттеры/сеттеры) и автоматически сопоставляет их, используя соглашения об именовании (convention over configuration). Она поддерживает вложенные объекты, коллекции, наследование и сложные трансформации, что делает её незаменимым инструментом в корпоративных приложениях на Java.
ModelMapper особенно полезен в следующих сценариях:
- Слои приложения: Преобразование Entity в DTO и обратно для передачи данных через REST API.
- Микросервисы: Маппинг между объектами разных сервисов.
- Рефакторинг: Когда структура классов меняется, достаточно изменить маппинг в одном месте.
- Тестирование: Быстрое создание тестовых данных на основе существующих объектов.
2. Установка
ModelMapper распространяется через Maven Central. Для его установки необходимо добавить зависимость в файл pom.xml (для Maven) или build.gradle (для Gradle).
Maven (pom.xml):
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>3.2.0</version>
</dependency>
Gradle (build.gradle):
implementation 'org.modelmapper:modelmapper:3.2.0'
Примечание: Версия 3.2.0 актуальна на момент написания статьи. Рекомендуется проверять последнюю версию на MVNRepository.
3. Быстрый старт — минимальный рабочий пример
Рассмотрим простейший пример: у нас есть класс UserEntity (сущность базы данных) и UserDTO (объект для передачи данных).
// Исходный класс (Entity)
public class UserEntity {
private Long id;
private String firstName;
private String lastName;
private String email;
// Конструкторы, геттеры и сеттеры
public UserEntity() {}
public UserEntity(Long id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
}
// Целевой класс (DTO)
public class UserDTO {
private Long id;
private String fullName; // Обратите внимание: поле называется иначе
private String emailAddress;
// Конструкторы, геттеры и сеттеры
public UserDTO() {}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getEmailAddress() { return emailAddress; }
public void setEmailAddress(String emailAddress) { this.emailAddress = emailAddress; }
}
// Основной класс с примером
import org.modelmapper.ModelMapper;
public class Main {
public static void main(String[] args) {
// 1. Создаем экземпляр ModelMapper
ModelMapper modelMapper = new ModelMapper();
// 2. Создаем исходный объект
UserEntity user = new UserEntity(1L, "John", "Doe", "john.doe@example.com");
// 3. Выполняем маппинг
UserDTO userDTO = modelMapper.map(user, UserDTO.class);
// 4. Выводим результат
System.out.println("ID: " + userDTO.getId());
System.out.println("Full Name: " + userDTO.getFullName()); // null! (см. раздел 4)
System.out.println("Email: " + userDTO.getEmailAddress()); // null!
}
}
Почему fullName и emailAddress равны null? Потому что ModelMapper по умолчанию сопоставляет поля по имени (firstName → firstName, lastName → lastName). Поле fullName в DTO не совпадает ни с одним полем в Entity. Для таких случаев нужно использовать явные маппинги (см. раздел 4).
4. Основные методы/функции/классы
4.1. ModelMapper.map(Object source, Class<D> destinationType)
Сигнатура: public <D> D map(Object source, Class<D> destinationType)
Что делает: Основной метод для маппинга. Создает новый экземпляр целевого класса и копирует в него значения из исходного объекта, сопоставляя поля по имени и типу.
Пример:
UserEntity source = new UserEntity(1L, "Alice", "Smith", "alice@test.com");
UserDTO result = modelMapper.map(source, UserDTO.class);
// result.getId() == 1L
4.2. ModelMapper.map(Object source, Object destination)
Сигнатура: public void map(Object source, Object destination)
Что делает: Маппинг в уже существующий объект (destination). Полезно, когда нужно обновить поля существующего объекта, а не создавать новый.
Пример:
UserEntity source = new UserEntity(2L, "Bob", "Brown", "bob@test.com");
UserDTO existingDTO = new UserDTO();
existingDTO.setId(99L); // Старое значение
modelMapper.map(source, existingDTO);
// existingDTO.getId() == 2L (перезаписано из source)
4.3. PropertyMap — настройка маппинга полей
Сигнатура: public abstract class PropertyMap<S, D> { protected abstract void configure(); }
Что делает: Позволяет вручную определить, как поля исходного объекта (S) преобразуются в поля целевого объекта (D). Используется для решения конфликтов имен или для применения трансформаций.
Пример: Преобразуем firstName + " " + lastName в fullName.
import org.modelmapper.PropertyMap;
PropertyMap<UserEntity, UserDTO> userMap = new PropertyMap<>() {
@Override
protected void configure() {
map().setFullName(source.getFirstName() + " " + source.getLastName());
map().setEmailAddress(source.getEmail());
}
};
modelMapper.addMappings(userMap);
UserDTO result = modelMapper.map(user, UserDTO.class);
// result.getFullName() == "John Doe"
4.4. TypeMap — конфигурация для пары типов
Сигнатура: public <S, D> TypeMap<S, D> createTypeMap(Class<S> sourceType, Class<D> destinationType)
Что делает: Создает или получает существующую карту преобразований для конкретной пары классов. Позволяет централизованно управлять маппингом между двумя типами.
Пример:
TypeMap<UserEntity, UserDTO> typeMap = modelMapper.createTypeMap(UserEntity.class, UserDTO.class);
typeMap.addMappings(mapper -> {
mapper.map(UserEntity::getEmail, UserDTO::setEmailAddress);
mapper.using(ctx -> ((String)ctx.getSource()).toUpperCase())
.map(UserEntity::getFirstName, UserDTO::setFullName);
});
UserDTO result = modelMapper.map(user, UserDTO.class);
// result.getEmailAddress() == "john.doe@example.com"
4.5. Converter — пользовательский конвертер
Сигнатура: public interface Converter<S, D> { D convert(MappingContext<S, D> context); }
Что делает: Позволяет определить сложную логику преобразования одного типа в другой. Например, преобразование строки в дату или числа в строку.
Пример: Преобразование String (дата) в LocalDate.
import org.modelmapper.Converter;
import org.modelmapper.spi.MappingContext;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
Converter<String, LocalDate> stringToDate = new Converter<>() {
@Override
public LocalDate convert(MappingContext<String, LocalDate> context) {
return LocalDate.parse(context.getSource(), DateTimeFormatter.ISO_LOCAL_DATE);
}
};
modelMapper.addConverter(stringToDate);
// Теперь все поля String → LocalDate будут использовать этот конвертер
4.6. Provider — кастомное создание целевых объектов
Сигнатура: public interface Provider<T> { T get(); }
Что делает: Определяет, как создавать экземпляры целевого класса, если конструктор по умолчанию не подходит (например, если класс использует билдер или фабрику).
Пример:
import org.modelmapper.Provider;
Provider<UserDTO> userDTOProvider = new Provider<>() {
@Override
public UserDTO get() {
return UserDTO.builder().build(); // Предположим, есть билдер
}
};
modelMapper.createTypeMap(UserEntity.class, UserDTO.class)
.setProvider(userDTOProvider);
4.7. Condition — условный маппинг
Сигнатура: public interface Condition<S, D> { boolean applies(MappingContext<S, D> context); }
Что делает: Позволяет выполнять маппинг поля только при выполнении определенного условия. Например, копировать поле только если оно не null.
Пример: Копируем email только если он не пустой.
import org.modelmapper.Condition;
import org.modelmapper.spi.MappingContext;
Condition<String, String> notEmpty = new Condition<>() {
@Override
public boolean applies(MappingContext<String, String> context) {
return context.getSource() != null && !context.getSource().isEmpty();
}
};
modelMapper.createTypeMap(UserEntity.class, UserDTO.class)
.addMappings(mapper -> mapper.when(notEmpty).map(UserEntity::getEmail, UserDTO::setEmailAddress));
4.8. ModelMapper.getConfiguration() — глобальная настройка
Сигнатура: public Configuration getConfiguration()
Что делает: Возвращает объект конфигурации, который позволяет настроить глобальное поведение маппера: режим совпадения полей (MATCHING_STRATEGY), обработка null, игнорирование неизвестных полей и т.д.
Пример:
import org.modelmapper.config.Configuration.AccessLevel;
import org.modelmapper.convention.MatchingStrategies;
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.STRICT) // Строгое совпадение имен
.setFieldMatchingEnabled(true) // Маппинг по полям (а не только по геттерам/сеттерам)
.setFieldAccessLevel(AccessLevel.PRIVATE) // Доступ к приватным полям
.setSkipNullEnabled(true); // Пропускать null значения при маппинге
5. Реальный практический пример (150+ строк кода)
Рассмотрим полноценный пример: маппинг между сущностью Order и DTO OrderResponse с вложенными объектами и вычисляемыми полями.
// ========== Модели данных ==========
// Исходная сущность (Entity)
class OrderEntity {
private Long id;
private String orderNumber;
private CustomerEntity customer;
private List<OrderItemEntity> items;
private LocalDateTime createdAt;
// Геттеры и сеттеры...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getOrderNumber() { return orderNumber; }
public void setOrderNumber(String orderNumber) { this.orderNumber = orderNumber; }
public CustomerEntity getCustomer() { return customer; }
public void setCustomer(CustomerEntity customer) { this.customer = customer; }
public List<OrderItemEntity> getItems() { return items; }
public void setItems(List<OrderItemEntity> items) { this.items = items; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
}
class CustomerEntity {
private Long id;
private String fullName;
private String email;
// Геттеры и сеттеры...
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getFullName() { return fullName; }
public void setFullName(String fullName) { this.fullName = fullName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; } Также в библиотеке
Event Bus и паттерн Observer в JavaScript (Node.js): реализация и примеры
CLI фреймворки для Java: обзор и сравнение лучших инструментов
Работа с YAML в Swift (iOS/macOS) — библиотеки Yams и YamlSwift
Работа с Apache Kafka в Node.js: библиотека KafkaJS