Кратко ръководство за 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(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, ) } }}