對(duì)象轉(zhuǎn)換工具 MapStruct 介紹

來(lái)源:稀土掘金 作者:mghio
鏈接:https://juejin.cn/post/6994233847076356133

前言

在我們?nèi)粘i_(kāi)發(fā)的分層結(jié)構(gòu)的應(yīng)用程序中忽刽,為了各層之間互相解耦驾荣,一般都會(huì)定義不同的對(duì)象用來(lái)在不同層之間傳遞數(shù)據(jù),因此瘸爽,就有了各種 XXXDTOXXXVO铅忿、XXXBO 等基于數(shù)據(jù)庫(kù)對(duì)象派生出來(lái)的對(duì)象剪决,當(dāng)在不同層之間傳輸數(shù)據(jù)時(shí),不可避免地經(jīng)常需要將這些對(duì)象進(jìn)行相互轉(zhuǎn)換檀训。

此時(shí)一般處理兩種處理方式:① 直接使用 SetterGetter 方法轉(zhuǎn)換柑潦、② 使用一些工具類(lèi)進(jìn)行轉(zhuǎn)換(e.g. BeanUtil.copyProperties)。第一種方式如果對(duì)象屬性比較多時(shí)峻凫,需要寫(xiě)很多的 Getter/Setter 代碼渗鬼。第二種方式看起來(lái)雖然比第一種方式要簡(jiǎn)單很多,但是因?yàn)槠涫褂昧朔瓷溆恚阅懿惶闷┨ィ以谑褂弥幸灿泻芏嘞葳宀钆!6裉煲榻B的主角 MapStruct 在不影響性能的情況下,同時(shí)解決了這兩種方式存在的缺點(diǎn)堰乔。

MapStruct 是什么

MapStruct 是一個(gè)代碼生成器偏化,它基于約定優(yōu)于配置方法極大地簡(jiǎn)化了 Java bean 類(lèi)型之間映射的實(shí)現(xiàn)。自動(dòng)生成的映射轉(zhuǎn)換代碼只使用簡(jiǎn)單的方法調(diào)用镐侯,因此速度快夹孔、類(lèi)型安全而且易于理解閱讀,源碼倉(cāng)庫(kù) Github 地址 MapStruct析孽〈钌耍總的來(lái)說(shuō),有如下三個(gè)特點(diǎn):

  1. 基于注解
  2. 在編譯期自動(dòng)生成映射轉(zhuǎn)換代碼
  3. 類(lèi)型安全袜瞬、高性能怜俐、無(wú)依賴性

MapStruct 使用步驟

MapStruct 的使用比較簡(jiǎn)單,只需如下三步即可邓尤。

① 引入依賴(這里以 Gradle 方式為例)

dependencies {
    implementation 'org.mapstruct:mapstruct:1.4.2.Final'
    annotationProcessor 'org.mapstruct:mapstruct-processor:1.4.2.Final'
}

