SpringBoot中MapStruct實現(xiàn)優(yōu)雅的數(shù)據(jù)復(fù)制
你是否在做項目時遇到過以下情況:
- DTO(數(shù)據(jù)傳輸對象)與Entity之間的轉(zhuǎn)換:在Java的Web應(yīng)用中,通常不會直接將數(shù)據(jù)庫中的Entity實體對象返回給前端。而是會創(chuàng)建一個DTO對象,這個DTO對象只包含需要返回給前端的字段。此時,就需要將Entity轉(zhuǎn)換為DTO。
- 復(fù)雜對象的映射:當(dāng)需要映射的對象包含大量的字段,或者字段之間存在復(fù)雜的依賴關(guān)系時,手動編寫映射代碼不僅繁瑣,而且容易出錯。
1.為什么選擇MapStruct
1.1.常見的屬性映射方法
一般來說,不使用MapStruct框架進(jìn)行屬性映射,常有的方法以下兩種:
- Getter/Setter方法手動映射
這種方法最樸素,手動編寫代碼將源對象的屬性存入目標(biāo)對象,需要注意實體類中嵌套屬性的判空操作以防止空指針異常。
- BeanUtils.copyProperties()方法進(jìn)行映射
BeanUtils底層使用的是反射機(jī)制實現(xiàn)屬性的映射。反射是一種在運行時動態(tài)獲取類信息、調(diào)用方法或訪問字段的機(jī)制,無法利用JVM的優(yōu)化機(jī)制,因此通常比直接方法調(diào)用慢得多。
此外,BeanUtils 只能同屬性映射,或者在屬性相同的情況下,允許被映射的對象屬性少;但當(dāng)遇到被映射的屬性數(shù)據(jù)類型被修改或者被映射的字段名被修改,則會導(dǎo)致映射失敗。
1.2.MapStruct的優(yōu)勢
MapStruct是一個基于注解的Java代碼生成器,它通過分析帶有@Mapper注解的接口,在編譯時自動生成實現(xiàn)該接口的映射器類。這個映射器類包含了用于執(zhí)行對象之間映射的具體代碼。
與常規(guī)方法相比,MapStruct具備的優(yōu)勢有:
簡化代碼。對于對象內(nèi)屬性較多的情況,使用MapStruct框架無須手動對每個屬性進(jìn)行g(shù)et/set和屬性判空操作。MapStruct可以通過注解和映射接口來定義映射規(guī)則,自動生成映射代碼,從而大大簡化了這種復(fù)雜對象的映射過程。
性能優(yōu)越。相較于反射這種映射方法,MapStruct在編譯期生成映射的靜態(tài)代碼,可以充分利用JVM的優(yōu)化機(jī)制,對于企業(yè)級的項目應(yīng)用來說,這種方式能大大提高數(shù)據(jù)復(fù)制的性能。
類型安全。由于MapStruct在編譯期生成映射代碼,這意味著如果源對象和目標(biāo)對象的映射存在錯誤,那么可以在編譯時就發(fā)現(xiàn)錯誤。相比之下,BeanUtils在運行時使用反射來執(zhí)行屬性復(fù)制,這可能會導(dǎo)致類型不匹配的問題在運行時才發(fā)現(xiàn)。
靈活映射。MapStruct可以輕松處理嵌套對象和集合的映射。對于嵌套對象,MapStruct可以遞歸地應(yīng)用映射規(guī)則;對于集合,MapStruct可以自動迭代集合中的每個元素并應(yīng)用相應(yīng)的映射規(guī)則。
有開發(fā)者對比過兩者的性能差距,如下表。這充分體現(xiàn)了MapStruct性能的強(qiáng)大。
| 對象轉(zhuǎn)換次數(shù) | 屬性個數(shù) | BeanUtils耗時 | MapStruct耗時 |
|---|---|---|---|
| 5千萬次 | 6 | 14秒 | 1秒 |
| 5千萬次 | 15 | 36秒 | 1秒 |
| 5千萬次 | 25 | 55秒 | 1秒 |
2.MapStruct快速入門
在快速入門中,我們的任務(wù)是將dto的數(shù)據(jù)復(fù)制到實體類中。
2.1.導(dǎo)入Maven依賴
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.4.2.Final</version>
</dependency>
2.2.創(chuàng)建相關(guān)對象
注意,實體類要具有g(shù)et/set方法,這里我使用了lombok的@Data注解來實現(xiàn)。
import lombok.Data;
/**
* @author modox
* @date 2024/5/5
*/
@Data
public class Hotel {
// 酒店名稱
private String hotelName;
// 酒店地址
private String hotelAddress;
// 所在城市
private String hotelCity;
// 聯(lián)系電話
private String hotelPhone;
}
dto類我使用了@Builder注解,可以快速為對象賦初始值。
import lombok.Builder;
import lombok.Data;
/**
* @author modox
* @date 2024/5/5
*/
@Data
@Builder
public class HotelDTO {
// 酒店名稱
private String name;
// 酒店地址
private String address;
// 所在城市
private String city;
}
2.3.創(chuàng)建轉(zhuǎn)換器Converter
使用抽象類來定義轉(zhuǎn)換器,只需中@Mapping注解中填寫target和source的字段名,即可實現(xiàn)屬性復(fù)制。
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
/**
* @author modox
* @date 2024/5/5
*/
@Mapper(componentModel = "spring")
public abstract class TestConverter {
//酒店詳情
@Mappings({
@Mapping(target = "hotelName", source = "name"),
@Mapping(target = "hotelAddress", source = "address"),
@Mapping(target = "hotelCity", source = "city"),
})
public abstract Hotel dto2Hotel(HotelDTO hotelDTO);
}
2.4.測試
在SpringBoot的測試類中測試,這里我使用DTO類的@Builder注解提供的方法為dto賦初值模擬實際開發(fā),通過調(diào)用converter的方法實現(xiàn)屬性映射。
@Test
public void Test() {
HotelDTO build = HotelDTO.builder()
.name("五星級酒店")
.address("中國")
.city("北京").build();
TestConverter converter = new TestConverterImpl();
Hotel hotel = converter.dto2Hotel(build);
System.out.println(hotel);
}
結(jié)果如圖:

