Java對象轉換方案分析與mapstruct實踐

2022-01-13 阿里技術

收錄於話題 #Java 86個

隨著系統模塊分層不斷細化,在Java日常開發中不可避免地涉及到各種對象的轉換,如:DO、DTO、VO等等,編寫映射轉換代碼是一個繁瑣重複且還易錯的工作,一個好的工具輔助,減輕了工作量、提升開發工作效率的同時還能減少bug的發生。
CarDTO entity = JSON.parseObject(JSON.toJSONString(carDO), CarDTO.class);

這種方案因為通過生成中間json格式字符串,然後再轉化成目標對象,性能非常差,同時因為中間會生成json格式字符串,如果轉化過多,gc會非常頻繁,同時針對複雜場景支持能力不足,基本很少用。BeanUtil.copyProperties()結合手寫get、set,對於簡單的轉換直接使用BeanUtil,複雜的轉換自己手工寫get、set。該方案的痛點就在於代碼編寫效率低、冗餘繁雜還略顯醜陋,並且BeanUtil因為使用了反射invoke去賦值性能不高。只能適合bean數量較少、內容不多、轉換不頻繁的場景。
org.apache.commons.beanutils.BeanUtils.copyProperties(do, entity);

這種方案因為用到反射的原因,同時本身設計問題,性能比較差。集團開發規約明確規定禁止使用。

org.springframework.beans.BeanUtils.copyProperties(do, entity);

這種方案針對apache的BeanUtils做了很多優化,整體性能提升不少,不過還是使用反射實現比不上原生代碼處理,其次針對複雜場景支持能力不足。

BeanCopier copier = BeanCopier.create(CarDO.class, CarDTO.class, false); copier.copy(do, dto, null);

這種方案動態生成一個要代理類的子類,其實就是通過字節碼方式轉換成性能最好的get和set方式,重要的開銷在創建BeanCopier,整體性能接近原生代碼處理,比BeanUtils要好很多,尤其在數據量很大時,但是針對複雜場景支持能力不足。Object Mapping 技術從大的角度來說分為兩類,一類是運行期轉換,另一類則是編譯期轉換:

綜合性能、成熟度、易用性、擴展性,mapstruct是比較優秀的一個框架。
... <properties>   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version> </properties> ... <dependencies>      <dependency>             <groupId>org.mapstruct</groupId>             <artifactId>mapstruct</artifactId>             <version>${org.mapstruct.version}</version>         </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>                                    <target>1.8</target>                                  <annotationProcessorPaths>                             <path>                                   <groupId>org.mapstruct</groupId>                                   <artifactId>mapstruct-processor</artifactId>                                   <version>${org.mapstruct.version}</version>                             </path>                                                     </annotationProcessorPaths>                   </configuration>               </plugin>         </plugins> </build>

這裡用到了lombok簡化代碼,lombok的原理也是在編譯時去生成get、set等被簡化的代碼。
@Data public class Car {         private String make;         private int numberOfSeats;         private CarType type; }@Data public class CarDTO {         private String make;         private int seatCount;         private String type; }

@Mapper中描述映射,在編輯的時候mapstruct將會根據此描述生成實現類:
@Mapper public interface CarMapper {         @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);  
@Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }

Car car = new Car(...); CarDTO carDTO = CarMapper.INSTANCE.CarToCarDTO(car);

getMapper會去load接口的Impl後綴的實現類。

通過生成spring bean注入使用,Mapper註解加上spring配置,會自動生成一個bean,直接使用bean注入即可訪問。
@Mapper(componentModel = "spring") public interface CarMapper {         @Mapping(source = "numberOfSeats", target = "seatCount")         CarDTO CarToCarDTO(Car car); }

如果配置了spring bean訪問會在註解上自動加上@Component。

如果是雙向映射,例如 從DO到DTO以及從DTO到DO,正向方法和反向方法的映射規則通常是相似的,並且可以通過切換源和目標來簡單地逆轉。使用註解@InheritInverseConfiguration 指示方法應繼承相應反向方法的反向配置。
@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);    
@Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car);
@InheritInverseConfiguration Car CarDTOToCar(CarDTO carDTO); }

有些情況下不需要映射轉換產生新的bean,而是更新已有的bean。
@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "numberOfSeats", target = "seatCount") void updateDTOFromCar(Car car, @MappingTarget CarDTO carDTO);