② 創(chuàng)建相關(guān)轉(zhuǎn)換對(duì)象

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Doctor {
  private Integer id;
  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class DoctorDTO {
  private Integer id;
  private String name;
}

③ 創(chuàng)建轉(zhuǎn)換器類(lèi)(Mapper)

需要注意的是拍鲤,轉(zhuǎn)換器不一定都要使用 Mapper 作為結(jié)尾袄秩,只是官方示例推薦以 XXXMapper 格式命名轉(zhuǎn)換器名稱(chēng)俱箱,這里舉例的是最簡(jiǎn)單的映射情況(字段名稱(chēng)和類(lèi)型都完全匹配)铛碑,只需要在轉(zhuǎn)換器類(lèi)上添加 @Mapper 注解即可晚凿,轉(zhuǎn)換器代碼如下所示:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface DoctorMapper {
  DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
  DoctorDTO toDTO(Doctor doctor);
}

通過(guò)下面這個(gè)簡(jiǎn)單的測(cè)試來(lái)校驗(yàn)轉(zhuǎn)換結(jié)果是否正確,測(cè)試代碼如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
public class DoctorTest {

  @Test
  public void testToDTO() {
    Integer doctorId = 9527;
    String doctorName = "mghio";

    Doctor doctor = new Doctor();
    doctor.setId(doctorId);
    doctor.setName(doctorName);

    DoctorDTO doctorDTO = DoctorMapper.INSTANCE.toDTO(doctor);

    assertEquals(doctorId, doctorDTO.getId());
    assertEquals(doctorName, doctorDTO.getName());
  }
}

測(cè)試結(jié)果正常通過(guò)厢蒜,說(shuō)明使用 DoctorMapper 轉(zhuǎn)換器達(dá)到我們的預(yù)期結(jié)果葛菇。

測(cè)試結(jié)果

MapStruct 實(shí)現(xiàn)淺析

在以上示例中壶辜,使用 MapStruct 通過(guò)簡(jiǎn)單的三步就實(shí)現(xiàn)了 DoctorDoctorDTO 的轉(zhuǎn)換痹扇,那么铛漓,MapStruct 是如何做到的呢?其實(shí)通過(guò)我們定義的轉(zhuǎn)換器可以發(fā)現(xiàn)鲫构,轉(zhuǎn)換器是接口類(lèi)型的浓恶,而我們知道在 Java 中,接口是無(wú)法提供功能的结笨,只是定義規(guī)范包晰,具體干活的還是它的實(shí)現(xiàn)類(lèi)。
因此我們可以大膽猜想炕吸,MapStruct 肯定給我們定義的轉(zhuǎn)換器接口(DoctorMapper)生成了實(shí)現(xiàn)類(lèi)伐憾,而通過(guò) Mappers.getMapper(DoctorMapper.class) 獲取到的轉(zhuǎn)換器實(shí)際上是獲取到了轉(zhuǎn)化器接口的實(shí)現(xiàn)類(lèi)。下面通過(guò)在測(cè)試類(lèi)中 debug 來(lái)驗(yàn)證一下:

debug驗(yàn)證

通過(guò) debug 可以看出算途,DoctorMapper.INSTANCE 獲取到的是接口的實(shí)現(xiàn)類(lèi) DoctorMapperImpl塞耕。這個(gè)轉(zhuǎn)換器接口實(shí)現(xiàn)類(lèi)是在編譯期自動(dòng)生成的蚀腿,Gradle 項(xiàng)目是在build/generated/sources/anotationProcessor/Java 下(Maven 項(xiàng)目在 target/generated-sources/annotations 目錄下)嘴瓤,生成以上示例轉(zhuǎn)換器接口的實(shí)現(xiàn)類(lèi)源碼如下:

反編譯

可以發(fā)現(xiàn)扫外,自動(dòng)生成的代碼和我們平時(shí)手寫(xiě)的差不多,簡(jiǎn)單易懂廓脆,代碼完全在編譯期間生成筛谚,沒(méi)有運(yùn)行時(shí)依賴。和使用反射的實(shí)現(xiàn)方式相比還有一個(gè)有點(diǎn)就是停忿,出錯(cuò)時(shí)很容易去 debug 實(shí)現(xiàn)源碼來(lái)定位驾讲,而反射相對(duì)來(lái)說(shuō)定位問(wèn)題就要困難得多了。

常見(jiàn)使用場(chǎng)景介紹

① 對(duì)象屬性名稱(chēng)和類(lèi)型完全相同

從上文的示例可以看出席赂,當(dāng)屬性名稱(chēng)和類(lèi)型完全一致時(shí)吮铭,我們只需要定義一個(gè)轉(zhuǎn)換器接口并添加 @Mapper 注解即可,然后 MapStruct 會(huì)自動(dòng)生成實(shí)現(xiàn)類(lèi)完成轉(zhuǎn)換颅停。示例代碼如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {
  private Integer id;
  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;
  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  Target toTarget(Source source);
}

② 對(duì)象屬性類(lèi)型相同但是名稱(chēng)不同

當(dāng)對(duì)象屬性類(lèi)型相同但是屬性名稱(chēng)不一樣時(shí)谓晌,通過(guò) @Mapping 注解來(lái)手動(dòng)指定轉(zhuǎn)換。示例代碼如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;
  private String sourceName;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;
  private String targetName;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);
}

③ 在 Mapper 中使用自定義轉(zhuǎn)換方法