最后,我們可以發(fā)現(xiàn)在target包的converter的相同目錄下,生成了TestConverter的實現(xiàn)類
里面為我們編寫好了映射的代碼。

3.MapStruct進(jìn)階操作
如果僅是這種簡單層級的對象映射,還不足以體現(xiàn)MapStruct的靈活性。下面將介紹MapStruct的進(jìn)階技巧。
3.1.嵌套映射
假設(shè)我們的Hotel實體類中嵌套了另外一個實體類Master
@Data
public class Hotel {
// 酒店名稱
private String hotelName;
// 酒店地址
private String hotelAddress;
// 所在城市
private String hotelCity;
// 聯(lián)系電話
private String hotelPhone;
private Master master;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Master {
private String personName;
private Integer personAge;
}
}
dto對象為:
@Data
@Builder
public class HotelDTO {
// 酒店名稱
private String name;
// 酒店地址
private String address;
// 所在城市
private String city;
private String personName;
private Integer personAge;
}
我們需要把personName和personAge映射到Hotel實體類的Master中,怎么做?
很簡單,只需要在target屬性中加上Hotel實體類嵌套實體類的字段名,加字符.,再跟上嵌套類的字段名即可
//酒店詳情
@Mappings({
@Mapping(target = "hotelName", source = "name"),
@Mapping(target = "hotelAddress", source = "address"),
@Mapping(target = "hotelCity", source = "city"),
@Mapping(target = "master.personName", source = "personName"),
@Mapping(target = "master.personAge", source = "personAge"),
})
public abstract Hotel dto2Hotel(HotelDTO hotelDTO);
結(jié)果如圖:

3.2.集合映射
如果源對象和目標(biāo)對象的集合的元素類型都是基本數(shù)據(jù)類型,直接在target和source中填寫字段名即可。
若源對象和目標(biāo)對象的集合元素類型不同,怎么做?
這個案例我們需要把DTO的personList映射到masterList中。
@Data
@Builder
public class HotelDTO {
// 酒店名稱
private String name;
// 酒店地址
private String address;
private List<HotelDTO.Person> personList;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Person {
private String personName;
private Integer personAge;
}
}
@Data
public class Hotel {
// 酒店名稱
private String hotelName;
// 酒店地址
private String hotelAddress;
private List<Master> masters;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Master {
private String name;
private Integer age;
}
}
編寫converter,這次需要進(jìn)行兩層映射。
第一層將person集合映射到master集合上。
第二層將person對象的屬性映射到master對象中。
// 酒店詳情
@Mappings({
@Mapping(target = "hotelName", source = "name"),
@Mapping(target = "hotelAddress", source = "address"),
@Mapping(target = "masters", source = "personList")
})
public abstract Hotel dto2Hotel(HotelDTO hotelDTO);
@Mappings({
@Mapping(target = "name", source = "personName"),
@Mapping(target = "age", source = "personAge"),
})
public abstract Hotel.Master toList(HotelDTO.Person person);
結(jié)果如圖:

查看target包下的代碼,可以發(fā)現(xiàn)MapStruct除了兩層映射外,還幫你自動生成了迭代集合添加元素的代碼,從而實現(xiàn)集合元素的復(fù)制。
@Component
public class TestConverterImpl extends TestConverter {
public TestConverterImpl() {
}
// 第一層映射
public Hotel dto2Hotel(HotelDTO hotelDTO) {
if (hotelDTO == null) {
return null;
} else {
Hotel hotel = new Hotel();
hotel.setMasters(this.personListToMasterList(hotelDTO.getPersonList()));
hotel.setHotelAddress(hotelDTO.getAddress());
hotel.setHotelName(hotelDTO.getName());
return hotel;
}
}
// 第二層映射
public Hotel.Master toList(HotelDTO.Person person) {
if (person == null) {
return null;
} else {
Hotel.Master master = new Hotel.Master();
master.setName(person.getPersonName());
master.setAge(person.getPersonAge());
return master;
}
}
// 調(diào)用第二層映射,將person集合的元素添加到master中
protected List<Hotel.Master> personListToMasterList(List<HotelDTO.Person> list) {
if (list == null) {
return null;
} else {
List<Hotel.Master> list1 = new ArrayList(list.size());
Iterator var3 = list.iterator();
while(var3.hasNext()) {
HotelDTO.Person person = (HotelDTO.Person)var3.next();
list1.add(this.toList(person));
}
return list1;
}
}
}
4.字段的邏輯處理
4.1.復(fù)雜邏輯處理(qualifiedByName和@Named)
這次我們需要把dto中的personName和personAge的list集合映射到實體類的masters集合中。常規(guī)的集合映射無法處理這種情況,這時需要使用到qualifiedByName和@Named進(jìn)行特殊處理。
@Data
@Builder
public class HotelDTO {
// 酒店名稱
private String name;
// 酒店地址
private String address;
private List<String> personName;
private List<Integer> personAge;
}
@Data
public class Hotel {
// 酒店名稱
private String hotelName;
// 酒店地址
private String hotelAddress;
// 主人
private List<Master> masters;
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class Master {
private String personName;
private Integer personAge;
}
}
這就需要拿到兩個list的數(shù)據(jù),進(jìn)行手動處理了。在@Mapping注解的qualifiedByName屬性指定方法名定位處理邏輯的方法,@Named(“dtoToMasters”)。
利用stream流進(jìn)行處理。
// 酒店詳情
@Mappings({
@Mapping(target = "hotelName", source = "name"),
@Mapping(target = "hotelAddress", source = "address"),
@Mapping(target = "masters", source = "hotelDTO", qualifiedByName = "dtoToMasters")
})
public abstract Hotel dto2Hotel(HotelDTO hotelDTO);
@Named("dtoToMasters")
List<Hotel.Master> dtoToMasters(HotelDTO hotelDTO) {
List<String> personNames = hotelDTO.getPersonName();
List<Integer> personAges = hotelDTO.getPersonAge();
if (personNames != null && personAges != null && personNames.size() == personAges.size()) {
return IntStream.range(0, personNames.size())
.mapToObj(i -> new Hotel.Master(personNames.get(i), personAges.get(i)))
.collect(Collectors.toList());
}
// 如果列表長度不匹配或其他錯誤情況,可以返回空列表或拋出異常
return Collections.emptyList();
}
返回結(jié)果:

