Spring Boot 2 實戰(zhàn):集成 MapStruct 類型轉(zhuǎn)換神器

1. 痛點

一種框架的出現(xiàn)都要解決個痛點,我想下面這這種不方便的操作經(jīng)常有人寫吧。
假如Car類是數(shù)據(jù)庫映射類:
??

 package cn.felord.mapstruct.entity;
 
 import lombok.Data;
 
 /**
  * Car
  *
  * @author Felordcn
  * @since 13:35 2019/10/12
  **/
 @Data
 public class Car {
     private String make;
     private int numberOfSeats;
     private CarType type;
 
 }

CarType 類:

 package cn.felord.mapstruct.entity;
 
 import lombok.Data;
 
 /**
  * CarType
  *
  * @author Felordcn
  * @since 13:36 2019/10/12
  **/
 @Data
 public class CarType {
     private String type;
 }

??
CarDTO是DTO類:

 package cn.felord.mapstruct.entity;
 
 import lombok.Data;
 
 /**
  * CarDTO
  *
  * @author Felordcn
  * @since 13:37 2019/10/12
  **/
 @Data
 public class CarDTO {
     private String make;
     private int seatCount;
     private String type;
 } 

??
我們從數(shù)據(jù)庫查詢Car 然后需要轉(zhuǎn)換為CarDTO,通常我們會這么寫一個方法進行轉(zhuǎn)換:

     public CarDTO carToCarDTO(Car car) {
         CarDTO carDTO = new CarDTO();
         
         carDTO.setMake(car.getMake());
         carDTO.setSeatCount(car.getNumberOfSeats());
         carDTO.setType(car.getCarType().getType());
         // 有可能更長 
         return carDTO;
     } 

??這種寫法非常繁瑣無味酪呻,而且沒有技術(shù)含量眶掌。甚至中間還牽涉了很多類型轉(zhuǎn)換,嵌套之類的繁瑣操作绒瘦,而我們想要的只是建立它們之間的映射關(guān)系而已称簿。有沒有一種通用的映射工具來幫我們搞定這一切。當(dāng)然有而且還不少惰帽。有人說apache的BeanUtil.copyProperties可以實現(xiàn)憨降,但是性能差而且容易出異常,很多規(guī)范嚴(yán)禁使用這種途徑该酗。以下是對幾種對象映射框架的對比授药,大多數(shù)情況下 MapStruct 性能最高士嚎。原理類似于lombokMapStruct都是在編譯期進行實現(xiàn)悔叽,而且基于Getter莱衩、Setter,沒有使用反射所以一般不存在運行時性能問題。
??

diff.png

今天就搞一搞MapStruct, 并跟Spring Boot 2.x 集成以下娇澎。 無論是idea 還是eclipse 都建議安裝 MapStruct Plugin 插件,當(dāng)然不安裝也是可以的笨蚁。

2. Spring Boot 2.1.9 集成 MapStruct

在 Spring Boot 的 pom.xml 下引入 MapStruct 的 maven 依賴坐標(biāo):

         <dependencies>
            <dependency>
                 <groupId>org.mapstruct</groupId>
                 <artifactId>mapstruct</artifactId>
                 <version>${mapstruct.version}</version>
                 <scope>compile</scope>
             </dependency>
             <dependency>
                 <groupId>org.mapstruct</groupId>
                 <artifactId>mapstruct-processor</artifactId>
                 <version>${mapstruct.version}</version>
                 <scope>compile</scope>
             </dependency>
                   <!-- other dependencies -->
         </dependencies>

??

3. 使用MapStruct

我們把開始的痛點解決一下√俗看看 MapStruct 如何降低你的編程成本括细。

3.1 編寫轉(zhuǎn)換源到目標(biāo)的映射

編寫CarCarDTO的映射:

 package cn.felord.mapstruct.mapping;
 
 import cn.felord.mapstruct.entity.Car;
 import cn.felord.mapstruct.entity.CarDTO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 
 /**
  * CarMapping
  *
  * @author Felordcn
  * @since 14 :02 2019/10/12
  */
 @Mapper
 public interface CarMapping {
     /**
      * 用來調(diào)用實例 實際開發(fā)中可使用注入Spring  不寫
      */
     CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);
 
 
     /**
      *  源類型 目標(biāo)類型 成員變量相同類型 相同變量名 不用寫{@link org.mapstruct.Mapping}來映射
      *
      * @param car the car
      * @return the car dto
      */
     @Mapping(target = "type", source = "carType.type")
     @Mapping(target = "seatCount", source = "numberOfSeats")
     CarDTO carToCarDTO(Car car);
 
 }

3.2 MapStruct映射方法講解

上面短短幾行代碼就可以了十分簡單!解釋一下操作步驟:

