Roman Kryvolapov Engineering Blog

Кратко ръководство за MapStruct

За преобразуване на едни класове в други е удобно да се използват библиотеки за автоматично преобразуване, вместо мапърите да се пишат на ръка. Според мен една от най-добрите библиотеки за това е MapStruct

https://mapstruct.org

Настройка на MapStruct

Предимството ѝ е, че грешките при преобразуване, например при несъвпадение на формата, могат да се показват още при компилацията на проекта, а не по време на изпълнение; за целта създаваме

Kotlin
import org.mapstruct.MapperConfig
import org.mapstruct.ReportingPolicy
@MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR)
interface StrictMapperConfig

и го използваме по-нататък. Аз ще използвам базовия клас BaseMapper

Kotlin
abstract class BaseMapper<From, To> {
abstract fun map(from: From): To
open fun mapList(fromList: List<From>): List<To> {
return fromList.mapTo(ArrayList(fromList.size), this::map)
}
}

Важни забележки

ВНИМАНИЕ: ако в един от моделите, които трябва да се преобразуват, има само едно поле, MapStruct връща грешка

error: Unmapped target property: "copy". public abstract com._.To map(@org.jetbrains.annotations.NotNull

Подобна грешка може да възникне например и когато класове наследяват интерфейси, несъвместими с MapStruct, или в някои други случаи.

ВНИМАНИЕ: MapStruct, както личи от използването на анотации, генерира код за преобразуването и ако използвате имена на променливи във формат is…, мапърът ще върне грешка заради конфликт в логиката. Най-доброто решение е да преименувате променливата; има и други решения в stackoverflow.

ВНИМАНИЕ: MapStruct не умее да обработва нормално nullable и не-nullable стойности – следете типът на променливата в началния и в крайния клас да е един и същ

ВНИМАНИЕ: ако в интерфейса, отбелязан с анотацията Mapper, се предполагат обикновени функции за преобразуване, използвайте абстрактен клас, а не интерфейс – при използване на функции на интерфейса вероятността за грешка при компилация е голяма

Най-простият вариант на преобразуване

Най-простият вариант на преобразуване, когато класовете напълно съвпадат

Kotlin
data class To(
val one: String,
val two: String,
)
data class From(
val one: String,
val two: String,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
class SomeModelMapper : BaseMapper<From, To>() {
@Mapper(config = StrictMapperConfig::class)
fun interface ModelMapper {
fun map(from: From): To
}
override fun map(from: From): To {
return Mappers.getMapper(ModelMapper::class.java).map(from)
}
}

Маппинг с различни имена на полетата

Ако името на едно или няколко полета се различава, добавяме анотацията Mapping или няколко анотации, ако е необходимо

Kotlin
data class To(
val oneNewName: String,
val twoNewName: String,
)
data class From(
val one: String,
val two: String,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
class SomeModelMapper : BaseMapper<From, To>() {
@Mapper(config = StrictMapperConfig::class)
fun interface ModelMapper {
@Mapping(source = "one", target = "oneNewName")
@Mapping(source = "two", target = "twoNewName")
fun map(from: From): To
}
override fun map(from: From): To {
return Mappers.getMapper(ModelMapper::class.java).map(from)
}
}

Преобразуване на полета от различни типове

А ето пример за преобразуване, ако е необходимо да се замени едно от полетата, например когато са от различен тип

Kotlin
data class To(
val one: List<String>,
val two: String,
)
data class From(
val one: String,
val two: String,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
class SomeModelMapper : BaseMapper<From, To>() {
@Mapper(config = StrictMapperConfig::class)
abstract class ModelMapper {
abstract fun map(from: From): To
fun mapOne(one: String): List<String> {
return listOf(one)
}
}
override fun map(from: From): To {
return Mappers.getMapper(ModelMapper::class.java).map(from)
}
}

Използване на анотацията Named

Можете да използвате и анотацията Named

Kotlin
data class To(
val one: List<String>,
val two: String,
)
data class From(
val one: String,
val two: String,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
class SomeModelMapper : BaseMapper<From, To>() {
companion object {
private const val MAP_ONE = "MAP_ONE"
}
@Mapper(config = StrictMapperConfig::class)
abstract class ModelMapper {
@Mapping(target = "one", source = "one", qualifiedByName = [MAP_ONE])
abstract fun map(from: From): To
@Named(MAP_ONE)
fun mapOne(one: String): List<String> {
return listOf(one)
}
}
override fun map(from: From): To {
return Mappers.getMapper(ModelMapper::class.java).map(from)
}
}

Преизползване на логиката

За да преизползваме логиката, можем да разделим преобразуването на различни класове / интерфейси

Kotlin
data class To(
val one: String,
val two: List<ToItem>,
)
data class ToItem(
val three: List<String>,
val four: String,
)
data class From(
val one: String,
val two: List<FromItem>,
)
data class FromItem(
val three: String,
val four: String,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
class SomeModelMapper : BaseMapper<From, To>() {
@Mapper(config = StrictMapperConfig::class, uses = [ItemMapper::class])
fun interface ModelMapper {
fun map(from: From): To
}
@Mapper(config = StrictMapperConfig::class)
abstract class ItemMapper {
fun mapThree(three: String): List<String> {
return listOf(three)
}
}
override fun map(from: From): To {
return Mappers.getMapper(ModelMapper::class.java).map(from)
}
}

Извикване на абстрактна функция от обикновена

Може и обратното – от обикновена функция да се извиква абстрактна, ако например трябва да се преобразуват вложени списъци с други класове, чието съдържание е еднакво

Kotlin
data class To(
val one: String,
val two: List<ToItem>,
)
data class ToItem(
val three: String,
val four: String,
)
data class From(
val one: String,
val two: List<FromItem>,
)
data class FromItem(
val three: String,
val four: String,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
class SomeModelMapper : BaseMapper<From, To>() {
@Mapper(config = StrictMapperConfig::class)
abstract class ModelMapper {
fun map(from: From): To {
return with(from) {
To(
one = one,
two = two.map(::mapItem),
)
}
}
abstract fun mapItem(two: FromItem): ToItem
}
override fun map(from: From): To {
return Mappers.getMapper(ModelMapper::class.java).map(from)
}
}

По аналогия могат да се създават и по-сложни конструкции, като в абстрактния клас с анотацията Mapper се използват абстрактни и обикновени функции, като същевременно се спазват имената на променливите и методите. MapStruct като цяло е доста капризна библиотека с много нюанси, но от тези, които съм пробвал да използвам, се оказа най-безопасната откъм грешки при работа.

Пример с парсване на XML

Ще приведа още един интересен пример, когато в модела в текстово поле има XML, който трябва да се парсне в клас; за целта ще използвам библиотеката org.simpleframework.xml

https://javadoc.io/doc/org.simpleframework/simple-xml/latest/index.html

Groovy
implementation 'org.simpleframework:simple-xml:2.7.1'

В примера за удобство в крайния клас направих 2 вложени класа – с данни от оригиналния клас и с данни от XML, но структурата може да бъде произволна

XML
<data>
<one>text one</one>
<two>text two</two>
</data>
Kotlin
data class To(
val data: ToData?,
val xml: ToXML?,
)
data class ToData(
val one: String?,
val two: String?,
)
data class ToXML(
val one: String?,
val two: String?,
)
data class From(
val one: String?,
val two: String?,
val xml: String?,
)
Kotlin
import org.simpleframework.xml.Element
import org.simpleframework.xml.Root
@Root(name = "data")
data class FromXML(
@field:Element(name = "one", required = false)
var one: String? = null,
@field:Element(name = "two", required = false)
var two: String? = null,
)
Kotlin
import org.mapstruct.Mapper
import org.mapstruct.factory.Mappers
import org.koin.core.component.inject
import org.koin.core.component.KoinComponent
import org.simpleframework.xml.core.Persister
class SomeModelMapper: BaseMapper<From, To>(), KoinComponent {
private val serializer: Persister by inject()
@Mapper(config = StrictMapperConfig::class)
interface ModelMapper {
fun mapData(from: From): ToData
fun mapXML(from: FromXML): ToXML
}
override fun map(from: From): To {
val data = Mappers.getMapper(ModelMapper::class.java).mapData(from)
return try {
val xml = serializer.read(FromXML::class.java, from.xml!!)!!
To(
data = data,
xml = Mappers.getMapper(ModelMapper::class.java).mapXML(xml),
)
} catch (e: Exception) {
logError("parse xml exception: ${e.message}", e, TAG)
To(
data = data,
xml = null,
)
}
}
}

Copyright: Roman Kryvolapov