4.2.額外邏輯處理(ignore和@AfterMapping)
@Mappings的ignore屬性,也可以對一個字段(不能是集合)進(jìn)行額外邏輯處理。通常搭配@AfterMapping注解使用。
這個案例中,我們需要根據(jù)DTO的mount屬性判斷是否大于15,如果大于,則判斷hotel實體類的isSuccess為true
@Data
public class Hotel {
// 酒店名稱
private String hotelName;
// 酒店地址
private String hotelAddress;
// 酒店生意是否興隆
private Boolean isSuccess;
}
@Data
@Builder
public class HotelDTO {
// 酒店名稱
private String name;
// 酒店地址
private String address;
private Integer mount;
}
編寫converter,注意@AfterMapping注解下的方法的參數(shù)列表,需要使用@MappingTarget注解指明目標(biāo)對象,
// 酒店詳情
@Mappings({
@Mapping(target = "hotelName", source = "name"),
@Mapping(target = "hotelAddress", source = "address"),
@Mapping(target = "isSuccess", ignore = true)
})
public abstract Hotel dto2Hotel(HotelDTO hotelDTO);
@AfterMapping
void isSuccess(HotelDTO hotelDTO, @MappingTarget Hotel hotel) {
if (hotelDTO.getMount() == null) {
return;
}
boolean b = hotelDTO.getMount() > 15;
hotel.setIsSuccess(b);
}
測試方法
@Test
public void Test() {
HotelDTO build = HotelDTO.builder()
.name("五星級酒店")
.address("中國")
.mount(18).build();
TestConverter converter = new TestConverterImpl();
Hotel hotel = converter.dto2Hotel(build);
System.out.println(hotel);
}
返回結(jié)果

4.3.簡單邏輯處理(expression)
expression可以在注解中編寫簡單的處理邏輯
在這個案例中我需要在實體類的nowTime字段獲取當(dāng)前時間。
@Data
public class Hotel {
// 酒店名稱
private String hotelName;
// 酒店地址
private String hotelAddress;
private LocalDateTime nowTime;
}
直接在expression屬性中使用方法獲取當(dāng)前時間。
// 酒店詳情
@Mappings({
@Mapping(target = "hotelName", source = "name"),
@Mapping(target = "hotelAddress", source = "address"),
@Mapping(expression = "java(java.time.LocalDateTime.now())", target = "nowTime")
})
public abstract Hotel dto2Hotel(HotelDTO hotelDTO);
結(jié)果如下

到此這篇關(guān)于SpringBoot中MapStruct實現(xiàn)優(yōu)雅的數(shù)據(jù)復(fù)制的文章就介紹到這了,更多相關(guān)SpringBoot MapStruct數(shù)據(jù)復(fù)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java?中的?getDeclaredFields()使用與原理解析
在Java反射機(jī)制中,getDeclaredFields()用于獲取類的所有字段,包括私有字段,通過反射,可以在運行時動態(tài)地獲取類的信息并操作其成員,本文詳細(xì)介紹了getDeclaredFields()的使用方法、工作原理以及最佳實踐,涵蓋了反射的基本概念、使用場景和注意事項,感興趣的朋友一起看看吧2025-01-01
Java日常練習(xí)題,每天進(jìn)步一點點(61)
下面小編就為大家?guī)硪黄狫ava基礎(chǔ)的幾道練習(xí)題(分享)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧,希望可以幫到你2021-08-08
java實現(xiàn)數(shù)字轉(zhuǎn)大寫的方法
這篇文章主要介紹了 java實現(xiàn)數(shù)字轉(zhuǎn)大寫的方法的相關(guān)資料,希望通過本文能幫助到大家,讓大家實現(xiàn)這樣的功能,需要的朋友可以參考下2017-10-10
Java數(shù)據(jù)結(jié)構(gòu)與算法學(xué)習(xí)之循環(huán)鏈表
循環(huán)鏈表是另一種形式的鏈?zhǔn)酱鎯Y(jié)構(gòu)。它的特點是表中最后一個結(jié)點的指針域指向頭結(jié)點,整個鏈表形成一個環(huán)。本文將為大家詳細(xì)介紹一下循環(huán)鏈表的特點與使用,需要的可以了解一下2021-12-12
關(guān)于Spring MVC在Controller層中注入request的坑詳解
這篇文章主要給大家介紹了關(guān)于Spring MVC在Controller層中注入request的坑的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04

