Невеликий мануал з MapStruct
Для перетворення одних класів на інші зручно використовувати бібліотеки для автоматичного перетворення замість того, щоб писати мапери вручну. На мою думку, одна з найкращих бібліотек для цього MapStruct
Налаштування MapStruct
Її перевага — помилки при перетворенні, наприклад якщо формат не збігається, можуть показуватися при компіляції проєкту, а не в рантаймі, для цього створюємо
import org.mapstruct.MapperConfigimport org.mapstruct.ReportingPolicy
@MapperConfig(unmappedTargetPolicy = ReportingPolicy.ERROR)interface StrictMapperConfigі використовуємо його надалі. Я буду використовувати базовий клас BaseMapper
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, передбачаються звичайні функції для перетворення, використовуйте абстрактний клас, а не інтерфейс, при використанні функцій інтерфейсу велика ймовірність помилки при компіляції
Найпростіший варіант перетворення
Найпростіший варіант перетворення, коли класи повністю збігаються
data class To( val one: String, val two: String,)
data class From( val one: String, val two: String,)import org.mapstruct.Mapperimport 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, або кілька анотацій, якщо це необхідно
data class To( val oneNewName: String, val twoNewName: String,)
data class From( val one: String, val two: String,)import org.mapstruct.Mapperimport 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) }}Перетворення полів різних типів
А ось приклад перетворення, якщо необхідно замінити одне з полів, наприклад вони різного типу
data class To( val one: List<String>, val two: String,)
data class From( val one: String, val two: String,)import org.mapstruct.Mapperimport 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
data class To( val one: List<String>, val two: String,)
data class From( val one: String, val two: String,)import org.mapstruct.Mapperimport 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) }}Перевикористання логіки
Щоб перевикористати логіку, можна розділити перетворення на різні класи / інтерфейси
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,)import org.mapstruct.Mapperimport 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) }}Виклик абстрактної функції зі звичайної
можна навпаки зі звичайної функції викликати абстрактну, якщо, наприклад, потрібно перетворити вкладені списки з іншими класами, вміст яких однаковий
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,)import org.mapstruct.Mapperimport 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
implementation 'org.simpleframework:simple-xml:2.7.1'У прикладі, для зручності, у кінцевому класі зробив 2 вкладені класи — з даними з оригінального класу та даними з XML, але структура може бути будь-якою
<data> <one>text one</one> <two>text two</two></data>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?,)import org.simpleframework.xml.Elementimport 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,)import org.mapstruct.Mapperimport org.mapstruct.factory.Mappersimport org.koin.core.component.injectimport org.koin.core.component.KoinComponentimport 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(FromXML::class.java).mapJSON(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, ) } }}