美文网首页Kotlin 实战
在 Kotlin 的 data class 中使用 MapStr

在 Kotlin 的 data class 中使用 MapStr

作者: fengzhizi715 | 来源:发表于2020-08-28 15:24 被阅读0次
    pexels-nguyễn-xuân-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 是一个很不错的选择。

    相关文章

      网友评论

        本文标题:在 Kotlin 的 data class 中使用 MapStr

        本文链接:https://www.haomeiwen.com/subject/xmtajktx.html