基本用法
假設(shè)我們有兩個類需要進行互相轉(zhuǎn)換肃续,分別是PersonDO和PersonDTO,類定義如下:
@Data
public class PersonDO {
private int id;
private String name;
private Integer age;
private Date birthday;
}
@Data
public class PersonDTO {
private String name;
private Integer age;
private Date birthday;
}
我們演示下如何使用MapStruct進行bean映射。
想要使用MapStruct疼蛾,首先需要依賴他的相關(guān)的jar包衍慎,使用maven依賴方式如下:
...
<properties>
<org.mapstruct.version>1.3.1.Final</org.mapstruct.version>
<lombok.version>1.18.10</lombok.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
...
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source> <!-- depending on your project -->
<target>1.8</target> <!-- depending on your project -->
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
</path>
<!-- other annotation processors -->
</annotationProcessorPaths>
</configuration>
</plugin>
</plugins>
</build>
因為MapStruct需要在編譯器生成轉(zhuǎn)換代碼,所以需要在maven-compiler-plugin插件中配置上對mapstruct-processor的引用乔夯。這部分在后文會再次介紹款侵。
之后甲脏,我們需要定義一個做映射的接口妹笆,主要代碼如下:
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings({
@Mapping(source = "username", target = "name"),
@Mapping(source = "age", target = "myage")
})
PersonDTO do2dto(PersonDO person);
}
使用注解 @Mapper定義一個Converter接口墩新,在其中定義一個do2dto方法海渊,方法的入?yún)㈩愋褪荘ersonDO切省,出參類型是PersonDTO帕胆,這個方法就用于將PersonDO轉(zhuǎn)成PersonDTO懒豹。
測試代碼如下:
@Test
public void mapConstructTest(){
PersonDO personDO = new PersonDO();
personDO.setId(1);
personDO.setName("jim");
personDO.setAge(29);
personDO.setBirthday(new Date());
PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
System.out.println(personDTO);
}
輸出結(jié)果:
PersonDTO(name=jim, age=29, birthday=Sun Aug 16 15:00:56 CST 2020)
假如存在名稱字段不一致的情況需要映射應(yīng)該怎么處理呢儒老?下面通過一個案例加以說明记餐,例如:
@Data
public class PersonDO {
private int id;
private String userName;
private Integer age;
private Date birthday;
}
@Data
public class PersonDTO {
private String name;
private Integer myage;
private Date birthday;
}
改寫Converter類如下:
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
PersonDTO do2dto(PersonDO person);
}
單元測試:
@Test
public void mapConstructTest(){
PersonDO personDO = new PersonDO();
personDO.setId(1);
personDO.setUsername("jim");
personDO.setAge(29);
personDO.setBirthday(new Date());
PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
System.out.println(personDTO);
}
運行結(jié)果:
PersonDTO(name=jim, myage=29, birthday=Sun Aug 16 15:05:53 CST 2020)
如果待轉(zhuǎn)換類中存在子類的屬性需要賦值給其他類的屬性應(yīng)該怎么做呢挖腰?
@Data
public class PersonDO {
private int id;
private String username;
private Integer age;
private Date birthday;
private UserInfo userInfo;
}
@Data
public class PersonDTO {
private String name;
private Integer myage;
private Date birthday;
private String img;
}
編寫類型轉(zhuǎn)換類:
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings({
@Mapping(source = "username", target = "name"),
@Mapping(source = "age", target = "myage"),
@Mapping(source="person.userInfo.userImg",target = "img")
})
PersonDTO do2dto(PersonDO person);
}
編寫單元測試類:
@Test
public void mapConstructTest(){
PersonDO personDO = new PersonDO();
personDO.setId(1);
personDO.setUsername("jim");
personDO.setAge(29);
personDO.setBirthday(new Date());
UserInfo userInfo=new UserInfo();
userInfo.setId(1);
userInfo.setUserImg("test.png");
personDO.setUserInfo(userInfo);
PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);
System.out.println(personDTO);
}
運行結(jié)果:
PersonDTO(name=jim, myage=29, birthday=Sun Aug 16 15:13:15 CST 2020, img=test.png)
加入希望在轉(zhuǎn)換的同時對日期格式進行格式化猴仑,PersonDTO
中新增了一個formatDate
字段用以表示格式化后的日期:
@Data
public class PersonDTO {
private String name;
private Integer myage;
private Date birthday;
private String img;
private String formatDate;
}
改寫轉(zhuǎn)換類,
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings({
@Mapping(source = "username", target = "name"),
@Mapping(source = "age", target = "myage"),
@Mapping(source="person.userInfo.userImg",target = "img"),
@Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
})
PersonDTO do2dto(PersonDO person);
}
運行單元測試類崖飘,結(jié)果如下:
PersonDTO(name=jim, myage=29, birthday=Sun Aug 16 15:17:15 CST 2020, img=test.png, formatDate=2020-08-16 15:17:15)
加入目標類中有一個屬性language
為固定常量值zh
坐漏,且被被復(fù)制類中沒有該屬性赊琳,例如:
@Data
public class PersonDO {
private int id;
private String username;
private Integer age;
private Date birthday;
private UserInfo userInfo;
}
@Data
public class PersonDTO {
private String name;
private Integer myage;
private String language;
private Date birthday;
private String img;
private String formatDate;
}
撰寫轉(zhuǎn)換類:
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings({
@Mapping(source = "username", target = "name"),
@Mapping(source = "age", target = "myage"),
@Mapping(source="person.userInfo.userImg",target = "img"),
@Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(target = "language", constant = "zh")
})
PersonDTO do2dto(PersonDO person);
}
運行結(jié)果:
PersonDTO(name=jim, myage=29, language=zh, birthday=Sun Aug 16 15:26:07 CST 2020, img=test.png, formatDate=2020-08-16 15:26:07)
如果我們希望在類屬性進行轉(zhuǎn)換的過程中進行一些更加自定義的操作躏筏,應(yīng)該如何基于mapconstruct的轉(zhuǎn)換類進行擴展呢?切看下面的一個簡單的示例:
新增類HomeAddress:
@Data
public class HomeAddress {
private String address;
}
PersonDO
中新增屬性address
@Data
public class PersonDO {
private int id;
private String username;
private Integer age;
private Date birthday;
private UserInfo userInfo;
private HomeAddress address;
}
PersonDTO
中新增屬性address
@Data
public class PersonDTO {
private String name;
private Integer myage;
private String language;
private Date birthday;
private String img;
private String formatDate;
private String address;
}
修改PersonConverter
類如下:
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings({
@Mapping(source = "username", target = "name"),
@Mapping(source = "age", target = "myage"),
@Mapping(source="person.userInfo.userImg",target = "img"),
@Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(target = "language", constant = "zh"),
@Mapping(target="address",expression = "java(homeAddressToString(person.getAddress()))")
})
PersonDTO do2dto(PersonDO person);
default String homeAddressToString(HomeAddress address){
return JSON.toJSONString(address);
}
}
單元測試酥泞;
@Mapper
public interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mappings({
@Mapping(source = "username", target = "name"),
@Mapping(source = "age", target = "myage"),
@Mapping(source="person.userInfo.userImg",target = "img"),
@Mapping(source = "birthday", target = "formatDate", dateFormat = "yyyy-MM-dd HH:mm:ss"),
@Mapping(target = "language", constant = "zh"),
@Mapping(target="address",expression = "java(homeAddressToString(person.getAddress()))")
})
PersonDTO do2dto(PersonDO person);
default String homeAddressToString(HomeAddress address){
return JSON.toJSONString(address);
}
}
運行結(jié)果:
PersonDTO(name=jim, myage=29, language=zh, birthday=Sun Aug 16 15:32:16 CST 2020, img=test.png, formatDate=2020-08-16 15:32:16, address={"address":"test address"})
實現(xiàn)原理
MapStruct和其他幾類框架最大的區(qū)別就是:與其他映射框架相比,MapStruct在編譯時生成bean映射悯姊,這確保了高性能,可以提前將問題反饋出來,也使得開發(fā)人員可以徹底的錯誤檢查辉阶。
還記得前面我們在引入MapStruct的依賴的時候瘩扼,特別在maven-compiler-plugin中增加了mapstruct-processor的支持嗎邢隧?
并且我們在代碼中使用了很多MapStruct提供的注解冈在,這使得在編譯期,MapStruct就可以直接生成bean映射的代碼炫贤,相當于代替我們寫了很多setter和getter。
如我們在代碼中定義了以下一個Mapper:
@Mapper
interface PersonConverter {
PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);
@Mapping(source = "userName", target = "name")
@Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")
@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")
PersonDO dto2do(PersonDTO dto2do);
default String homeAddressToString(HomeAddress address){
return JSON.toJSONString(address);
}
}
經(jīng)過代碼編譯后,會自動生成一個PersonConverterImpl:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2020-08-09T12:58:41+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_181 (Oracle Corporation)"
)
class PersonConverterImpl implements PersonConverter {
@Override
public PersonDO dto2do(PersonDTO dto2do) {
if ( dto2do == null ) {
return null;
}
PersonDO personDO = new PersonDO();
personDO.setName( dto2do.getUserName() );
if ( dto2do.getAge() != null ) {
personDO.setAge( dto2do.getAge() );
}
if ( dto2do.getGender() != null ) {
personDO.setGender( dto2do.getGender().name() );
}
personDO.setAddress( homeAddressToString(dto2do.getAddress()) );
return personDO;
}
}
在運行期掠河,對于bean進行映射的時候唠摹,就會直接調(diào)用PersonConverterImpl的dto2do方法勾拉,這樣就沒有什么特殊的事情要做了藕赞,只是在內(nèi)存中進行set和get就可以了斧蜕。
所以惩激,因為在編譯期做了很多事情,所以MapStruct在運行期的性能會很好风钻,并且還有一個好處骡技,那就是可以把問題的暴露提前到編譯期。
使得如果代碼中字段映射有問題昼窗,那么應(yīng)用就會無法編譯,強制開發(fā)者要解決這個問題才行。
學(xué)會查看編譯后的源代碼還是可以幫助我們解決不少問題的富雅,下面以一個筆者在實際使用過程中遇到和解決問題的經(jīng)歷解釋一下如何去查看編譯后的源碼并且借此解決問題的。
首先,像上面提到的進的經(jīng)典用法一下蛤奢,筆者寫了一個Converter的轉(zhuǎn)換類。
@Mappings({
@Mapping(source = "page", target = "pageNo"),
@Mapping(source = "limit", target = "pageSize")
})
TaskCarbonQueryParam getNotifyMeInCorpQuery2TaskCarbonQueryParam(GetNotifyMeInCorpQuery getNotifyMeInCorpQuery);
但是在編譯的時候卻產(chǎn)生了如下的報錯:
由于目標類不是筆者定義的,而是兄弟團隊的小伙伴提供的一個二方API包中定義的bean瓜晤,去看一下這個類的類定義如下:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TaskCarbonQueryParam extends Paginator {
private String tenantId;
private String carbonUserId;
....
}
@Getter
public abstract class Paginator extends DTO {
private int pageNo = 1;
private int pageSize = 20;
public void setPageNo(int pageNo) {
if (pageNo <= 0) {
pageNo = 1;
}
this.pageNo = pageNo;
}
public void setPageSize(int pageSize) {
if (pageSize <= 0) {
pageSize = 20;
}
this.pageSize = pageSize;
}
}
確認類屬性名稱確實沒有寫錯痢掠,不知道因何原因報錯足画,我們選擇去看一下編譯出來的代碼:
發(fā)現(xiàn)正逞痛牵可以編譯成功的是直接new一個對象然后往里面挨個set值象缀,但是下面出現(xiàn)問題類央星,因為子類使用了lombok的@Builder注解莉给,但是@Builder注解的副作用在于無法將父類的屬性加入到builder模式中,導(dǎo)致在builder的時候無法取用到父類的屬性造成了失敗徐矩。解決方案是將子類的@builder注解去除掉。
性能對比
在Echart折線圖中撰寫echarts腳本:
option = {
title: {
text: '拷貝工具類性能對比'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: ['MapStruct', 'Spring BeanUtils', 'Cglib BeanCopier', 'Apache PropertyUtils', 'Apache BeanUtils', 'Dozer']
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: ['1000', '10000', '100000', '1000000']
},
yAxis: {
type: 'value'
},
series: [
{
name: 'MapStruct',
type: 'line',
stack: '總量',
data: [0, 1, 3, 6]
},
{
name: 'Spring BeanUtils',
type: 'line',
stack: '總量',
data: [5,10,45,169]
},
{
name: 'Cglib BeanCopier',
type: 'line',
stack: '總量',
data: [4,18,45,91]
},
{
name: 'Apache PropertyUtils',
type: 'line',
stack: '總量',
data: [60,265,1444,11492]
},
{
name: 'Apache BeanUtils',
type: 'line',
stack: '總量',
data: [138,816,4154,36938]
},
{
name: 'Dozer',
type: 'line',
stack: '總量',
data: [566, 2254, 11136, 102965]
}
]
};
可以看到,MapStruct的耗時相比較于其他幾款工具來說是非常短的刽漂。