集合類型(List,Set,Map等)的映射以與映射bean類型相同的方式完成,即通過在映射器接口中定義具有所需源類型和目標類型的映射方法。MapStruct支持Java Collection Framework中的多種可迭代類型。生成的代碼將包含一個循環,該循環遍歷源集合,轉換每個元素並將其放入目標集合。如果在給定的映射器或其使用的映射器中找到用於集合元素類型的映射方法,則將調用此方法以執行元素轉換,如果存在針對源元素類型和目標元素類型的隱式轉換,則將調用此轉換。
@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class); 
@Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car);
List<CarDTO> carsToCarDtos(List<Car> cars);
Set<String> integerSetToStringSet(Set<Integer> integers);
@MapMapping(valueDateFormat = "dd.MM.yyyy") Map<String, String> longDateMapToStringStringMap(Map<Long, Date> source); }

MapStruct 還支持具有多個源參數的映射方法。例如,將多個實體組合成一個數據傳輸對象。在原案例新增一個Person對象,CarDTO中新增driverName屬性,根據Person對象獲得。
@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "car.numberOfSeats", target = "seatCount") @Mapping(source = "person.name", target = "driverName") CarDTO CarToCarDTO(Car car, Person person); }

編譯生成的代碼:

如果相應的源屬性是null ,則可以指定默認值以將預定義值設置為目標屬性。在任何情況下,都可以指定常量來設置這樣的預定義值。默認值和常量被指定為字符串值。當目標類型是原始類型或裝箱類型時,String 值將採用字面量,在這種情況下允許位/八進位/十進位/十六進位模式,只要它們是有效的文字即可。在所有其他情況下,常量或默認值會通過內置轉換或調用其他映射方法進行類型轉換,以匹配目標屬性所需的類型。
@Mapper public interface SourceTargetMapper {         SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     
@Mapping(target = "stringProperty", source = "stringProp", defaultValue = "undefined") @Mapping(target = "longProperty", source = "longProp", defaultValue = "-1") @Mapping(target = "stringConstant", constant = "Constant Value") @Mapping(target = "integerConstant", constant = "14") @Mapping(target = "longWrapperConstant", constant = "3001") @Mapping(target = "dateConstant", dateFormat = "dd-MM-yyyy", constant = "09-01-2014") @Mapping(target = "stringListConstants", constant = "jack-jill-tom") Target sourceToTarget(Source s); }

在某些情況下,可能需要手動實現 MapStruct 無法生成的從一種類型到另一種類型的特定映射。

可以在Mapper中定義默認實現方法,生成轉換代碼將調用相關方法:

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "numberOfSeats", target = "seatCount") @Mapping(source = "length", target = "lengthType") CarDTO CarToCarDTO(Car car);
default String getLengthType(int length) { if (length > 5) { return "large"; } else { return "small"; } } }

也可以定義其他映射器,如下案例Car中Date需要轉換成DTO中的String:
public class DateMapper {         public String asString(Date date) {                 return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).format( date ) : null;         }     
public Date asDate(String date) { try { return date != null ? new SimpleDateFormat( "yyyy-MM-dd" ).parse( date ) : null; } catch ( ParseException e ) { throw new RuntimeException( e ); } } }

@Mapper(uses = DateMapper.class) public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }

若遇到多個類似的方法調用時會出現模稜兩可,需使用@qualifiedBy指定:
@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "numberOfSeats", target = "seatCount") @Mapping(source = "length", target = "lengthType", qualifiedByName = "newStandard") CarDTO CarToCarDTO(Car car);
@Named("oldStandard") default String getLengthType(int length) { if (length > 5) { return "large"; } else { return "small"; } } @Named("newStandard") default String getLengthType2(int length) { if (length > 7) { return "large"; } else { return "small"; } } }


表達式自定義映射

目前僅支持 Java 作為語言。例如,此功能可用於調用構造函數,整個源對象都可以在表達式中使用。應注意僅插入有效的 Java 代碼:MapStruct 不會在生成時驗證表達式,但在編譯期間生成的類中會顯示錯誤。
@Data @AllArgsConstructor public class Driver {         private String name;         private int age; }

@Mapper public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "car.numberOfSeats", target = "seatCount") @Mapping(target = "driver", expression = "java( new com.alibaba.my.mapstruct.example4.beans.Driver(person.getName(), person.getAge()))") CarDTO CarToCarDTO(Car car, Person person); }

@Mapper( imports = UUID.class )public interface SourceTargetMapper {         SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );     
@Mapping(target="id", source="sourceId", defaultExpression = "java( UUID.randomUUID().toString() )") Target sourceToTarget(Source s); }