首先聲明一個映射接口用@org.mapstruct.Mapper (不要跟mybatis注解混淆)標(biāo)記戚啥,說明這是一個實體類型轉(zhuǎn)換接口奋单。這里我們聲明了一個 CAR_MAPPING 來方便我們調(diào)用,CarDTO toCarDTO(Car car)是不是很熟悉猫十, 像mybatis一樣抽象出我們的轉(zhuǎn)換方法览濒。@org.mapstruct.Mapping注解用來聲明成員屬性的映射。該注解有兩個重要的屬性:

  • source 代表轉(zhuǎn)換的源拖云。這里就是Car 匾七。
  • target 代表轉(zhuǎn)換的目標(biāo)。這里是CarDTO 江兢。

這里以成員變量的參數(shù)名為依據(jù)昨忆,如果有嵌套比如 Car 里面有個 CarType 類型的成員變量 carType,其 type 屬性 來映射 CarDTO 中的 type 字符串杉允,我們使用 type.type 來獲取屬性值邑贴。如果有多層以此類推。MapStruct 最終調(diào)用的是 settergetter 方法叔磷,而非反射拢驾。這也是其性能比較好的原因之一。numberOfSeats 映射到 seatCount 就比較好理解了改基。我們是不是忘記了一個屬性 make繁疤,因為他們的位置且名稱完全一致,所以可以省略秕狰。而且對于包裝類是自動拆箱封箱操作的稠腊,并且是線程安全的。MapStruct不單單有這些功能鸣哀,還有其他一些復(fù)雜的功能:

設(shè)置轉(zhuǎn)換默認值和常量架忌。當(dāng)目標(biāo)值是 null 時我們可以設(shè)置其默認值,注意這些都是基本類型以及對應(yīng)都 boxing 類型我衬,如下

 @Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined")

需要注意的是常量不能對源進行引用(不能指定 source 屬性)叹放,下面是正確的操作:

 @Mapping(target = "stringConstant", constant = "Constant Value")

3.2 Mapper 編譯

當(dāng)你的應(yīng)用編譯后饰恕。你會在編譯后的目錄比如 maven是 target\generated-sources\annotations 下的子目錄發(fā)現(xiàn)生成了一個實現(xiàn)類 比如 我們上面的CarMapping 會生成CarMappingImpl 如下:

 package cn.felord.mapstruct.mapping;
  
 import cn.felord.mapstruct.entity.Car;
 import cn.felord.mapstruct.entity.CarDTO;
 import org.mapstruct.Mapper;
 import org.mapstruct.Mapping;
 import org.mapstruct.factory.Mappers;
 import javax.annotation.Generated;
 import org.springframework.stereotype.Component;
 
 @Generated(
     value = "org.mapstruct.ap.MappingProcessor",
     date = "2019-10-12T15:05:36+0800",
     comments = "version: 1.3.0.Final, compiler: javac, environment: Java 1.8.0_222 (Amazon.com Inc.)"
 )
 @Component
 public class CarMappingImpl implements CarMapping {
 
     @Override
     public CarDTO carToCarDTO(Car car) {
         if ( car == null ) {
             return null;
         }
 
         CarDTO carDTO = new CarDTO();
 
         carDTO.setType( carCarTypeType( car ) );
         carDTO.setSeatCount( car.getNumberOfSeats() );
         carDTO.setMake( car.getMake() );
 
         return carDTO;
     }
 
     private String carCarTypeType(Car car) {
         if ( car == null ) {
             return null;
         }
         CarType carType = car.getCarType();
         if ( carType == null ) {
             return null;
         }
         String type = carType.getType();
         if ( type == null ) {
             return null;
         }
         return type;
     }
 }

4. MapStruct 進階操作

下面介紹幾種 MapStruct 的進階操作:

4.1 格式化操作

格式化也是我們經(jīng)常使用的操作,比如數(shù)字格式化井仰,日期格式化埋嵌。
這是處理數(shù)字格式化的操作,遵循java.text.DecimalFormat的規(guī)范:

     @Mapping(source = "price", numberFormat = "$#.00")

下面展示了將一個日期集合映射到日期字符串集合的格式化操作上:

 @IterableMapping(dateFormat = "dd.MM.yyyy")
 List<String> stringListToDateList(List<Date> dates);

4.2 使用 java 表達式

下面演示如何使用LocalDateTime 作為當(dāng)前的時間值注入 addTime 屬性中俱恶。

首先在@org.mapstruct.Mapperimports 屬性中導(dǎo)入 LocalDateTime雹嗦,該屬性是數(shù)組意味著你可以根據(jù)需要導(dǎo)入更多的處理類:

 @Mapper(imports = {LocalDateTime.class})

接下來只需要在對應(yīng)的方法上添加注解@org.mapstruct.Mapping ,其屬性expression 接收一個 java() 包括的表達式:

  • 無入?yún)姹荆?/li>
 @Mapping(target = "addTime", expression = "java(LocalDateTime.now())")
  • 攜帶入?yún)⒌陌姹? 我們將 Car 的出廠日期字符串manufactureDateStr 注入到 CarDTOLocalDateTime 類型屬性addTime 中去:
 @Mapping(target = "addTime", expression = "java(LocalDateTime.parse(car.manufactureDateStr))")
 CarDTO carToCarDTO(Car car);

