MapStruct
Kora интегрирует библиотеки преобразования объектов так: реализация, которую такая библиотека генерирует во время компиляции, становится обычным компонентом контейнера зависимостей.
Поддерживаются две библиотеки:
- MapStruct — обработчик аннотаций Java, доступен из Java напрямую, а из Kotlin — через kapt.
- Konvert —
KSP-обработчик для Kotlin, работающий в той же компиляции, что и собственныеKSP-обработчики Kora.
Обе интеграции — это расширения времени компиляции для обработчика Kora. Ни одна из них не требует отдельного артефакта Kora, модуля в @KoraApp, секции конфигурации или зависимости времени выполнения: вы объявляете маппер так, как это описано в документации самой библиотеки, а Kora разрешает сгенерированную реализацию в точке внедрения.
Какую библиотеку выбрать
В Java используйте MapStruct. В Kotlin предпочтительнее Konvert: MapStruct — это обработчик аннотаций Java, его приходится запускать через kapt, а это отдельный конвейер компиляции по отношению к KSP, в котором работают Kotlin-обработчики Kora. Konvert же не требует никакой дополнительной настройки сборки.
Подключение¶
Расширение для MapStruct поставляется внутри стандартного артефакта обработчиков Kora — io.koraframework:annotation-processors для Java и io.koraframework:symbol-processors для Kotlin — так что со стороны Kora добавлять нечего.
Расширение регистрируется через ServiceLoader и активируется только тогда, когда тип аннотации org.mapstruct.Mapper доступен в classpath: в проекте без MapStruct оно не делает ничего.
Зависимость в build.gradle:
annotationProcessor "org.mapstruct:mapstruct-processor:1.6.3" //(1)!
annotationProcessor "io.koraframework:annotation-processors" //(2)!
implementation "org.mapstruct:mapstruct:1.6.3" //(3)!
- Обработчик аннотаций
MapStruct— именно он генерирует реализацию<Mapper>Impl(обязательный, без значения по умолчанию). - Обработчики аннотаций Kora, в составе которых идёт расширение для
MapStruct(обязательный, без значения по умолчанию). - Сами аннотации
MapStruct:@Mapper,@Mappingи прочие (обязательный, без значения по умолчанию).
Оба обработчика должны быть объявлены в одной и той же конфигурации annotationProcessor, чтобы они работали в рамках одной компиляции: Kora ищет реализацию, которую MapStruct создал на более раннем раунде обработки аннотаций этой же компиляции.
MapStruct — это обработчик аннотаций Java, и запустить его через KSP нельзя, поэтому в Kotlin он работает через kapt, тогда как собственные обработчики Kora по-прежнему работают через KSP.
Расширение Kora для KSP только ищет тип <Mapper>Impl — создание этого типа остаётся задачей kapt.
Плагины в build.gradle.kts:
plugins {
kotlin("jvm") version ("2.4.10")
kotlin("kapt") version ("2.4.10")
id("com.google.devtools.ksp") version ("2.3.11")
}
Зависимость в build.gradle.kts:
kapt("org.mapstruct:mapstruct-processor:1.6.3") //(1)!
ksp("io.koraframework:symbol-processors:2.0.0.RC1") //(2)!
implementation("org.mapstruct:mapstruct:1.6.3") //(3)!
- Обработчик аннотаций
MapStruct, запускаемый черезkapt(обязательный, без значения по умолчанию). KSP-обработчики Kora, в составе которых идёт расширение дляMapStruct(обязательный, без значения по умолчанию).- Сами аннотации
MapStruct:@Mapper,@Mappingи прочие (обязательный, без значения по умолчанию).
KSP должен видеть то, что сгенерировал kapt, и запускаться после него:
ksp {
allowSourcesFromOtherPlugins = true
}
tasks.matching { it.name.startsWith("ksp") }.configureEach {
dependsOn(tasks.named("kaptGenerateStubsKotlin"))
dependsOn(tasks.named("kaptKotlin"))
}
kapt и KSP в одном модуле
kapt и KSP — два независимых конвейера компиляции, и заставить их работать вместе в одном модуле непросто: сборка может завершаться успешно только со второй попытки, а приведённую выше настройку приходится перепроверять при каждом обновлении плагина Kotlin или KSP.
Для Kotlin поддерживаемый и куда более простой вариант — Konvert, который является KSP-обработчиком и ничего из этого не требует.
Использование¶
Написание самих мапперов целиком лежит на MapStruct; Kora лишь добавляет расширение времени компиляции, которое публикует сгенерированные мапперы в контейнере зависимостей.
Когда графу требуется зависимость, тип которой — интерфейс или абстрактный класс с аннотацией @Mapper, расширение находит класс <Mapper>Impl, сгенерированный MapStruct в том же пакете, и передаёт графу его единственный публичный конструктор.
Поэтому маппер не нужно помечать аннотацией @Component, не нужно подключать никакой модуль и не нужен componentModel = "kora" — стандартная модель компонентов MapStruct работает как есть.
Объявите маппер стандартным для 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
}
При componentModel = "jakarta" MapStruct помечает сгенерированную реализацию аннотациями jakarta.inject, поэтому jakarta.inject:jakarta.inject-api должен быть в classpath компиляции модуля. Сама Kora эти аннотации не читает и не требует — она использует лишь тот конструктор, который данная модель компонентов заставляет MapStruct сгенерировать.
Тег¶
@Mapper может быть уточнён тегом @Tag. Объявите один и тот же тег на маппере и в точке внедрения — и маппер будет предоставлен именно там. Это позволяет зарегистрировать несколько мапперов одного типа и различать их:
Ошибки¶
Расширение сообщает о проблемах с маппером как об ошибках компиляции обработчика Kora:
MapStruct mapper implementation was not generated for ...— графу потребовался тип с@Mapper, но класса<Mapper>Implв текущей компиляции нет. Либо обработчикMapStructне объявлен в конфигурации обработчиков этого модуля (в Kotlin — не объявлен вkapt, либоKSPзапускается раньшеkapt), либо самMapStructзавершился с ошибкой раньше и вывел собственные сообщения выше этого. Исправьте первую сообщённую ошибку и пересоберите проект.Generated dependency class was not found: expected type: ...— та же ситуация со стороны Kotlin-расширенияKSP.Invalid MapStruct mapper implementation for ...— у сгенерированного<Mapper>Implбольше одного публичного конструктора либо нет ни одного, и Kora не может решить, как его создавать. Обычно это означает неожиданное сочетаниеcomponentModelиinjectionStrategy: либо поправьте их, либо откажитесь от расширения для этого маппера и предоставьте реализацию вручную как обычный @Component.
Konvert¶
Konvert генерирует мапперы для Kotlin средствами KSP.
Поскольку это KSP-обработчик, он работает в той же компиляции, что и Kotlin-обработчики Kora, поэтому проекту не нужен ни kapt, ни настройка задач — Konvert подключается просто как ещё одна зависимость ksp(...).
Расширение для Konvert поставляется внутри io.koraframework:symbol-processors. Как и в случае с MapStruct, оно регистрируется через ServiceLoader и активируется только тогда, когда тип аннотации io.mcarle.konvert.api.Konverter доступен в classpath.
Konvert работает только с Kotlin — для Java используйте MapStruct.
Подключение¶
Зависимость в build.gradle.kts:
ksp("io.mcarle:konvert:4.5.1") //(1)!
ksp("io.koraframework:symbol-processors:2.0.0.RC1") //(2)!
implementation("io.mcarle:konvert-api:4.5.1") //(3)!
KSP-обработчикKonvert— именно он генерирует реализации мапперов (обязательный, без значения по умолчанию).KSP-обработчики Kora, в составе которых идёт расширение дляKonvert(обязательный, без значения по умолчанию).- Аннотации
Konvert, в том числе@Konverter(обязательный, без значения по умолчанию).
Использование¶
Пометьте интерфейс аннотацией @Konverter и объявите преобразования его методами:
data class Car(val make: String, val seatCount: Int)
data class CarDto(val make: String, val seatCount: Int)
@Konverter
interface CarMapper {
fun carToCarDto(car: Car): CarDto
}
Konvert генерирует в том же пакете объект верхнего уровня object CarMapperImpl : CarMapper, а расширение Kora публикует этот объект в графе под типом CarMapper.
Как и в случае с MapStruct, маппер не нужно помечать аннотацией @Component и не нужно подключать никакой модуль.
Переименование полей, вычисляемые значения и любые другие правила по отдельным свойствам задаются собственными аннотациями Konvert — см. документацию Konvert.
Внедрённый маппер — обычный компонент:
@Component
class CarService(private val carMapper: CarMapper) {
fun convert(car: Car): CarDto {
return carMapper.carToCarDto(car)
}
}
Тег¶
@Konverter может быть уточнён тегом @Tag точно так же, как и @Mapper: объявите один и тот же тег на интерфейсе маппера и в точке внедрения.
@Tag(MyTag::class)
@Konverter
interface CarMapper {
fun carToCarDto(car: Car): CarDto
}
@Component
class CarService(@Tag(MyTag::class) private val carMapper: CarMapper)
Ограничения¶
Расширение для Konvert намеренно уже, чем расширение для MapStruct:
@Konverterраспознаётся только на интерфейсе. Абстрактный класс с аннотацией@Konverterрасширением не предоставляется.- Сгенерированная реализация — это Kotlin-
object, у него нет конструктора, и он не может получить ничего из графа. Маппер, которому нужен вспомогательный компонент, приходится писать как обычный @Component, делегирующий сгенерированному объектуCarMapperImpl. - Реализация ищется как
<SimpleName>Implв пакете самого маппера. Для@Konverter, вложенного в другой тип,Konvertвсё равно генерирует объект верхнего уровня и отбрасывает имя внешнего типа — поэтому поиск отбрасывает его тоже.
Ошибки¶
Generated Konvert implementation was not found: expected type: ...— графу потребовался тип с@Konverter, но сгенерированного объекта нет. ЛибоKSP-обработчикKonvertне объявлен в этом модуле, либоKonvertзавершился с ошибкой раньше в той же компиляции и вывел собственные сообщения выше этого. Исправьте первую сообщённую ошибку и пересоберите проект.