有時(shí)候癞揉,對(duì)于某些類(lèi)型(比如:一個(gè)類(lèi)的屬性是自定義的類(lèi))纸肉,無(wú)法以自動(dòng)生成代碼的形式進(jìn)行處理。此時(shí)我們需要自定義類(lèi)型轉(zhuǎn)換的方法喊熟,在 JDK 7 之前的版本柏肪,就需要使用抽象類(lèi)來(lái)定義轉(zhuǎn)換 Mapper 了,在 JDK 8 以上的版本可以使用接口的默認(rèn)方法*來(lái)自定義類(lèi)型轉(zhuǎn)換的方法芥牌。示例代碼如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Source {

  private Integer id;
  private String sourceName;
  private InnerSource innerSource;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class InnerSource {

  private Integer deleted;
  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Target {

  private Integer id;
  private String targetName;
  private InnerTarget innerTarget;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class InnerTarget {

  private Boolean isDeleted;
  private String name;
}
/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);

  default InnerTarget innerTarget2InnerSource(InnerSource innerSource) {
    if (innerSource == null) {
      return null;
    }
    InnerTarget innerTarget = new InnerTarget();
    innerTarget.setIsDeleted(innerSource.getDeleted() == 1);
    innerTarget.setName(innerSource.getName());
    return innerTarget;
  }
}

④ 多個(gè)對(duì)象轉(zhuǎn)換成一個(gè)對(duì)象返回

在一些實(shí)際業(yè)務(wù)編碼的過(guò)程中烦味,不可避免地需要將多個(gè)對(duì)象轉(zhuǎn)化為一個(gè)對(duì)象的場(chǎng)景,MapStruct 也能很好的支持壁拉,對(duì)于這種最終返回信息來(lái)源于多個(gè)類(lèi)拐叉,我們可以通過(guò)配置來(lái)實(shí)現(xiàn)多對(duì)一的轉(zhuǎn)換。示例代碼如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Data
public class Doctor {

  private Integer id;
  private String name;
}
/**
 * @author mghio
 * @since 2021-08-09
 */
@Data
public class Address {

  private String street;
  private Integer zipCode;
}
/**
 * @author mghio
 * @since 2021-08-09
 */
@Mapper
public interface AddressMapper {

  AddressMapper INSTANCE = Mappers.getMapper(AddressMapper.class);

  @Mapping(source = "doctor.id", target = "personId")
  @Mapping(source = "address.street", target = "streetDesc")
  DeliveryAddressDTO doctorAndAddress2DeliveryAddressDTO(Doctor doctor, Address address);
}

從這個(gè)示例中的轉(zhuǎn)換器(AddressMapper)可以看出扇商,當(dāng)屬性名稱(chēng)和類(lèi)型完全匹配時(shí)同樣可以自動(dòng)轉(zhuǎn)換凤瘦,但是當(dāng)來(lái)源對(duì)象有多個(gè)屬性名稱(chēng)及類(lèi)型完全和目標(biāo)對(duì)象相同時(shí),還是需要手動(dòng)配置指定的案铺,因?yàn)榇藭r(shí) MapStruct 也無(wú)法準(zhǔn)確判斷應(yīng)該使用哪個(gè)屬性轉(zhuǎn)換蔬芥。

獲取轉(zhuǎn)換器(Mapper)的幾種方式

獲取轉(zhuǎn)換器的方式根據(jù) @Mapper 注解的 componentModel 屬性不同而不同,支持以下四種不同的取值:

  1. default 默認(rèn)方式控汉,默認(rèn)方式笔诵,使用工廠方式(Mappers.getMapper(Class) )來(lái)獲取
  2. cdi 此時(shí)生成的映射器是一個(gè)應(yīng)用程序范圍的 CDI bean,使用 @Inject 注解來(lái)獲取
  3. spring Spring 的方式姑子,可以通過(guò) @Autowired 注解來(lái)獲取乎婿,在 Spring 框架中推薦使用此方式
  4. jsr330 生成的映射器用 @javax.inject.Named@Singleton 注解,通過(guò) @Inject 來(lái)獲取

① 通過(guò)工廠方式獲取

上文的示例中都是通過(guò)工廠方式獲取的街佑,也就是使用 MapStruct 提供的 Mappers.getMapper(Class<T> clazz) 方法來(lái)獲取指定類(lèi)型的 Mapper谢翎。然后在調(diào)用的時(shí)候就不需要反復(fù)創(chuàng)建對(duì)象了捍靠,方法的最終實(shí)現(xiàn)是通過(guò)我們定義接口的類(lèi)加載器加載 MapStruct 生成的實(shí)現(xiàn)類(lèi)(類(lèi)名稱(chēng)規(guī)則為:接口名稱(chēng) + Impl),然后調(diào)用該類(lèi)的無(wú)參構(gòu)造器創(chuàng)建對(duì)象森逮。核心源碼如下所示:

② 使用依賴注入方式獲取