4.3 MapStruct 轉(zhuǎn)換 Mapper 注入Spring IoC 容器

如果使用要把Mapper 注入Spring IoC 容器我們只需要這么聲明速那,不用再構(gòu)建一個單例俐银,就可以像其他 spring bean一樣對CarMapping 進行引用了:

  package cn.felord.mapstruct.mapping;
  
  import cn.felord.mapstruct.entity.Car;
  import cn.felord.mapstruct.entity.CarDTO;
  import org.mapstruct.Mapper;
  import org.mapstruct.Mapping;
  import org.mapstruct.factory.Mappers;
 
 /**
  * CarMapping 注入spring 寫法
  *
  * @author Felordcn
  * @since 14 :02 2019/10/12
  */
 @Mapper(componentModel = "spring")
 public interface CarMapping {
     /**
      * 用來調(diào)用實例 實際開發(fā)中可使用注入Spring  不寫
      */
 //    CarMapping CAR_MAPPING = Mappers.getMapper(CarMapping.class);
 
 
     /**
      * 源類型 目標(biāo)類型 成員變量相同類型 相同變量名 不用寫{@link Mapping}來映射
      *
      * @param car the car
      * @return the car dto
      */
     @Mapping(target = "type", source = "carType.type")
     @Mapping(target = "seatCount", source = "numberOfSeats")
     CarDTO carToCarDTO(Car car);
 
 }

??

5.總結(jié)

其實MapStruct 還有很多的功能尿背。但是從可讀性來說端仰,我建議使用以上幾種容易理解的功能即可。如果你感興趣可以去mapstruct.org進一步學(xué)習(xí)田藐。配合lombok和我介紹的jsr303荔烧,讓你更加專注于業(yè)務(wù),而且代碼更加清晰汽久。

關(guān)注公眾號:碼農(nóng)小胖哥鹤竭,獲取更多資訊

個人博客:https://felord.cn

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市景醇,隨后出現(xiàn)的幾起案子臀稚,更是在濱河造成了極大的恐慌,老刑警劉巖三痰,帶你破解...
    沈念sama閱讀 219,039評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件吧寺,死亡現(xiàn)場離奇詭異,居然都是意外死亡散劫,警方通過查閱死者的電腦和手機稚机,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,426評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來获搏,“玉大人赖条,你說我怎么就攤上這事〕N酰” “怎么了纬乍?”我有些...
    開封第一講書人閱讀 165,417評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長裸卫。 經(jīng)常有香客問我蕾额,道長,這世上最難降的妖魔是什么彼城? 我笑而不...
    開封第一講書人閱讀 58,868評論 1 295
  • 正文 為了忘掉前任诅蝶,我火速辦了婚禮退个,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘调炬。我一直安慰自己语盈,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,892評論 6 392
  • 文/花漫 我一把揭開白布缰泡。 她就那樣靜靜地躺著刀荒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪棘钞。 梳的紋絲不亂的頭發(fā)上缠借,一...
    開封第一講書人閱讀 51,692評論 1 305
  • 那天,我揣著相機與錄音宜猜,去河邊找鬼泼返。 笑死,一個胖子當(dāng)著我的面吹牛姨拥,可吹牛的內(nèi)容都是我干的绅喉。 我是一名探鬼主播,決...
    沈念sama閱讀 40,416評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼叫乌,長吁一口氣:“原來是場噩夢啊……” “哼柴罐!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起憨奸,我...
    開封第一講書人閱讀 39,326評論 0 276
  • 序言:老撾萬榮一對情侶失蹤革屠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后排宰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體似芝,經(jīng)...
    沈念sama閱讀 45,782評論 1 316
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,957評論 3 337
  • 正文 我和宋清朗相戀三年额各,在試婚紗的時候發(fā)現(xiàn)自己被綠了国觉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,102評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡虾啦,死狀恐怖麻诀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情傲醉,我是刑警寧澤蝇闭,帶...
    沈念sama閱讀 35,790評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站硬毕,受9級特大地震影響呻引,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜吐咳,卻給世界環(huán)境...
    茶點故事閱讀 41,442評論 3 331
  • 文/蒙蒙 一逻悠、第九天 我趴在偏房一處隱蔽的房頂上張望元践。 院中可真熱鬧,春花似錦童谒、人聲如沸单旁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,996評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽象浑。三九已至,卻和暖如春琅豆,著一層夾襖步出監(jiān)牢的瞬間愉豺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,113評論 1 272
  • 我被黑心中介騙來泰國打工茫因, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蚪拦,地道東北人。 一個月前我還...
    沈念sama閱讀 48,332評論 3 373
  • 正文 我出身青樓节腐,卻偏偏與公主長得像外盯,于是被迫代替她去往敵國和親摘盆。 傳聞我的和親對象是個殘疾皇子翼雀,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,044評論 2 355

推薦閱讀更多精彩內(nèi)容