Для преобразования одних классов в другие удобно использовать библиотеки для автоматического преобразования вместо того, чтобы писать мапперы вручную. На мой взгляд одна из лучших библиотек для этого MapStruct
Ее преимущество- ошибки при преобразовании, например если формат не совпадает, могут показываться при компиляции проекта, а не в рантайме, для этого создаем
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,
)
}
}
}