在某些情況下,可能需要自定義生成的映射方法,例如在目標對象中設置無法由生成的方法實現設置的附加屬性。實現起來也很簡單,用裝飾器模式實現映射器的一個抽象類,在映射器Mapper中添加註解@DecoratedWith指向裝飾器類,使用時還是正常調用。
@Mapper @DecoratedWith(CarMapperDecorator.class) public interface CarMapper {         CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);     
@Mapping(source = "numberOfSeats", target = "seatCount") CarDTO CarToCarDTO(Car car); }

public abstract class CarMapperDecorator implements CarMapper {         private final CarMapper delegate;         protected CarMapperDecorator(CarMapper delegate) {                 this.delegate = delegate;         }         @Override         public CarDTO CarToCarDTO(Car car) {                 CarDTO dto = delegate.CarToCarDTO(car);                 dto.setMakeInfo(car.getMake() + " " + new SimpleDateFormat( "yyyy-MM-dd" ).format(car.getCreateDate()));                 return dto;         } }

技術公開課

Java高級編程

本課程共162課時,包含Java多線程編程、常用類庫、IO編程、網絡編程、類集框架、JDBC等實用開發技術,幫助同學們掌握系統提供的類庫並熟練使用JavaDoc文檔。同時考慮到對面向對象的理解以及常用類的設計模式,在課程講解中還將進行原始碼的使用分析與結構分析。

