在 Kotlin 的 data class 中使用 MapStruct

pexels-nguy?n-xuan-trung-899737.jpg

一. data class 的 copy() 为浅拷贝

浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。

data class 的 copy() 是复制函数,能够复制一个对象的全部属性,也能复制部分的属性。

例如下面的代码:

data class Address(var street:String)

data class User(var name:String,var password:String,var address: Address)

fun main(args: Array<String>) {
    val user1 = User("tony","123456", Address("renming"))

    val user2 = user1.copy()
    println(user2)

    println(user1.address===user2.address) // 判断 data class 的 copy 是否为浅拷贝,如果二者的address指向的内存地址相同则为浅拷贝,反之为深拷贝

    val user3 = user1.copy("monica")
    println(user3)

    val user4 = user1.copy(password = "abcdef")
    println(user4)
}

执行结果:

User(name=tony, password=123456, address=Address(street=renming))
true
User(name=monica, password=123456, address=Address(street=renming))
User(name=tony, password=abcdef, address=Address(street=renming))

user1.address===user2.address 打印的结果是 true 表示二者内存地址相同。 如果对象内部有引用类型的变量,通过拷贝后二者指向的是同一地址,表示为浅拷贝。所以 data class 的 copy 为浅拷贝。

当然,如果想实现深拷贝可以有很多种方式,比如使用序列化反序列化、一些开源库(例如:https://github.com/enbandari/KotlinDeepCopy

本文接下来要介绍的不是深拷贝,但跟深拷贝会有一些关系,是 Java Bean 到 Java Bean 的之间的映射。这样类似的工具有:Apache 的 BeanUtils、Dozer、MapStruct 等等。

二. MapStruct 简介

MapStruct 是一个基于JSR 269的 Java 注释处理器。开发者只需要定义一个 Mapper 接口,该接口声明任何所需的映射方法。在编译期间 MapStruct 将生成此接口的实现类。

使用 MapStruct 可以在两个 Java Bean 之间实现自动映射的功能,只需要创建好接口。由于它是在编译时自动创建具体的实现,因此无需反射等开销,在性能上也会好于 Apache 的 BeanUtils、Dozer 等。

三. Kotlin 中使用 MapStruct

在 github 上找到了一个 MapStruct Kotlin 实现的开源项目:https://github.com/Pozo/mapstruct-kotlin

3.1 mapstruct-kotlin 的安装:

添加 kapt 插件

apply plugin: 'kotlin-kapt'

然后在项目中添加如下依赖:

api("com.github.pozo:mapstruct-kotlin:1.3.1.2")
kapt("com.github.pozo:mapstruct-kotlin-processor:1.3.1.2")

另外,还需要添加如下依赖:

api("org.mapstruct:mapstruct:1.4.0.Beta3")
kapt("org.mapstruct:mapstruct-processor:1.4.0.Beta3")

3.2 mapstruct-kotlin 的基本使用

对于需要使用 MapStruct 的 data class,必须加上一个@KotlinBuilder注解

@KotlinBuilder
data class User(var name:String,var password:String,var address: Address)

@KotlinBuilder
data class UserDto(var name:String,var password:String,var address: Address)

通过添加@KotlinBuilder注解会在编译时生成 UserBuilder、UserDtoBuilder 对象,他们在 Mapper 的实现类中被使用,用于创建对象以及对对象的赋值。

再定义一个 Mapper:

@Mapper
interface UserMapper {

    fun toDto(user: User): UserDto
}

这样,就可以使用了。MapStruct 会在编译时自动生成好 UserMapperImpl 类,完成将 User 对象转换成 UserDto 对象。

fun main() {
    val userMapper = UserMapperImpl()

    val user = User("tony","123456", Address("renming"))

    val userDto = userMapper.toDto(user)

    println("${user.name},${user.address}")
}

执行结果:

tony,Address(street=renming)

3.3 mapstruct-kotlin 的复杂应用

对于稍微复杂的类:

// domain elements
@KotlinBuilder
data class Role(val id: Int, val name: String, val abbreviation: String?)

@KotlinBuilder
data class Person(val firstName: String, val lastName: String, val age: Int, val role: Role?)

// dto elements
@KotlinBuilder
data class RoleDto(val id: Int, val name: String, val abbreviation: String, val ignoredAttr: Int?)

@KotlinBuilder
data class PersonDto(
    val firstName: String,
    val phone: String?,
    val birthDate: LocalDate?,
    val lastName: String,
    val age: Int,
    val role: RoleDto?
)

Person 类中还包含有 Role 类,以及 Person 跟 PersonDto 的属性并不完全一致的情况。在 Mapper 接口中,支持使用@Mappings来做映射。

@Mapper(uses = [RoleMapper::class])
interface PersonMapper {

    @Mappings(
        value = [
            Mapping(target = "role", ignore = true),
            Mapping(target = "phone", ignore = true),
            Mapping(target = "birthDate", ignore = true),
            Mapping(target = "role.id", source = "role.id"),
            Mapping(target = "role.name", source = "role.name")
        ]
    )
    fun toDto(person: Person): PersonDto

    @Mappings(
        value = [
            Mapping(target = "age", ignore = true),
            Mapping(target = "role.abbreviation", ignore = true)
        ]
    )
    @InheritInverseConfiguration
    fun toPerson(person: PersonDto): Person

}

在 PersonMapper 的 toDto() 中,对于 PersonDto 没有的属性,在 Mapping 时可以使用ignore = true。

下面来看看,将 person 映射成 personDto,以及 personDto 再映射回 person。

fun main() {

    val role = Role(1, "role one", "R1")
    val person = Person("Tony", "Shen", 20, role)
    val personMapper = PersonMapperImpl()

    val personDto = personMapper.toDto(person)
    val personFromDto = personMapper.toPerson(personDto)
    println("personDto.firstName=${personDto.firstName}")
    println("personDto.role.id=${personDto.role?.id}")
    println("personDto.phone=${personDto.phone}")
    println("personFromDto.firstName=${personFromDto.firstName}")
    println("personFromDto.age=${personFromDto.age}")
}

执行结果:

personDto.firstName=Tony
personDto.role.id=1
personDto.phone=null
personFromDto.firstName=Tony
personFromDto.age=0

由于 Person 没有 phone 这个属性并且在 Mapping 时忽略了,因此转换成 PersonDto 后personDto.phone=null。

而 PersonDto 虽然有 age 属性,但是在 Mapping 时忽略了,因此转换成 Person 后personFromDto.age=0。

这样的结果达到了我们的预期。

总结

在使用 Kotlin 的 data class 时,如果需要做 Java Bean 之间的映射,使用 MapStruct 是一个很不错的选择。

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,128评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,316评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,737评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,283评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,384评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,458评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,467评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,251评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,688评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,980评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,155评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,818评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,492评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,142评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,382评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,020评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,044评论 2 352

推荐阅读更多精彩内容

  • google宣布Kotlin作为andorid一级开发语言有一段时间了。在这段时间,我也在新的??樯铣⑹允褂昧薻o...
    锋ivy阅读 1,030评论 1 2
  • 日常开发中,我们时长会写很多关于PO转VO的代码或者是VO转DTO相关的代码,造成我们的程序异常的臃肿。如下: 编...
    茶还是咖啡阅读 23,163评论 0 10
  • 基本用法 假设我们有两个类需要进行互相转换,分别是PersonDO和PersonDTO,类定义如下: 我们演示下如...
    GeekerLou阅读 5,445评论 2 7
  • 看下使用 之前遇到javaBean之间的转换,可能一般都用BeanUtil.copyProperties直接转换了...
    wang_cheng阅读 2,530评论 0 0
  • 久违的晴天,家长会。 家长大会开好到教室时,离放学已经没多少时间了。班主任说已经安排了三个家长分享经验。 放学铃声...
    飘雪儿5阅读 7,518评论 16 22