SpringBoot中MapStruct實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)復(fù)制
你是否在做項(xiàng)目時(shí)遇到過(guò)以下情況:
- DTO(數(shù)據(jù)傳輸對(duì)象)與Entity之間的轉(zhuǎn)換:在Java的Web應(yīng)用中,通常不會(huì)直接將數(shù)據(jù)庫(kù)中的Entity實(shí)體對(duì)象返回給前端。而是會(huì)創(chuàng)建一個(gè)DTO對(duì)象,這個(gè)DTO對(duì)象只包含需要返回給前端的字段。此時(shí),就需要將Entity轉(zhuǎn)換為DTO。
- 復(fù)雜對(duì)象的映射:當(dāng)需要映射的對(duì)象包含大量的字段,或者字段之間存在復(fù)雜的依賴(lài)關(guān)系時(shí),手動(dòng)編寫(xiě)映射代碼不僅繁瑣,而且容易出錯(cuò)。
1.為什么選擇MapStruct
1.1.常見(jiàn)的屬性映射方法
一般來(lái)說(shuō),不使用MapStruct框架進(jìn)行屬性映射,常有的方法以下兩種:
- Getter/Setter方法手動(dòng)映射
這種方法最樸素,手動(dòng)編寫(xiě)代碼將源對(duì)象的屬性存入目標(biāo)對(duì)象,需要注意實(shí)體類(lèi)中嵌套屬性的判空操作以防止空指針異常。
- BeanUtils.copyProperties()方法進(jìn)行映射
BeanUtils
底層使用的是反射機(jī)制實(shí)現(xiàn)屬性的映射。反射是一種在運(yùn)行時(shí)動(dòng)態(tài)獲取類(lèi)信息、調(diào)用方法或訪問(wèn)字段的機(jī)制,無(wú)法利用JVM的優(yōu)化機(jī)制,因此通常比直接方法調(diào)用慢得多。
此外,BeanUtils
只能同屬性映射,或者在屬性相同的情況下,允許被映射的對(duì)象屬性少;但當(dāng)遇到被映射的屬性數(shù)據(jù)類(lèi)型被修改或者被映射的字段名被修改,則會(huì)導(dǎo)致映射失敗。
1.2.MapStruct的優(yōu)勢(shì)
MapStruct是一個(gè)基于注解的Java代碼生成器,它通過(guò)分析帶有@Mapper注解的接口,在編譯時(shí)自動(dòng)生成實(shí)現(xiàn)該接口的映射器類(lèi)。這個(gè)映射器類(lèi)包含了用于執(zhí)行對(duì)象之間映射的具體代碼。
與常規(guī)方法相比,MapStruct具備的優(yōu)勢(shì)有:
簡(jiǎn)化代碼。對(duì)于對(duì)象內(nèi)屬性較多的情況,使用MapStruct框架無(wú)須手動(dòng)對(duì)每個(gè)屬性進(jìn)行g(shù)et/set和屬性判空操作。MapStruct可以通過(guò)注解和映射接口來(lái)定義映射規(guī)則,自動(dòng)生成映射代碼,從而大大簡(jiǎn)化了這種復(fù)雜對(duì)象的映射過(guò)程。
性能優(yōu)越。相較于反射這種映射方法,MapStruct在編譯期生成映射的靜態(tài)代碼,可以充分利用JVM的優(yōu)化機(jī)制,對(duì)于企業(yè)級(jí)的項(xiàng)目應(yīng)用來(lái)說(shuō),這種方式能大大提高數(shù)據(jù)復(fù)制的性能。
類(lèi)型安全。由于MapStruct在編譯期生成映射代碼,這意味著如果源對(duì)象和目標(biāo)對(duì)象的映射存在錯(cuò)誤,那么可以在編譯時(shí)就發(fā)現(xiàn)錯(cuò)誤。相比之下,BeanUtils在運(yùn)行時(shí)使用反射來(lái)執(zhí)行屬性復(fù)制,這可能會(huì)導(dǎo)致類(lèi)型不匹配的問(wèn)題在運(yùn)行時(shí)才發(fā)現(xiàn)。
靈活映射。MapStruct可以輕松處理嵌套對(duì)象和集合的映射。對(duì)于嵌套對(duì)象,MapStruct可以遞歸地應(yīng)用映射規(guī)則;對(duì)于集合,MapStruct可以自動(dòng)迭代集合中的每個(gè)元素并應(yīng)用相應(yīng)的映射規(guī)則。
有開(kāi)發(fā)者對(duì)比過(guò)兩者的性能差距,如下表。這充分體現(xiàn)了MapStruct性能的強(qiáng)大。
對(duì)象轉(zhuǎn)換次數(shù) | 屬性個(gè)數(shù) | BeanUtils耗時(shí) | MapStruct耗時(shí) |
---|---|---|---|
5千萬(wàn)次 | 6 | 14秒 | 1秒 |
5千萬(wàn)次 | 15 | 36秒 | 1秒 |
5千萬(wàn)次 | 25 | 55秒 | 1秒 |
2.MapStruct快速入門(mén)
在快速入門(mén)中,我們的任務(wù)是將dto的數(shù)據(jù)復(fù)制到實(shí)體類(lèi)中。
2.1.導(dǎo)入Maven依賴(lài)
<!-- 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)對(duì)象
注意,實(shí)體類(lèi)要具有g(shù)et/set方法,這里我使用了lombok的@Data注解來(lái)實(shí)現(xiàn)。
import lombok.Data; /** * @author modox * @date 2024/5/5 */ @Data public class Hotel { // 酒店名稱(chēng) private String hotelName; // 酒店地址 private String hotelAddress; // 所在城市 private String hotelCity; // 聯(lián)系電話 private String hotelPhone; }
dto類(lèi)我使用了@Builder注解,可以快速為對(duì)象賦初始值。
import lombok.Builder; import lombok.Data; /** * @author modox * @date 2024/5/5 */ @Data @Builder public class HotelDTO { // 酒店名稱(chēng) private String name; // 酒店地址 private String address; // 所在城市 private String city; }
2.3.創(chuàng)建轉(zhuǎn)換器Converter
使用抽象類(lèi)來(lái)定義轉(zhuǎn)換器,只需中@Mapping注解中填寫(xiě)target
和source
的字段名,即可實(shí)現(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.測(cè)試
在SpringBoot的測(cè)試類(lèi)中測(cè)試,這里我使用DTO類(lèi)的@Builder注解提供的方法為dto賦初值模擬實(shí)際開(kāi)發(fā),通過(guò)調(diào)用converter的方法實(shí)現(xiàn)屬性映射。
@Test public void Test() { HotelDTO build = HotelDTO.builder() .name("五星級(jí)酒店") .address("中國(guó)") .city("北京").build(); TestConverter converter = new TestConverterImpl(); Hotel hotel = converter.dto2Hotel(build); System.out.println(hotel); }
結(jié)果如圖:
最后,我們可以發(fā)現(xiàn)在target包的converter的相同目錄下,生成了TestConverter的實(shí)現(xiàn)類(lèi)
里面為我們編寫(xiě)好了映射的代碼。
3.MapStruct進(jìn)階操作
如果僅是這種簡(jiǎn)單層級(jí)的對(duì)象映射,還不足以體現(xiàn)MapStruct的靈活性。下面將介紹MapStruct的進(jìn)階技巧。
3.1.嵌套映射
假設(shè)我們的Hotel實(shí)體類(lèi)中嵌套了另外一個(gè)實(shí)體類(lèi)Master
@Data public class Hotel { // 酒店名稱(chēng) 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對(duì)象為:
@Data @Builder public class HotelDTO { // 酒店名稱(chēng) private String name; // 酒店地址 private String address; // 所在城市 private String city; private String personName; private Integer personAge; }
我們需要把personName
和personAge
映射到Hotel實(shí)體類(lèi)的Master中,怎么做?
很簡(jiǎn)單,只需要在target屬性中加上Hotel實(shí)體類(lèi)嵌套實(shí)體類(lèi)的字段名,加字符.
,再跟上嵌套類(lèi)的字段名即可
//酒店詳情 @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.集合映射
如果源對(duì)象和目標(biāo)對(duì)象的集合的元素類(lèi)型都是基本數(shù)據(jù)類(lèi)型,直接在target和source中填寫(xiě)字段名即可。
若源對(duì)象和目標(biāo)對(duì)象的集合元素類(lèi)型不同,怎么做?
這個(gè)案例我們需要把DTO的personList映射到masterList中。
@Data @Builder public class HotelDTO { // 酒店名稱(chēng) 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 { // 酒店名稱(chēng) private String hotelName; // 酒店地址 private String hotelAddress; private List<Master> masters; @Data @NoArgsConstructor @AllArgsConstructor public static class Master { private String name; private Integer age; } }
編寫(xiě)converter,這次需要進(jìn)行兩層映射。
第一層將person集合映射到master集合上。
第二層將person對(duì)象的屬性映射到master對(duì)象中。
// 酒店詳情 @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除了兩層映射外,還幫你自動(dòng)生成了迭代集合添加元素的代碼,從而實(shí)現(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集合映射到實(shí)體類(lèi)的masters集合中。常規(guī)的集合映射無(wú)法處理這種情況,這時(shí)需要使用到qualifiedByName和@Named進(jìn)行特殊處理。
@Data @Builder public class HotelDTO { // 酒店名稱(chēng) private String name; // 酒店地址 private String address; private List<String> personName; private List<Integer> personAge; }
@Data public class Hotel { // 酒店名稱(chēng) private String hotelName; // 酒店地址 private String hotelAddress; // 主人 private List<Master> masters; @Data @NoArgsConstructor @AllArgsConstructor public static class Master { private String personName; private Integer personAge; } }
這就需要拿到兩個(gè)list的數(shù)據(jù),進(jìn)行手動(dòng)處理了。在@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()); } // 如果列表長(zhǎng)度不匹配或其他錯(cuò)誤情況,可以返回空列表或拋出異常 return Collections.emptyList(); }
返回結(jié)果:
4.2.額外邏輯處理(ignore和@AfterMapping)
@Mappings的ignore屬性,也可以對(duì)一個(gè)字段(不能是集合)進(jìn)行額外邏輯處理。通常搭配@AfterMapping注解使用。
這個(gè)案例中,我們需要根據(jù)DTO的mount屬性判斷是否大于15,如果大于,則判斷hotel實(shí)體類(lèi)的isSuccess為true
@Data public class Hotel { // 酒店名稱(chēng) private String hotelName; // 酒店地址 private String hotelAddress; // 酒店生意是否興隆 private Boolean isSuccess; }
@Data @Builder public class HotelDTO { // 酒店名稱(chēng) private String name; // 酒店地址 private String address; private Integer mount; }
編寫(xiě)converter,注意@AfterMapping注解下的方法的參數(shù)列表,需要使用@MappingTarget注解指明目標(biāo)對(duì)象,
// 酒店詳情 @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); }
測(cè)試方法
@Test public void Test() { HotelDTO build = HotelDTO.builder() .name("五星級(jí)酒店") .address("中國(guó)") .mount(18).build(); TestConverter converter = new TestConverterImpl(); Hotel hotel = converter.dto2Hotel(build); System.out.println(hotel); }
返回結(jié)果
4.3.簡(jiǎn)單邏輯處理(expression)
expression可以在注解中編寫(xiě)簡(jiǎn)單的處理邏輯
在這個(gè)案例中我需要在實(shí)體類(lèi)的nowTime字段獲取當(dāng)前時(shí)間。
@Data public class Hotel { // 酒店名稱(chēng) private String hotelName; // 酒店地址 private String hotelAddress; private LocalDateTime nowTime; }
直接在expression屬性中使用方法獲取當(dāng)前時(shí)間。
// 酒店詳情 @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實(shí)現(xiàn)優(yōu)雅的數(shù)據(jù)復(fù)制的文章就介紹到這了,更多相關(guān)SpringBoot MapStruct數(shù)據(jù)復(fù)制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
springboot:接收date類(lèi)型的參數(shù)方式
這篇文章主要介紹了springboot:接收date類(lèi)型的參數(shù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-10-10Java?中的?getDeclaredFields()使用與原理解析
在Java反射機(jī)制中,getDeclaredFields()用于獲取類(lèi)的所有字段,包括私有字段,通過(guò)反射,可以在運(yùn)行時(shí)動(dòng)態(tài)地獲取類(lèi)的信息并操作其成員,本文詳細(xì)介紹了getDeclaredFields()的使用方法、工作原理以及最佳實(shí)踐,涵蓋了反射的基本概念、使用場(chǎng)景和注意事項(xiàng),感興趣的朋友一起看看吧2025-01-01Java日常練習(xí)題,每天進(jìn)步一點(diǎn)點(diǎn)(61)
下面小編就為大家?guī)?lái)一篇Java基礎(chǔ)的幾道練習(xí)題(分享)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧,希望可以幫到你2021-08-08Java多線程 樂(lè)觀鎖和CAS機(jī)制詳細(xì)
這篇文章主要介紹了Java多線程 樂(lè)觀鎖和CAS機(jī)制,樂(lè)觀鎖是對(duì)于數(shù)據(jù)沖突保持一種樂(lè)觀態(tài)度,操作數(shù)據(jù)時(shí)不會(huì)對(duì)操作的數(shù)據(jù)進(jìn)行加鎖,需要的朋友可以參考下2021-10-10java實(shí)現(xiàn)數(shù)字轉(zhuǎn)大寫(xiě)的方法
這篇文章主要介紹了 java實(shí)現(xiàn)數(shù)字轉(zhuǎn)大寫(xiě)的方法的相關(guān)資料,希望通過(guò)本文能幫助到大家,讓大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-10-10Java數(shù)據(jù)結(jié)構(gòu)與算法學(xué)習(xí)之循環(huán)鏈表
循環(huán)鏈表是另一種形式的鏈?zhǔn)酱鎯?chǔ)結(jié)構(gòu)。它的特點(diǎn)是表中最后一個(gè)結(jié)點(diǎn)的指針域指向頭結(jié)點(diǎn),整個(gè)鏈表形成一個(gè)環(huán)。本文將為大家詳細(xì)介紹一下循環(huán)鏈表的特點(diǎn)與使用,需要的可以了解一下2021-12-12關(guān)于Spring MVC在Controller層中注入request的坑詳解
這篇文章主要給大家介紹了關(guān)于Spring MVC在Controller層中注入request的坑的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2018-04-04JAVA操作HDFS案例的簡(jiǎn)單實(shí)現(xiàn)
本篇文章主要介紹了JAVA操作HDFS案例的簡(jiǎn)單實(shí)現(xiàn),小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-08-08