MapStruct
簡介
MapStruct
是一個代碼生成器,它基於約定優於配置方法極大地簡化了 Java bean 類型之間映射的實現。生成的映射代碼使用簡單的方法調用,因此速度快、類型安全且易於理解。MapStruct
是一個注解處理器,它插入到 Java 編譯器中,可用於命令行構建(Maven、Gradle 等)或者IDE。MapStruct
使用合理的默認值,但在配置或實現特殊行為時會避開自定義的實現方式。MapStruct Spring Extensions
已經加入到org.springframework.core.convert
包中作為一種轉換的實現提供使用
目的
在分布式架構(或微服務架構)需要拆分模塊時,不得不思考一個問題:WEB 層能不能出現 DAO 或者 DO 對象?我覺得不行。服務層和表現層應當解耦,后者不應當觸碰到任何持久化對象,其所有的數據來源,均應當由前者提供 ,而這需要在不同的對象模型(例如實體和 DTO)之間進行映射。編寫這樣的映射代碼是一項乏味且容易出錯的任務。MapStruct
旨在通過盡可能自動化來簡化這項工作。映射代碼的工具有很多種,如各種BeanUtils
等,與其他映射框架相比,MapStruct
在編譯時生成 bean 映射,以確保高性能和安全.
對比
數據流對比 可以看 5種常見Bean映射工具的性能比對 (juejin.cn)
市面上還是有很多的相關代碼映射工具,如
ModelMapper
(GitHub - modelmapper/modelmapper: Intelligent object mapping)BeanUtils
selma
(GitHub - xebia-france/selma: Selma Java bean mapping that compiles)mapstruct
- ...
以上工具可以大概分為2類
- **通過反射調用
set/get
或者直接對成員變量賦值, 一般都是調用反射包的invoke
方法 **BeanUtils
都是通過java.beans.PropertyDescriptor
和reflect
包來進行對應的處理,apache 支持名稱相同但類型不同的屬性的轉換,spring 支持忽略某些屬性不進行映射ModelMapper
也是在reflect
包封裝反射支持的
- 編譯期動態生成set/get代碼的class文件 ,在運行時直接調用該class文件。
selma
使用靜態編譯生成字節碼,而不會在運行時或在字符串字段中編寫的偽代碼進行任何反射。mapStruct
是最初提出了映射生成的想法,功能更加豐富,社區建設比較好
從性能、問題排查、文檔、成熟度、擴展性等因素來考慮,MapStruct
是一個不錯的選擇;
使用篇
基礎使用
-
pom
加載依賴<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency>
此外,需要加載maven的
compiler
插件<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>15</source> <target>15</target> <annotationProcessorPaths> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </path> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> <compilerArgs>--enable-preview</compilerArgs> </configuration> </plugin> </plugins> </build>
-
編寫對應的轉換實體和轉換Mapper
-
CarEntity
@Data public class CarDto{ private String make; private Integer seatCount; private String type; }
-
CarDTO
@Data public class CarDto{ private String make; private Integer seatCount; private String type; }
-
CarMapper
CarMapper
有多種 實現方式,建議如果要使用Mapper的話,繼承org.springframework.core.convert.converter.Converter
,2個類的成員變量基本相同的情況下,可以不用做額外的方法處理,Mapper
最常見的還是以下2種- 聲明為
SpringBean
- 生成靜態常量
2種方式都行,代碼如下
@Mapper //@Mapper(componentModel = "spring") //第一種方式 public interface CarMapper extends Converter<Car, CarDto> { //第二種方式 CarMapper MAPPER = Mappers.getMapper(CarMapper.class); @Mapping(target = "seatCount", source = "numberOfSeat") @Override CarDto convert(Car car); }
- 聲明為
-
test
測試類采用的第二種方式進行的轉換,可見在使用方面還是比較方便的
@Test public void transferTest(){ Car car = new Car(); car.setMake("轉換測試"); car.setNumberOfSeat(11); car.setType("dd"); System.out.println(car); CarDto convert = CarMapper.MAPPER.convert(car); System.out.println(convert); }
-
進階使用方式
下面選擇幾個常用場景描述下
- 多參數
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
- 使用spring管理,不寫常量類
@Mapper(componentModel = "spring")
public interface CarMapper {
CarDto carToCarDto(Car car);
}
3.調用其他的映射
@Mapper(uses=DateMapper.class)
public class CarMapper {
CarDto carToCarDto(Car car);
}
- 直接將mapper中返回的值轉換出去
@Repository // CDI component model
public class ReferenceMapper {
@PersistenceContext
private EntityManager entityManager;
public <T extends BaseEntity> T resolve(Reference reference, @TargetType Class<T> entityClass) {
return reference != null ? entityManager.find( entityClass, reference.getPk() ) : null;
}
public Reference toReference(BaseEntity entity) {
return entity != null ? new Reference( entity.getPk() ) : null;
}
}
@Mapper(componentModel = "cdi", uses = ReferenceMapper.class )
public interface CarMapper {
Car carDtoToCar(CarDto carDto);
}
- 指定字段使用指定方法轉換
@Mapper( uses = Titles.class )
public interface MovieMapper {
@Mapping( target = "title", qualifiedByName = { "TitleTranslator", "EnglishToGerman" } )
GermanRelease toGerman( OriginalRelease movies );
}
@Named("TitleTranslator")
public class Titles {
@Named("EnglishToGerman")
public String translateTitleEG(String title) {
// some mapping logic
}
@Named("GermanToEnglish")
public String translateTitleGE(String title) {
// some mapping logic
}
}
解析篇
框架實現依賴
mapStruct
采用了JDK6中的新特性 插入式注解處理API(Pluggable Annotation Processing API) ,lombok
注解,IDEA在編寫代碼時候的標記語法錯誤的紅色下划線都是通過這個特性實現的.其主要抽象類是AbstractProcessor
,需要注意的是,該API只處理編譯期注解
插入式注解處理API(JSR 269)提供一套標准API來處理Annotations(JSR 175),實際上JSR 269不僅僅用來處理Annotation,我覺得更強大的功能是它建立了Java 語言本身的一個模型,它把method, package, constructor, type, variable, enum, annotation等Java語言元素映射為Types和Elements, 從而將Java語言的語義映射成為對象, 我們可以在javax.lang.model包下面可以看到這些類. 所以我們可以利用JSR 269提供的API來構建一個功能豐富的元編程(metaprogramming)環境. JSR 269用Annotation Processor在編譯期間而不是運行期間處理Annotation, Annotation Processor相當於編譯器的一個插件,所以稱為插入式注解處理.如果Annotation Processor處理Annotation時(執行process方法)產生了新的Java代碼,編譯器會再調用一次Annotation Processor,如果第二次處理還有新代碼產生,就會接着調用Annotation Processor,直到沒有新代碼產生為止.每執行一次process()方法被稱為一個"round",這樣整個Annotation processing過程可以看作是一個round的序列. JSR 269主要被設計成為針對Tools或者容器的API.
注解API實現步驟
-
定義
annotation process
:org.mapstruct.ap.MappingProcessor
,並繼承javax.annotation.processing.AbstractProcessor
-
定義注解
org.mapstruct.Mapper
,並將運行策略改成@Retention(RetentionPolicy.SOURCE)
-
在
MappingProcessor
中使用javax.annotation.processing.SupportedAnnotationTypes
指定在第2步創建的注解類型的名稱(注意需要全類名,"包名.注解類型名稱",否則會不生效) -
在
MappingProcessor
中使用javax.annotation.processing.SupportedSourceVersion
指定編譯版本SourceVersion.latestSupported()
。 -
在
MappingProcessor
中使用javax.annotation.processing.SupportedOptions
指定編譯參數。 -
指定processor參與編譯
-
直接使用編譯參數指定,
javac -processor org.mapstruct.ap.MappingProcessor Main.java
。 -
通過服務注冊指定,就是
META-INF/services/javax.annotation.processing.Processor
文件中添加org.mapstruct.ap.MappingProcessor。 -
通過Maven的編譯插件的配置指定如下:
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.5.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> <annotationProcessors> <annotationProcessor> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessor> </annotationProcessors> </configuration> </plugin>
-
框架流程
mapStruct
雖然說實現的功能流程簡單
, 但是它巧妙利用了Types和Elements
,將復雜的class生成分析轉成對應去處理,倒是有其獨特和稱贊的地方