Kora облачно ориентированный серверный фреймворк написанный на Java для написания Java / Kotlin приложений с упором на производительность, эффективность, прозрачность сделанный выходцами из Т-Банк / Тинькофф

Kora is a cloud-oriented server-side Java framework for writing Java / Kotlin applications with a focus on performance, efficiency and transparency

Перейти к содержанию

MapStruct

Модуль позволяет интегрировать библиотеку MapStruct для преобразования классов между собой.

Подключение

Интеграция с Kora — это расширение времени компиляции, которое активируется автоматически, как только mapstruct-processor оказывается в classpath обработчиков аннотаций — никакой дополнительный артефакт Kora или подключение модуля не требуется.

Зависимость build.gradle:

annotationProcessor "org.mapstruct:mapstruct-processor:1.5.5.Final"
implementation "org.mapstruct:mapstruct:1.5.5.Final"

MapStruct в Kotlin работает через kapt, поэтому требуется настроить плагин kapt в build.gradle.kts:

plugins {
    kotlin("kapt") version ("1.9.10")
}

Последняя рабочая версия для 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:

kapt("org.mapstruct:mapstruct-processor:1.5.5.Final")
implementation("org.mapstruct:mapstruct:1.5.5.Final")

Использование

Создание самих мапперов возлагается на библиотеку MapStruct; Kora лишь добавляет расширение времени компиляции, которое делает сгенерированные мапперы доступными в контейнере зависимостей.

Расширение регистрируется автоматически через ServiceLoader и активируется, как только аннотация org.mapstruct.Mapper присутствует в classpath (расширение обработчика аннотаций для Java, расширение KSP для Kotlin). Для каждого запрошенного интерфейса или абстрактного класса @Mapper оно находит сгенерированную MapStruct реализацию <Mapper>Impl в том же пакете и предоставляет её публичный конструктор как компонент. Благодаря этому вам не нужен ни модуль Kora, ни какая-либо конфигурация, и вам не нужен componentModel = "kora" — стандартный componentModel работает из коробки.

Объявите маппер стандартным для MapStruct способом, и он станет доступным для внедрения:

public enum CarType { TYPE1, TYPE2 }

public record Car(String make, int numberOfSeats, CarType type) { }

public record CarDto(String make, int seatCount, String type) { }

@Mapper
public interface CarMapper {

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto map(Car car);
}
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, как и любую другую зависимость:

@Component
public final class CarService {

    private final CarMapper carMapper;

    public CarService(CarMapper carMapper) {
        this.carMapper = carMapper;
    }

    public CarDto convert(Car car) {
        return carMapper.map(car);
    }
}
@Component
class CarService(private val carMapper: CarMapper) {

    fun convert(car: Car): CarDto {
        return carMapper.map(car)
    }
}

Зависимости маппера

Маппер часто делегирует работу вспомогательным мапперам или сервисам. 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, и расширение предоставляет маппер только тогда, когда запрошенные теги совпадают с тегами, объявленными на типе маппера. Это позволяет зарегистрировать несколько мапперов одного типа и различать их в точке внедрения:

@Tag(MyTag.class)
@Mapper
public interface CarMapper {

    @Mapping(source = "numberOfSeats", target = "seatCount")
    CarDto map(Car car);
}

@Component
public final class CarService {

    public CarService(@Tag(MyTag.class) CarMapper carMapper) {
        // ...
    }
}
@Tag(MyTag::class)
@Mapper
interface CarMapper {

    @Mapping(source = "numberOfSeats", target = "seatCount")
    fun map(car: Car): CarDto
}

@Component
class CarService(@Tag(MyTag::class) private val carMapper: CarMapper)