在 Kotlin 的 data class 中使用 MapStruct

語言: CN / TW / HK

一. 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 為淺拷貝。

當然,如果想實現深拷貝可以有很多種方式,比如使用序列化反序列化、一些開源庫(例如:github.com/enbandari/K…

本文接下來要介紹的不是深拷貝,但跟深拷貝會有一些關係,是 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 實現的開源專案:github.com/Pozo/mapstr…

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 是一個很不錯的選擇。