對(duì)于依賴注入(dependency injection)榨婆,使用 Spring 框架開(kāi)發(fā)的朋友們應(yīng)該很熟悉了,工作中經(jīng)常使用褒侧。MapStruct 也支持依賴注入的使用方式良风,并且官方也推薦使用依賴注入的方式獲取。使用 Spring 依賴注入的方式只需要指定 @Mapper 注解的 componentModel = "spring" 即可闷供,示例代碼如下:

/**
 * @author mghio
 * @since 2021-08-08
 */
@Mapper(componentModel = "spring")
public interface SourceMapper {

  SourceMapper INSTANCE = Mappers.getMapper(SourceMapper.class);

  @Mapping(source = "sourceName", target = "targetName")
  Target toTarget(Source source);
}  

我們可以使用 @Autowired 獲取的原因是 SourceMapper 接口的實(shí)現(xiàn)類(lèi)已經(jīng)被注冊(cè)為容器中一個(gè) Bean 了烟央,通過(guò)如下生成的接口實(shí)現(xiàn)類(lèi)的代碼也可以看到,在類(lèi)上自動(dòng)加上了 @Component 注解歪脏。


最后還有兩個(gè)注意事項(xiàng):
① 當(dāng)兩個(gè)轉(zhuǎn)換對(duì)象的屬性不一致時(shí)(比如 DoctorDTO 中不存在 Doctor 對(duì)象中的某個(gè)字段)吊档,編譯時(shí)會(huì)出現(xiàn)警告提示⊥倥矗可以在@Mapping 注解中配置 ignore = true怠硼,或者當(dāng)不一致字段比較多時(shí),可以直接設(shè)置 @Mapper 注解的 unmappedTargetPolicy 屬性或unmappedSourcePolicy 屬性設(shè)置為 ReportingPolicy.IGNORE移怯。
② 如果你項(xiàng)目中也使用了 Lombok香璃,需要注意一下 Lombok 的版本至少是 1.18.10 或者以上才行,否則會(huì)出現(xiàn)編譯失敗的情況舟误。剛開(kāi)始用的時(shí)候我也踩到這個(gè)坑了葡秒。。嵌溢。

總結(jié)

本文介紹了對(duì)象轉(zhuǎn)換工具 Mapstruct 庫(kù)眯牧,以安全優(yōu)雅的方式來(lái)減少我們的轉(zhuǎn)換代碼。從文中的示例中可以看出赖草,Mapstruct 提供了大量的功能和配置学少,使我們能夠以簡(jiǎn)單快捷的方式創(chuàng)建從簡(jiǎn)單到復(fù)雜的映射器。文中所介紹到的只是 Mapstruct 庫(kù)的冰山一角秧骑,還有很多強(qiáng)大的功能文中沒(méi)有提到版确,感興趣的朋友可以自行查看 官方使用指南

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末乎折,一起剝皮案震驚了整個(gè)濱河市绒疗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌骂澄,老刑警劉巖吓蘑,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異坟冲,居然都是意外死亡磨镶,警方通過(guò)查閱死者的電腦和手機(jī)溃蔫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)棋嘲,“玉大人,你說(shuō)我怎么就攤上這事矩桂》幸疲” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵侄榴,是天一觀的道長(zhǎng)雹锣。 經(jīng)常有香客問(wèn)我,道長(zhǎng)癞蚕,這世上最難降的妖魔是什么蕊爵? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮桦山,結(jié)果婚禮上攒射,老公的妹妹穿的比我還像新娘。我一直安慰自己恒水,他們只是感情好会放,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著钉凌,像睡著了一般咧最。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上御雕,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天矢沿,我揣著相機(jī)與錄音,去河邊找鬼酸纲。 笑死捣鲸,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的闽坡。 我是一名探鬼主播摄狱,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼无午!你這毒婦竟也來(lái)了媒役?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤宪迟,失蹤者是張志新(化名)和其女友劉穎酣衷,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體次泽,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡穿仪,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年席爽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片啊片。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡只锻,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出紫谷,到底是詐尸還是另有隱情齐饮,我是刑警寧澤,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布笤昨,位于F島的核電站祖驱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏瞒窒。R本人自食惡果不足惜捺僻,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望崇裁。 院中可真熱鬧匕坯,春花似錦、人聲如沸拔稳。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)壳炎。三九已至泞歉,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間匿辩,已是汗流浹背腰耙。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留铲球,地道東北人挺庞。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像稼病,于是被迫代替她去往敵國(guó)和親选侨。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

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