MapStruct
Модуль позволяет интегрировать библиотеку MapStruct для преобразования классов между собой.
Подключение¶
Интеграция с Kora — это расширение времени компиляции, которое активируется автоматически, как только mapstruct-processor
оказывается в classpath обработчиков аннотаций — никакой дополнительный артефакт Kora или подключение модуля не требуется.
Зависимость build.gradle:
MapStruct в Kotlin работает через kapt, поэтому требуется настроить плагин kapt в build.gradle.kts:
Последняя рабочая версия для kapt + ksp — это 1.9.10-1.0.13, в более поздних версиях KSP совместимость между двумя инструментами нарушена на уровне Gradle-плагина.
Необходимо разрешить использование выходных данных kapt в качестве входных для KSP в build.gradle.kts:
ksp {
allowSourcesFromOtherPlugins = true
}
tasks.withType<KspTask> {
dependsOn(tasks.named("kaptGenerateStubsKotlin").get())
dependsOn(tasks.named("kaptKotlin").get())
}
Успешная сборка приложения возможна только со второй попытки, это особенность поведения KSP.
Зависимость build.gradle.kts:
Использование¶
Создание самих мапперов возлагается на библиотеку MapStruct; Kora лишь добавляет расширение времени компиляции, которое делает сгенерированные мапперы доступными в контейнере зависимостей.
Расширение регистрируется автоматически через ServiceLoader и активируется, как только аннотация org.mapstruct.Mapper
присутствует в classpath (расширение обработчика аннотаций для Java, расширение KSP для Kotlin). Для каждого
запрошенного интерфейса или абстрактного класса @Mapper оно находит сгенерированную MapStruct реализацию <Mapper>Impl в том же пакете
и предоставляет её публичный конструктор как компонент. Благодаря этому вам не нужен ни модуль Kora, ни какая-либо конфигурация, и вам
не нужен componentModel = "kora" — стандартный componentModel работает из коробки.
Объявите маппер стандартным для MapStruct способом, и он станет доступным для внедрения:
enum class CarType { TYPE1, TYPE2 }
data class Car(val make: String, val numberOfSeats: Int, val type: CarType)
data class CarDto(val make: String, val seatCount: Int, val type: String)
@Mapper
interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
fun map(car: Car): CarDto
}
@Mapper поддерживается как на интерфейсах, так и на абстрактных классах, а также на мапперах, вложенных внутрь внешнего типа — в
случае вложенного типа расширение определяет сгенерированную реализацию, соединяя имена внешних типов через $
(например, SomeInterface.CarMapper становится SomeInterface$CarMapperImpl).
Использование в сервисе¶
Внедрённый маппер — это обычный компонент Kora, поэтому вы внедряете его через конструктор в сервис @Component, как и любую другую зависимость:
Зависимости маппера¶
Маппер часто делегирует работу вспомогательным мапперам или сервисам. MapStruct связывает такие вспомогательные компоненты через атрибут uses
аннотации @Mapper. Чтобы Kora предоставляла их из контейнера зависимостей (вместо того, чтобы MapStruct создавала их сам), сгенерируйте
реализацию с внедрением через конструктор: задайте injectionStrategy = InjectionStrategy.CONSTRUCTOR и
componentModel = "jakarta". Тогда сгенерированный <Mapper>Impl получает каждый тип из uses через свой публичный конструктор, и
Kora разрешает каждый из них из графа — поэтому вспомогательный компонент должен быть доступен как компонент (например, помеченный аннотацией
@Component или предоставленный фабрикой).
@Component
public final class DateMapper {
public String asString(Date date) {
return date != null ? new SimpleDateFormat("yyyy-MM-dd").format(date) : null;
}
public Date asDate(String date) throws ParseException {
return date != null ? new SimpleDateFormat("yyyy-MM-dd").parse(date) : null;
}
}
@Mapper(uses = DateMapper.class,
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
componentModel = "jakarta")
public interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
CarDto map(Car car);
}
@Component
class DateMapper {
fun asString(date: Date?): String? =
date?.let { SimpleDateFormat("yyyy-MM-dd").format(it) }
fun asDate(date: String?): Date? =
date?.let { SimpleDateFormat("yyyy-MM-dd").parse(it) }
}
@Mapper(uses = [DateMapper::class],
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
componentModel = "jakarta")
interface CarMapper {
@Mapping(source = "numberOfSeats", target = "seatCount")
fun map(car: Car): CarDto
}
Тег¶
@Mapper может быть уточнён тегом @Tag, и расширение предоставляет маппер только тогда, когда запрошенные
теги совпадают с тегами, объявленными на типе маппера. Это позволяет зарегистрировать несколько мапперов одного типа и различать их
в точке внедрения: