系統設計 - 對象轉換方案
文 | 少個分號 (轉載請註明出處)
公衆號:DDD 和微服務
知乎:少個分號
微信號:shaogefenhao
網站:shaogefenhao.com
在 Java 項目中,對象轉換是一個比較繁瑣的工作,常見的轉換場景有:
-
頁面請求對象(Use Case Command)轉領域模型實體(Entity)。
-
領域模型轉換爲數據庫操作對象(PO)。
-
領域模塊轉返回給前端的結果對象(Use Case Response)。
-
……
這些對象統稱爲 POJOs,也就是 Plan Object Java Object 的縮寫。
這一期的系統設計,聊聊如何對 POJOs 進行相互轉換,以及一些技巧和心得。
可選方案
在我編寫 Java 代碼的經歷中,就逃不了轉換這些對象。
我用過的方案有:
-
手動轉換,使用 Setter 或者 Getter,有些時候可以使用 Lombok 簡化它們。
-
Commons-BeanUtils:這是 Apache 的一個通用包,在很多框架中被大量使用。這個包提供了一個 BeanUtils 類,它包裝了反射 API,提供了一些方便的對象轉換方法。正是因爲反射的原因,它不需要任何構建工具的配置,缺點是性能比較差,且功能比較簡單。
-
Dozer:Dozer 是一個早期的對象轉換框架,提供的功能比 Commons-BeanUtils 多,但是已經停止維護和更新了。
-
ModelMapper:ModelMapper 也是通過反射完成的,並通過遞歸相關機制,實現嵌套對象的映射,映射過程比較智能。
-
MapStruct:MapStruct 在功能性上和 ModelMapper 類似,特別的地方在於它不是運行時動態完成轉換,而是在編譯期通過代碼生成的方式實現的。
還有一些取巧的方法但是不推薦使用:
-
直接使用 Spring 框架中的 BeanUtils 類,該類和 Commons-BeanUtils 功能和實現原理類似。
-
使用 Jackson 的 ObjectMapper,通過序列化和反序列化實現轉換邏輯。
總的下來,如果想要找一個對象映射轉換的工具,MapStruct 是比較好的方案。原因如下:
-
編譯期生成代碼的方式實現,性能更好,容易 Debug,類型安全。
-
映射轉換的過程非常靈活。
-
自動遞歸嵌套轉換自動子對象。
-
可配置和干預轉換過程,實在不行也可以自定義轉換過程。
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 實例常用有三種方式:
-
生成 Spring 支持的依賴注入。在 Mapper 類加上註解 @Mapper(componentModel = MappingConstants.ComponentModel.Spring) 即可實現 Spring 的依賴注入。
-
使用單例。在接口中增加 CarMapper INSTANCE = Mappers.getMapper(CarMapper.class) 即可。
-
使用工廠方法獲取示例對象。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