MVC模式是目前主流項目的標準開發模式,這種模式下框架的分層結構清晰,主要分爲Controller,Service,Dao。分層的結構下,各層之間的數據傳輸要求就會存在差別,咱們不能用一個對象來貫穿3層,這樣不符合開發規範且不夠靈活。前端
咱們經常會遇到層級之間字段格式需求不一致的狀況,例如數據庫中某個字段是datetime
日期格式,這個時間戳在數據庫中的存儲值爲2020-11-06 23:59:59.999999
,可是傳遞給前端的時候要求接口返回yyyy-MM-dd
的格式,或者有些數據在數據庫中是逗號拼接的String類型,可是前端須要的是切割後的List類型等等。java
因此咱們提出了層級間的對象模型,就是咱們常見的VO,DTO,DO,PO等等。這種區分層級對象模型的方式雖然清晰化了咱們各層級間的對象傳遞,可是對象模型間的相互轉換和值拷貝確是讓人感受很麻煩,拷貝來拷貝去,來來回回,過程重複乏味,編寫此類映射代碼是一項繁瑣且容易出錯的任務。git
最簡單粗糙的拷貝方法就是不斷的new對象而後對象間的 setter 和 getter,這種方式應對字段屬性少的還能夠,若是屬性字段不少那麼大段的set,get的代碼就顯得很不雅美。所以須要藉助對象拷貝工具,目前市場上的也蠻多的像BeanCopy,Dozer等等,可是這些我感受都不夠好,今天我推薦一個實體映射工具就是 MapStruct。github
MapStruct的官網地址是 mapstruct.org/MapStruct,是一個快速安全的bean 映射代碼生成器,只須要經過簡單的註解就能夠實現對象間的屬性轉換,是一款 Apache LICENSE 2.0 受權的開源產品,Github的源碼地址是 https://github.com/mapstruct。數據庫
經過官網的三連問(What,Why,How)咱們能夠大概的瞭解到 MapStruct 的做用,它的優點以及它是如何實現的。express
從上面的三連問中咱們能夠獲得以下信息:安全
基於約定優於配置的方法 MapStruct 極大地簡化了 Java bean 類型之間的映射的實現,經過簡單的註解就能夠工做。生成的映射代碼使用普通的方法調用而不是反射,所以速度快,類型安全且易於理解。markdown
在編譯時生成 Bean 映射 與其餘映射框架相比,MapStruct 在編譯時生成 Bean 映射,這樣能夠確保高性能,並且開發人員能夠快速的獲得反饋和完全的錯誤檢查。app
一個註釋處理器 MapStruct 是一個註釋處理器,已插入 Java 編譯器,可用於命令行構建(Maven,Gradle等),也可用於您首選的IDE中(IDEA,Eclipse等)。框架
MapStruct 須要 Java 1.8或更高版本。對於Maven-based 的項目,在pom 文件中添加以下依賴便可
<!-- 指定版本-->
<properties> <org.mapstruct.version>1.4.1.Final</org.mapstruct.version> </properties> <!-- 添加依賴 --> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency> </dependencies> 複製代碼
基本的依賴引入後就能夠編寫代碼了,簡單的定義一個映射類,爲了與 Mybatis中的 mapper 接口區分,咱們能夠取名爲 xxObjectConverter
。
例如汽車對象的映射類名爲 CarObjectConverter
,咱們有兩個對象模型 DO 和 DTO,它們內部的屬性字段以下:
數據庫對應的持久化對象模型 CarDo
public class Car {
@ApiModelProperty(value = "主鍵id") private Long id; @ApiModelProperty(value = "製造商") private String manufacturers; @ApiModelProperty(value = "銷售渠道") private String saleChannel; @ApiModelProperty(value = "生產日期") private Date productionDate; ... } 複製代碼
層級間傳輸的對象模型 CarDto
public class CarDto {
@ApiModelProperty(value = "主鍵id") private Long id; @ApiModelProperty(value = "製造商") private String maker; @ApiModelProperty(value = "銷售渠道") private List<Integer> saleChannel; @ApiModelProperty(value = "生產日期") private Date productionDate; ... } 複製代碼
再編寫具體的 MapStruct 對象映射器
@Mapper
public interface CarObjectConverter{ CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class); @Mapping(target = "maker", source = "manufacturers") CarDto carToCarDto(Car car); } 複製代碼
對於字段名相同的能夠不用額外的指定映射規則,可是字段名不一樣的屬性則須要指出字段的映射規則,如上咱們持久層 DO 的製造商的字段名是manufacturers
而層級間傳輸的DTO模型中則是maker
,咱們就須要在映射方法上經過@Mapping
註解指出映射規則,我我的習慣是喜歡將target
寫在前面,source
寫在後面,這樣是與映射對象的位置保持一致,差別字段多的時候方便對比且不易混淆。
開發過程當中還會常常遇到一些日期格式的轉換,就如開篇時說的那種,這時咱們也能夠指定日期的映射規則
@Mapper
public interface CarObjectConverter{ CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class); @Mapping(target = "maker", source = "manufacturers") @Mapping(target = "productionDate", dateFormat = "yyyy-MM-dd", source = "productionDate") CarDto carToCarDto(Car car); } 複製代碼
這些都仍是一些簡單的字段的映射,但有時候咱們兩個對象模型間的字段類型不一致,如上汽車的銷售渠道字段saleChannel
,這個在數據庫中是字符串逗號拼接的值1,2,3
,而咱們傳遞出去的須要是 List 的 Integer 類型,這種複雜的如何映射呢?
也是有方法的,咱們先編寫一個將字符串逗號分隔而後轉成 List 的工具方法,以下
public class CollectionUtils {
public static List<Integer> list2String(String str) { if (StringUtils.isNoneBlank(str)) { return Arrays.asList(str.split(",")).stream().map(s -> Integer.valueOf(s.trim())).collect(Collectors.toList()); } return null; } } 複製代碼
而後在映射Mapping中使用表達式便可
@Mapper
public interface CarObjectConverter { CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class); @Mapping(target = "maker", source = "manufacturers") @Mapping(target = "productionDate", dateFormat = "yyyy-MM-dd", source = "productionDate") @Mapping(target = "saleChannel", expression = "java(com.jiajian.demo.utils.CollectionUtils.list2String(car.getSaleChannel()))") CarDto carToCarDto(Car car); } 複製代碼
這樣就完成了全部字段的映射工做,咱們在須要對象模型轉換的地方按照以下方式調用便可
CarDto carDto = CarObjectConverter.INSTANCE.carToCarDto(car);
複製代碼
這種是單體對象之間的 Copy 不少時候咱們須要 List 對象模型間的轉換,只須要再寫一個方法carToCarDtos
便可
@Mapper
public interface CarObjectConverter{ CarObjectConverter INSTANCE = Mappers.getMapper(CarObjectConverter.class); @Mapping(target = "maker", source = "manufacturers") @Mapping(target = "productionDate", dateFormat = "yyyy-MM-dd", source = "productionDate") @Mapping(target ="saleChannel", expression = "java(com.jiajian.demo.utils.CollectionUtils.list2String(car.getSaleChannel()))") CarDto carToCarDto(Car car); List<CarDto> carToCarDtos(List<Car> carList); } 複製代碼
會不會好奇這是怎麼實現的,咱們只是建立了一個接口而後在接口方法上加一個註解並在註解裏面指定字段的映射規則就能夠實現對象屬性間的拷貝,這是怎麼作到的呢?
咱們這裏經過 MapStruct 建立的只是一個接口,要實現具體的功能接口必有實現。
MapStruct 會在咱們代碼編譯的時候爲咱們建立一個實現類,而這個實現類裏面經過字段的setter, getter方法來實現字段的賦值,從而實現對象的映射。
「這裏須要注意一點:若是你修改了任一映射對象,記得須要先執行
」mvn clean
再啓動項目,不然調試的時候會報錯。
MapStrut 的功能遠不至於上面介紹的這些,我只是挑出幾個經常使用的語法進行示例講解,若是讀者感興趣想深刻的瞭解更多能夠參考官方的參考文檔,Reference Guide。
碰見 MapStruct 後我就開始在項目中拋棄掉了原來的那些 BeanCopyUtils 的工具,相對而言 MapStruct 確實更簡潔且易使用並且定製功能也很強。
從編譯文件能夠看出 MapStruct 是經過setter,getter來實現屬性值的拷貝,而後這種方式不是最簡單又最安全高效的嗎?只是 MapStruct 更好的幫助咱們實現了,避免了項目中冗餘的重複代碼,大道至簡。
本文使用 mdnice 排版