前言
在我們?nèi)粘i_(kāi)發(fā)的分層結(jié)構(gòu)的應(yīng)用程序中,為了各層之間互相解耦仔粥,一般都會(huì)定義不同的對(duì)象用來(lái)在不同層之間傳遞數(shù)據(jù)挠铲,因此冕屯,就有了各種 XXXDTO
、XXXVO
拂苹、XXXBO
等基于數(shù)據(jù)庫(kù)對(duì)象派生出來(lái)的對(duì)象安聘,當(dāng)在不同層之間傳輸數(shù)據(jù)時(shí),不可避免地經(jīng)常需要將這些對(duì)象進(jìn)行相互轉(zhuǎn)換瓢棒。
此時(shí)一般處理兩種處理方式:① 直接使用 Setter
和 Getter
方法轉(zhuǎn)換浴韭、② 使用一些工具類進(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
類型之間映射的實(shí)現(xiàn)。自動(dòng)生成的映射轉(zhuǎn)換代碼只使用簡(jiǎn)單的方法調(diào)用肌括,因此速度快点骑、類型安全而且易于理解閱讀,源碼倉(cāng)庫(kù) Github
地址 MapStruct谍夭『诘危總的來(lái)說(shuō),有如下三個(gè)特點(diǎn):
- 基于注解
- 在編譯期自動(dòng)生成映射轉(zhuǎn)換代碼
- 類型安全紧索、高性能袁辈、無(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)換器類(Mapper)
需要注意的是晚缩,轉(zhuǎn)換器不一定都要使用 Mapper
作為結(jié)尾,只是官方示例推薦以 XXXMapper
格式命名轉(zhuǎn)換器名稱媳危,這里舉例的是最簡(jiǎn)單的映射情況(字段名稱和類型都完全匹配)荞彼,只需要在轉(zhuǎn)換器類上添加 @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é)果。
MapStruct 實(shí)現(xiàn)淺析
在以上示例中暮蹂,使用 MapStruct
通過(guò)簡(jiǎn)單的三步就實(shí)現(xiàn)了 Doctor
到 DoctorDTO
的轉(zhuǎn)換寞缝,那么,MapStruct
是如何做到的呢仰泻?其實(shí)通過(guò)我們定義的轉(zhuǎn)換器可以發(fā)現(xiàn)荆陆,轉(zhuǎn)換器是接口類型的,而我們知道在 Java
中集侯,接口是無(wú)法提供功能的被啼,只是定義規(guī)范,具體干活的還是它的實(shí)現(xiàn)類浅悉。
因此我們可以大膽猜想,MapStruct
肯定給我們定義的轉(zhuǎn)換器接口(DoctorMapper
)生成了實(shí)現(xiàn)類券犁,而通過(guò) Mappers.getMapper(DoctorMapper.class)
獲取到的轉(zhuǎn)換器實(shí)際上是獲取到了轉(zhuǎn)化器接口的實(shí)現(xiàn)類术健。下面通過(guò)在測(cè)試類中 debug
來(lái)驗(yàn)證一下:
通過(guò) debug
可以看出,DoctorMapper.INSTANCE
獲取到的是接口的實(shí)現(xiàn)類 DoctorMapperImpl
粘衬。這個(gè)轉(zhuǎn)換器接口實(shí)現(xiàn)類是在編譯期自動(dòng)生成的荞估,Gradle
項(xiàng)目是在 build/generated/sources/anotationProcessor/Java
下(Maven
項(xiàng)目在 target/generated-sources/annotations
目錄下)咳促,生成以上示例轉(zhuǎn)換器接口的實(shí)現(xiàn)類源碼如下:
可以發(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ì)象屬性名稱和類型完全相同
從上文的示例可以看出轴术,當(dāng)屬性名稱和類型完全一致時(shí),我們只需要定義一個(gè)轉(zhuǎn)換器接口并添加 @Mapper
注解即可钦无,然后 MapStruct
會(huì)自動(dòng)生成實(shí)現(xiàn)類完成轉(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ì)象屬性類型相同但是名稱不同
當(dāng)對(duì)象屬性類型相同但是屬性名稱不一樣時(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ì)于某些類型(比如:一個(gè)類的屬性是自定義的類),無(wú)法以自動(dòng)生成代碼的形式進(jìn)行處理弟塞。此時(shí)我們需要自定義類型轉(zhuǎn)換的方法凭峡,在 JDK 7
之前的版本,就需要使用抽象類來(lái)定義轉(zhuǎn)換 Mapper
了宣肚,在 JDK 8
以上的版本可以使用接口的默認(rèn)方法來(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è)類,我們可以通過(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)屬性名稱和類型完全匹配時(shí)同樣可以自動(dòng)轉(zhuǎn)換,但是當(dāng)來(lái)源對(duì)象有多個(gè)屬性名稱及類型完全和目標(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
屬性不同而不同错洁,支持以下四種不同的取值:
-
default 默認(rèn)方式秉宿,默認(rèn)方式,使用工廠方式(
Mappers.getMapper(Class)
)來(lái)獲取 -
cdi 此時(shí)生成的映射器是一個(gè)應(yīng)用程序范圍的
CDI bean
屯碴,使用@Inject
注解來(lái)獲取 -
spring
Spring
的方式描睦,可以通過(guò)@Autowired
注解來(lái)獲取,在Spring
框架中推薦使用此方式 -
jsr330 生成的映射器用
@javax.inject.Named
和@Singleton
注解导而,通過(guò)@Inject
來(lái)獲取
① 通過(guò)工廠方式獲取
上文的示例中都是通過(guò)工廠方式獲取的忱叭,也就是使用 MapStruct
提供的 Mappers.getMapper(Class<T> clazz)
方法來(lái)獲取指定類型的 Mapper
隔崎。然后在調(diào)用的時(shí)候就不需要反復(fù)創(chuàng)建對(duì)象了,方法的最終實(shí)現(xiàn)是通過(guò)我們定義接口的類加載器加載 MapStruct
生成的實(shí)現(xiàn)類(類名稱規(guī)則為:接口名稱 + Impl
)韵丑,然后調(diào)用該類的無(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)類已經(jīng)被注冊(cè)為容器中一個(gè) Bean
了,通過(guò)如下生成的接口實(shí)現(xiàn)類的代碼也可以看到拾弃,在類上自動(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)有提到,感興趣的朋友可以自行查看 官方使用指南虱痕。