系統設計 - 對象轉換方案

文 | 少個分號 (轉載請註明出處)

公衆號:DDD 和微服務

知乎:少個分號

微信號:shaogefenhao

網站:shaogefenhao.com

在 Java 項目中,對象轉換是一個比較繁瑣的工作,常見的轉換場景有:

這些對象統稱爲 POJOs,也就是 Plan Object Java Object 的縮寫。

這一期的系統設計,聊聊如何對 POJOs 進行相互轉換,以及一些技巧和心得。

可選方案

在我編寫 Java 代碼的經歷中,就逃不了轉換這些對象。

我用過的方案有:

還有一些取巧的方法但是不推薦使用:

總的下來,如果想要找一個對象映射轉換的工具,MapStruct 是比較好的方案。原因如下:

Mapstruct 技巧

Mapstruct 使用比較簡單,只是需要配置一下構建工具(Maven、Gradle),參考官方文檔即可:https://mapstruct.org/documentation/stable/reference/html/#setup。不過在實踐上,這裏整理和收集了一些使用技巧對我們可能比較有幫助(官方文檔的內容非常多,使用起來並不方便)。

單個對象轉換

Mapstruct 的典型使用方法是定義一個接口,在編譯後會生成相關的實現代碼,然後在 Spring Boot 項目中引用使用即可。

例如:

@Mapper
public interface CarMapper {
    @Mapping(target = "manufacturer", source = "make")
    @Mapping(target = "seatCount", source = "numberOfSeats")
    CarDto carToCarDto(Car car);
    @Mapping(target = "fullName", source = "name")
    PersonDto personToPersonDto(Person person);
}

默認情況下,轉換會發生同名的屬性不需要設置 @Mapping 註解,會自動實現轉換邏輯。在生成的代碼中,默認通過 Setter 來實現。如果需要通過 Builder、構造方法來完成也可以進行配置,選擇不同的策略即可。

獲取生成的 CarMapper 實例常用有三種方式:

  1. 生成 Spring 支持的依賴注入。在 Mapper 類加上註解 @Mapper(componentModel = MappingConstants.ComponentModel.Spring) 即可實現 Spring 的依賴注入。

  2. 使用單例。在接口中增加 CarMapper INSTANCE = Mappers.getMapper(CarMapper.class) 即可。

  3. 使用工廠方法獲取示例對象。CarMapper mapper = Mappers.getMapper(CarMapper.class);

在項目中通常使用依賴注入的形式使用,這樣和 JPA 的 Repository、Mybatis 的 Mapper 風格統一,其實這不是一個好的實踐。原因在於,數據轉換不應該去調用數據庫、外部 API 等業務邏輯,如果通過單例引入,就可以在開發過程中避免此類操作。

不過基本的轉換方式不太能滿足我們的需求,例如自動嵌套轉換、列表轉換等。

列表轉換

最常用的需求是需要將列表中的對象循環轉換,這種場景在 Mapstruct 是自動的,在日常工作中使用非常高頻。

@Mapper
public interface CarMapper {
    // 可以直接使用 carsToCarDtos,在生成的代碼中會自動循環調用 carToCarDto
    List<CarDto> carsToCarDtos(List<Car> cars);
    CarDto carToCarDto(Car car);
}

除了 List 這種集合容器之外,Set 等常見 Collection 實現都支持類似操作,甚至支持將 Map 對象轉換爲 Bean 對象。其實可以被迭代的集合都可以使用這個特性,一些分頁對象也可以利用這個特性簡化開發。

需要注意的是,在單個轉換時儘量不要進行耗時操作否則會不小心引入 N+1 問題。

除了集合有這個特性之外,子對象也會被自動調用。如果有另外的一個對象的屬性爲類型爲 Car 的對象,那麼在轉換時也會自動調用 carToCarDto 方法。

自定義轉換方法

對於複雜的嵌套對象,如果當中一個子對象的轉換比較麻煩,無法使用註解映射轉換,可以在接口中實現一個自定義的方法。

@Mapper
public interface CarMapper {
    @Mapping(...)
    ...
    CarDto carToCarDto(Car car);
    default PersonDto personToPersonDto(Person person) {
        //自定義實現的方法也會被其它轉換方法調用
    }
}

命名轉換

嵌套的自動轉換是根據類型來定位需要的方法,在大多數場景下都能滿足需求。有一些場景,例如將時間轉換爲字符串,可能在不同的場景下有不同的格式化方法。

那麼可以給多個轉換方法定義不同的名稱,並在屬性映射時使用即可。

@Named("TIME_TO_DATE_STRING")
default String timeToDateString(LocalDateTime time) {
// 格式化爲日期字符串
}
@Named("TIME_TO_TIME_STRING")
default String timeToTimeString(LocalDateTime time) {
// 格式化爲時間字符串
}

例如,需要將對象的創建時間轉換爲創建日期字符串,以便輸出給前端。

@Mapping( target = "createdDate", source = "createdTime" , qualifiedByName = "TIME_TO_DATE_STRING")
    CarDto carToCarDto(Car car);

自動通用轉換

如果一些轉換邏輯被重複使用,我們也可以編寫一個類,通過 @Mapper uses 屬性注入進來,達到複用的作用。

在任意一個 Mapper 接口上使用註解:

@Mapper(uses = CommonMapperMethod.class)

然後在 CommonMapperMethod 中定義一些方法,這些方法就會被 Mapper 生成的代碼引用,比較常用的場景是將系統中的字典數據轉換爲實體。

例如,將對象中的幣種 ID,轉換爲完整的幣種對象。

public class CommonMapperMethod {
     @Named("TO_CURRENCCY_ENTITY")
     public Currency toCurrencyEntity(String currencyId) {
         return // 從幣種字典中獲取幣種對象
     }
}

這樣如果對象上有 currencyId 就可以比較方便的轉換爲 Currency 對象,而無需多次編寫。

@Mapping( target = "currency", source = "currencyId" , qualifiedByName = "TO_CURRENCCY_ENTITY")
    CarDto carToCarDto(Car car);

使用表達式轉換

有些場景下,需要對某些屬性執行額外的操作,但是設計多個字段。這樣使用命名轉換就不太方面。那麼還可以使用表達式轉換。

例如,使用 Java 表達式將人的姓名拼接到一起。

@Mapper
public interface UserMapper {
    @Mapping(target = "fullName",
    expression = "java(user.getfirstName() + user.getLastName())")
    UserResponse toUserResponse(User user);
}

在使用表達式的時候如果需要引入一些通用的工具類,可以用 @Mapper 的 imports 屬性引入一個包含靜態方法的工具類即可,這樣相關的工具類也會出現在生成的代碼 import 語句中。

使用表達式轉換可以實現更多有用的功能,但是維護性比較差,建議謹慎使用。

參考資料

[1] https://mapstruct.org/documentation/stable/reference/html/

[2] https://stackoverflow.com/questions/1432764/any-tool-for-java-object-to-object-mapping

[3] http://modelmapper.org/

[4] https://github.com/DozerMapper/dozer/

[5] https://github.com/mapstruct/mapstruct

爲保證內容準確專業,如發現內容錯誤,可以反饋給作者微信(shaogefenhao)領取紅包。如果對您有幫助,歡迎收藏、轉發、在看!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/nasj-w7GF3NUHC0gRo5b-g