相關焦點

  • Java –將字符串轉換為int
    在Java中,我們可以使用Integer.parseInt()或Integer.valueOf()將String轉換為int。 Integer.parseInt() –返回原始int 。 Integer.valueOf() –返回一個Integer對象。 對於String位置或負數,轉換是相同的。
  • Java map詳解-用法、遍歷、排序、常用API
    ;import java.util.Hashtable;import java.util.LinkedHashMap;import java.util.Map;import java.util.Random;import java.util.TreeMap;import java.util.UUID;public
  • Java8 Stream接口流式方法:map操作、filter操作以及flatMap操作
    extends R> mapper);這個方法傳入一個Function的函數式接口,這個接口,接收一個泛型T,返回泛型R,map函數的定義,返回的流,表示的泛型是R對象,這個表示,調用這個函數後,可以改變返回的類型 public static void main(String
  • 面試官:Java 8 map 和 flatMap 的區別?大部分人答不上來!
    <Stream> 轉換為 List;輸出結果:=====map list===== https://---www---.演示:/** * mapToLong 轉換 * @author: 棧長 * @from: 公眾號Java技術棧 */private static void mapToLong() {    System.out.println("=====map to long list=====");
  • Java 那些最常用的工具類庫 | 原力計劃
    String username;    private String password;}User user = new User();BeanUtils.setProperty(user, "username", "li");BeanUtils.getProperty(user, "username");map和bean的互相轉換
  • 面試|JAVA 引用詳解
    本文先來介紹一下java的四種引用類型。一,四種引用介紹從Java SE2開始,就提供了四種類型的引用:強引用、軟引用、弱引用和虛引用。Java中提供這四種引用類型主要有兩個目的:第一是可以讓程式設計師通過代碼的方式決定某些對象的生命周期;第二是有利於JVM進行垃圾回收。
  • 什麼時候適合使用 Map 而不是 Object
    value'這裡可以明顯看出其實其定義行為是十分相似的,想必看到這裡大家還沒看出來「Map」到底在何時使用才是最佳實踐,別急接著來。如果操作不當沒有正確遍歷對象屬性,可能會導致出現問題,產生你意料之外的 bug
  • 14個java編程技巧(最佳實踐的初學者)
    避免不必要的對象一個最昂貴的操作(在內存利用率)是java對象的創建。因此,建議只在必要時創建或初始化對象。下面的代碼給出了一個例子:原因是,如果使用雙引號,字符串對待,但在單引號的情況下,字符自動轉換為int型,進行計算。8. 通過簡單的技巧避免內存洩漏內存洩漏經常會導致軟體的性能退化。因為,java自動管理內存,開發商沒有太多的控制。但仍有一些標準的做法,可以用來防止內存洩漏。當查詢完成時,總是釋放資料庫連接。
  • ReflectASM-invoke,高效率java反射機制原理
    所以我們只能想辦法優化反射,而不能抵制反射,那麼優化方案,這裡給大家推薦了ReflectASM。一、性能對比我們先通過簡單的代碼來看看,各種調用方式之間的性能差距。結論:方法直接調用屬於最快的方法,其次是java最基本的反射,而反射中又分是否緩存class兩種,由結果得出其實反射中很大一部分時間是在查找class,實際invoke效率還是不錯的。而reflectasm反射效率要在java傳統的反射之上快了接近1/3.二、reflectasm原理解析。
  • Java8 Stream新特性詳解及實戰
    元素:特定類型的對象,比如List裡面放置的對象,會形成一個隊列。Stream不會存儲元素,只是按需計算。數據源:流的來源,對照上圖中的集合,數組,I/O channel, 產生器generator等。聚合操作:類似SQL語句的各種過濾操作,對照上圖中的filter、sorted、map等。
  • 夯實Java基礎系列14:深入理解Java枚舉類
    由於Java 不支持多繼承,所以枚舉對象不能再繼承其他類。; map = new EnumMap<Color, String>(Color.class); map.put(Color.Blue, "Blue"); map.put(Color.Yellow, "Yellow"); map.put(Color.Red, "Red"); System.out.println(m
  • java stream常見用法匯總,開發效率大幅提升
    users.stream().forEach(user -> System.out.println(user));3.2 查找 find// 取出第一個對象User user = users.stream().findFirst().orElse(null); // 輸出 {"age":1,"name":
  • 有了這款可視化工具,Java 應用性能調優超簡單!
    JVisualVM 簡介VisualVM 是Netbeans的profile子項目,已在JDK6.0 update 7 中自帶,能夠監控線程,內存情況,查看方法的CPU時間和內存中的對 象,已被GC的對象,反向查看分配的堆棧(如100個String對象分別由哪幾個對象分配出來的)。
  • 時間轉換竟多出1年!Java開發中的20個坑你遇到過幾個?
    「編譯器會把 Integer a = 127 轉換為 Integer.valueOf(127)。」 我們看下源碼。 at java.util.AbstractList.add(AbstractList.java:148) at java.util.AbstractList.add(AbstractList.java:108) at object.ArrayAsListTest.main(ArrayAsListTest.java:11)
  • java和C++的區別
    java和C++都是面向對象的程式語言,但它們之間也存在著不同。在Java中,一切都是一種抗議(從Java.lang.Object獲得一切時,命令的單根鏈)。在C++中,沒有這樣的命令根鏈。C++既支持過程編程,也支持面向對象的編程;通過這種方式,它被稱為混合編程。對java感興趣的同學可以參加java培訓來獲得更一步的了解和認識。
  • 輕鬆看懂Java字節碼
    但我並不打算繼續直接分析這個十六進位文件。反編譯字節碼文件使用到java內置的一個反編譯工具 javap 可以反編譯字節碼文件。 通過 javap -help 可了解javap的基本用法用法: javap <options> <classes>其中, 可能的選項包括: -help --help -?
  • 網易伏羲雲原生遊戲中間件平臺實踐
    本文主要介紹伏羲雲原生遊戲中間件平臺的實踐過程。伏羲私有云為遊戲以及AI研究應用提供完善、可靠的計算能力。遊戲作為網易的核心業務,如何更好的為遊戲服務是平臺的重點。遊戲是網易的核心業務。在遊戲項目中,從遊戲的開發、測試、到上線各個階段都具有各種中間件的需求。在此背景下,對我們如何為遊戲提供高效、可靠的中間件服務提出巨大的挑戰。
  • 工程之道,深度學習推理性能業界最佳優化實踐
    MegEngine「訓練推理一體化」的獨特範式,通過靜態圖優化保證模型精度與訓練時一致,無縫導入推理側,再藉助工業驗證的高效卷積優化技術,打造深度學習推理側極致加速方案,實現當前業界最快運行速度。本文從推理側的數據排布(Inference Layout)講起,接著介紹 MegEngine 的 Im2col+MatMul、Winograd、Fast-Run 工程優化實踐。
  • 四大主流WebShell管理工具分析 | 防守方攻略
    chr編碼器:對payload的所有字符都利用利用chr函數進行轉換。chr16編碼器:對payload的所有字符都利用chr函數轉換,與chr編碼器不同的是chr16編碼器對chr函數傳遞的參數是十六進位。rot13編碼器:對payload中的字母進行rot13轉換。
  • 深入理解 Java 虛擬機(第一彈) - Java 內存區域透徹分析
    計數器為空(Undefined),因為native方法是java通過JNI直接調用本地C/C++庫,可以近似的認為native方法相當於C/C++暴露給java的一個接口,java通過調用這個接口從而調用到C/C++方法。由於該方法是通過C/C++而不是java進行實現。