Перейти к содержимому

Небольшой мануал по MapStruct

Для преобразования одних классов в другие удобно использовать библиотеки для автоматического преобразования вместо того, чтобы писать мапперы вручную. На мой взгляд одна из лучших библиотек для этого MapStruct

https://mapstruct.org

Ее преимущество- ошибки при преобразовании, например если формат не совпадает, могут показываться при компиляции проекта, а не в рантайме, для этого создаем

import org.mapstruct.MapperConfig
import 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.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, или несколько аннотаций, если это необходимо

data class To(
    val oneNewName: String,
    val twoNewName: String,
)

data class From(
    val one: String,
    val two: String,
)
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)
    }
}

А вот пример преобразования, если необходимо заменить одно из полей, например они разного типа

data class To(
    val one: List<String>,
    val two: String,
)

data class From(
    val one: String,
    val two: String,
)
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

data class To(
    val one: List<String>,
    val two: String,
)

data class From(
    val one: String,
    val two: String,
)
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)
    }
}

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

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.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)
    }
}

можно наоборот из обычной функции вызывать абстрактную, если например нужно преобразовать вложенные листы с другими классами, содержимое которых одинаково

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.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, который необходимо распарсить в класс, для этого буду использовать библиотеку 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.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,
)
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(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,
            )
        }
    }
}

Copyright: Roman Kryvolapov