在 Kotlin 的 data class 中使用 MapStruct

简介: 在 Kotlin 的 data class 中使用 MapStruct

一. 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 是一个很不错的选择。

相关文章
|
Kotlin
Kotlin | 实现数据类(data)深拷贝
在Kotlin中,data数据类默认的copy方法实现的是浅拷贝,但我们有时候需要实现深拷贝。 在kotlin中,实现就比较容易了。
535 0
Kotlin | 实现数据类(data)深拷贝
|
Java Kotlin
Kotlin data数据类、copy()函数、sealed密封类
Kotlin data数据类、copy()函数、sealed密封类使用
114 0
|
12月前
|
安全 Java 编译器
3 亿美元的 bug,Kotlin 帮你避免 | 内联类 value class
3 亿美元的 bug,Kotlin 帮你避免 | 内联类 value class
92 0
|
IDE 编译器 开发工具
深入学习 Kotlin 特色之 Sealed Class 和 Interface
深入学习 Kotlin 特色之 Sealed Class 和 Interface
DHL
|
算法 Java 编译器
容易被忽视的几个 Kotlin 细节, value class 执行效率竟然这么高
今天这篇文章主要介绍 value class 和 data class 的区别,这可能是平时在做业务开发的时候,容易被忽视的几个细节。通过这篇文章,你将学习到以下内容。
DHL
384 0
容易被忽视的几个 Kotlin 细节, value class 执行效率竟然这么高
|
Java Spring
【Spring Boot + Kotlin 实战教程】Spring Data JPA 多表关联查询 映射到 Dto 的方法
【Spring Boot + Kotlin 实战教程】Spring Data JPA 多表关联查询 映射到 Dto 的方法 TechArticle package com.
1383 0
|
Web App开发 Java Apache
Spring Data JPA WITH Kotlin
@Entity 注解的实体类,必须要有一个默认的无参构造函数。 否则: 异常消息: No default constructor for entity: : com.
1080 0
|
6天前
|
Java 数据库 Android开发
【专栏】构建高效 Android 应用:探究 Kotlin 多线程优化策略
【4月更文挑战第27天】本文探讨了Kotlin在Android开发中的多线程优化,包括线程池、协程的使用,任务分解、避免阻塞操作以及资源管理。通过案例分析展示了网络请求、图像处理和数据库操作的优化实践。同时,文章指出并发编程的挑战,如性能评估、调试及兼容性问题,并强调了多线程优化对提升应用性能的重要性。开发者应持续学习和探索新的优化策略,以适应移动应用市场的竞争需求。
|
7天前
|
传感器 Android开发 开发者
构建高效Android应用:Kotlin的协程与Flow
【4月更文挑战第26天】随着移动应用开发的不断进步,开发者寻求更简洁高效的编码方式以应对复杂多变的业务需求。在众多技术方案中,Kotlin语言凭借其简洁性和强大的功能库逐渐成为Android开发的主流选择。特别是Kotlin的协程和Flow这两个特性,它们为处理异步任务和数据流提供了强大而灵活的工具。本文将深入探讨如何通过Kotlin协程和Flow来优化Android应用性能,实现更加流畅的用户体验,并展示在实际开发中的应用实例。
|
7天前
|
安全 数据处理 Android开发
构建高效Android应用:Kotlin协程的实践之路
【4月更文挑战第26天】 在面对现代Android开发时,性能优化和流畅的用户体验成为了开发者们追求的目标。Kotlin作为一种现代化的编程语言,通过其协程特性为Android应用带来了前所未有的并发处理能力。本文将深入探讨如何利用Kotlin协程提升Android应用的响应性和效率,同时保持代码的简洁性。我们将从协程的基础概念出发,逐步揭示如何在实际应用中运用这些强大的工具,以及它们如何改善应用架构和用户交互体验。