MapStruct是什么
在現在多模塊多層級的項目中,應用於應用之間,模塊於模塊之間數據模型一般都不通用,每層都有自己的數據模型。
這種對象與對象之間的互相轉換,目前都是使用get,set方法,或者使用自定義的Beans.copyProperties進行轉換。
使用get,set方式會使得編碼非常的麻煩,Beans.copyProperties的方式是使用反射的方式,對性能的消耗比較大。
MapStruct就是一個屬性映射工具。
與手動編寫映射代碼相比,MapStruct通過生成繁瑣且易於出錯的代碼來節省時間。約定優於配置的方式,MapStruct使用合理的默認值,但在配置或實現特殊行為時不加理會。
和動態映射框架相比,MapStruct具有以下優點:
- 通過使用普通方法調用而不是反射來快速執行
- 編譯時類型安全性
- 構建是清除錯誤報告
- 映射不完整(並非所有
target屬性都被映射) - 映射不正確(找不到正確的映射方法或類型轉換)
- 映射不完整(並非所有
MapStruct配置
基於Mavne
<properties>
<org.mapstruct.version>1.3.1.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.5.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>
基於Gradle
plugins {
id 'net.ltgt.apt' version '0.20'
}
apply plugin: 'net.ltgt.apt-idea'
apply plugin: 'net.ltgt.apt-eclipse'
dependencies {
implementation "org.mapstruct:mapstruct:${mapstructVersion}"
annotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
// If you are using mapstruct in test code
testAnnotationProcessor "org.mapstruct:mapstruct-processor:${mapstructVersion}"
}
配置選項
MapStruct代碼生成器有一些處理器選項配置
- mapstruct.suppressGeneratorTimesstamp
- 如果設置為true,將禁止在生成的映射器類中創建時間戳
- 默認為false
- mapstruct.suppressGeneratorVersionInfoComment
- 如果設置為true,將禁止在生成的映射器類中創建屬性
- 默認為false
- mapstruct.defaultComponentModel
- 基於生成映射器的組件模型的名稱
- 支持:default,cdi,spring,jsr330
- 默認為default,使用spring可以使用@Autowired方式注入
- mapstruct.unmappedTargetPolicy
- 在未使用
source值填充映射方法的target的屬性的情況下要應用的默認報告策略。 - 支持:ERROR(映射代碼生成失敗),WARN(構建時引起警告),IGNORE(將被忽略)
- 默認為WARN
- 在未使用
配置方式如下
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>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<compilerArg>
-Amapstruct.suppressGeneratorTimestamp=true
</compilerArg>
<compilerArg>
-Amapstruct.suppressGeneratorVersionInfoComment=true
</compilerArg>
</compilerArgs>
</configuration>
</plugin>
gradle方式
compileJava {
options.compilerArgs = [
'-Amapstruct.suppressGeneratorTimestamp=true',
'-Amapstruct.suppressGeneratorVersionInfoComment=true'
]
}
MapStruct使用
基礎應用
要創建映射器,只需要使用所需映射方法定義一個Java接口,並且使用org.mapstruct.Mapper注解進行注釋
@Data
public class Source {
private String testing;
private String a;
}
public class Target {
private String test;
private String a;
public String getTest() {
return test;
}
public void setTest(String test) {
this.test = test;
}
public String getA() {
return a;
}
public void setA(String a) {
this.a = a;
}
}
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper(SourceTargetMapper.class);
@Mapping(target = "test", source = "testing")
Target toTarget(Source s);
}
該@Mapper注解將使用MapStruct代碼生成器創建對應的映射類
在生成的方法實現中,source類型的所有可讀屬性都將復制到target類型的相應屬性中
- 當一個屬性於其
target實體對應的名稱相同時,它將被隱式映射, - 當屬性在
target實體中具有不同的名稱時,可以通過@Mapping注釋指定其名稱。
生成的代碼如下:
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2019-12-19T15:10:51+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_172 (Oracle Corporation)"
)
public class SourceTargetMapperImpl implements SourceTargetMapper {
@Override
public Target toTarget(Source s) {
if ( s == null ) {
return null;
}
Target target = new Target();
target.setTest( s.getTesting() );
target.setA( s.getA() );
return target;
}
}
MapStruct的原理是生成和我們自己寫的代碼一樣的代碼,這意味着這些值是通過簡單的getter/setter調用而不是反射或類似的方法從source類復制到target類的。使得MapStruct的性能會比動態框架更加優秀
添加自定義轉換方法
在某種情況下可能需要手動實現從一種類型到另一種類型的特定映射。解決這個問題的方法是實現自定義方法,然后該類由MapStruct生成的映射器使用
自定義方法可以寫在接口中,使用Java8的接口方法,也可以寫在其他類中,在接口的@Mapper注解中引入
方式一: Car中有類型為Person的屬性,CarDto中有類型為PersonDto的屬性
@Mapper
public interface CarMapper {
@Mapping(...)
...
CarDto carToCarDto(Car car);
default PersonDto personToPersonDto(Person person) {
//hand-written mapping logic
}
}
方式二、通過外部引入自定義的轉換方式,將同名的Optional類型屬性和非Optional類型屬性相互轉換
public class OptionalMapper {
public <T> T toOptional(Optional<T> test) {
if (test == null) {
return null;
}
return test.orElse(null);
}
public <T> Optional<T> formOptional(T test) {
if (test == null) {
return Optional.empty();
}
return Optional.of(test);
}
}
@Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR, uses = OptionalMapper.class)
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper(SourceTargetMapper.class);
Target toTarget(Source s);
}
具有多個source參數的映射
有時候需要將多個source對象聚合成一個target的場景
這個和普通的一對一很相似,在@Mapping注解中定義對應source和target對應的屬性即可,參數可以是基礎數據類型和String,映射時使用對象名稱進行。
@Mapper
public interface AddressMapper {
@Mapping(source = "person.description", target = "description")
@Mapping(source = "address.houseNo", target = "houseNumber")
DeliveryAddressDto personAndAddressToDeliveryAddressDto(Person person, Address address);
}
更新類的屬性
在一些場景里面,映射不需要創建新的target,而是更新該類型的現有實例,可以使用注解@MappingTarget注釋參數,來表明該參數是映射的target。下面是例子:
@Data
public class MammalEntity {
private Long numberOfLegs;
private Long numberOfStomachs;
}
@Data
public class MammalDto {
private Integer numberOfLegs;
}
@Mapper
public interface SourceTargetMapper {
SourceTargetMapper MAPPER = Mappers.getMapper( SourceTargetMapper.class );
@Mapping( target = "numberOfStomachs", source = "numberOfStomachs" )
void toTarget(MammalDto source, Long numberOfStomachs, @MappingTarget MammalEntity target);
}
生成的映射類如下:
public class SourceTargetMapperImpl implements SourceTargetMapper {
@Override
public void toTarget(MammalDto source, Long numberOfStomachs, MammalEntity target) {
if ( source == null && numberOfStomachs == null ) {
return;
}
if ( source != null ) {
if ( source.getNumberOfLegs() != null ) {
target.setNumberOfLegs( source.getNumberOfLegs().longValue() );
}
else {
target.setNumberOfLegs( null );
}
}
if ( numberOfStomachs != null ) {
target.setNumberOfStomachs( numberOfStomachs );
}
}
}
映射對象引用
通常一個對象不僅具有基本屬性,而且還引用了其他對象
例如:Car類包含一個Person對象,該對應應該映射到CarDto中的PersionDto.
這種情況下,只需為引用的對象類型定義一個映射方法。
@Mapper
public interface CarMapper {
CarDto carToCarDto(Car car);
PersonDto personToPersonDto(Person person);
}
生成映射方法是,MapStruct將為source對象和target中的每個屬性對應以下規則:
-
如果source和target屬性具有相同的類型,則簡單的將source復制到target,如果屬性是一個集合,則該集合的副本將設置到
target屬性中,集合類型生成代碼如下:List<Integer> list = s.getListTest(); if ( list != null ) { target.setListTest( new ArrayList<Integer>( list ) ); } -
如果
source屬性和target屬性類型不同,檢查是否存在另一種映射方式,該映射方法將source屬性的類型作為參數類型並將target屬性的類型作為返回類型。如果存在這樣的方法,它將在生成的映射實現中調用。 -
如果不存在這樣的方法,MapStruct將查看是否存在屬性的
source類型和target類型的內置轉換。在這種情況下,生成的映射代碼將應用此轉換。 -
如果找不到這種方法,MapStruct將嘗試生成自動子映射方法,該方法將在
source屬性和target屬性之間進行映射。 -
如果MapStruct無法創建基於名稱的映射方法,則會在構建時引發錯誤,指示不可映射的屬性及其路徑。
隱式類型轉換
MapStruct會自動處理類型轉換,例如:如果某個屬性int在souce中類型為String,則生成的代碼將分別通過調用String.valueOf(int),和Integer.parseInt(String)執行轉換。
以下轉換將自動應用:
-
在所有Java原語數據類型及其對應的包裝器類型之間(例如在int和之間Integer,boolean以及Boolean等)之間。生成的代碼是已知的null,即,當將包裝器類型轉換為相應的原語類型時,null將執行檢查.
-
在所有Java數字類型和包裝器類型之間,例如在int和long或byte和之間Integer。
從較大的數據類型轉換為較小的數據類型(例如從long到int)可能會導致值或精度損失。在Mapper和MapperConfig注釋有一個方法typeConversionPolicy來控制警告/錯誤。由於向后兼容的原因,默認值為“ ReportingPolicy.IGNORE”。
-
所有Java基本類型之間(包括其包裝)和String之間,例如int和String或Boolean和String。java.text.DecimalFormat可以指定理解的格式字符串。
@IterableMapping(numberFormat = "$#.00") List<String> prices(List<Integer> prices);自動生成的映射類方法
@Override public List<String> prices(List<Integer> prices) { if ( prices == null ) { return null; } List<String> list = new ArrayList<String>( prices.size() ); for ( Integer integer : prices ) { list.add( new DecimalFormat( "$#.00" ).format( integer ) ); } return list; } -
在enum類型和之間String。
-
在大數字類型(java.math.BigInteger,java.math.BigDecimal)和Java基本類型(包括其包裝器)以及字符串之間。java.text.DecimalFormat可以指定理解的格式字符串
-
在java.util.Date/ XMLGregorianCalendar和之間String。java.text.SimpleDateFormat可以通過以下dateFormat選項指定可理解的格式字符串:
@IterableMapping(dateFormat = "dd.MM.yyyy") List<String> stringListToDateList(List<Date> dates);自動生成的映射類方法
@Override public List<String> stringListToDateList(List<Date> dates) { if ( dates == null ) { return null; } List<String> list = new ArrayList<String>( dates.size() ); for ( Date date : dates ) { list.add( new SimpleDateFormat( "dd.MM.yyyy" ).format( date ) ); } return list; }
高級映射選項
默認值和常量
如果相應的source屬性為null,則可以指定將target的屬性設置為默認值。在任何情況下都可以指定常量來設置這樣的預定義值。
默認值和常量使用String來指定,當target類型是基礎類型或者包裝類時,映射類會進行自動的類型轉換,以匹配target屬性所需的類型
@Mapping(target = "name", constant = "常量")
@Mapping(target = "quantity", defaultValue = "1L")
OrderItem toOrder(OrderItemDto orderItemDto);
@Override
public OrderItem toOrder(OrderItemDto orderItemDto) {
if ( orderItemDto == null ) {
return null;
}
OrderItem orderItem = new OrderItem();
if ( orderItemDto.quantity != null ) {
orderItem.setQuantity( orderItemDto.quantity );
}
else {
orderItem.setQuantity( (long) 1L );
}
orderItem.setName( "常量" );
return orderItem;
}
表達式
該功能對於調用函數很有用,整個source對象在表達式中都是可被調用的。
MapStruct不會生成驗證表達式,但在編譯過程中錯誤會顯示在生成的類中
imports com.sample.TimeAndFormat;
@Mapper( imports = TimeAndFormat.class )
public interface SourceTargetMapper {
SourceTargetMapper INSTANCE = Mappers.getMapper( SourceTargetMapper.class );
@Mapping(target = "timeAndFormat",
expression = "java( new TimeAndFormat( s.getTime(), s.getFormat() ) )")
Target sourceToTarget(Source s);
}
MapStruct不會處理類的導入,但是可以在@Mapper注解中使用imports中引入
默認表達式
默認表達式是默認值和表達式的組合,當source屬性為null是才使用
imports java.util.UUID;
@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);
}
配置的繼承
方法級的配置注解,例如:@Mapping, @BeanMapping, @IterableMapping等等,都可以使用注解@InheritConfiguration從一個映射方法的類似使用中繼承
@Mapper
public interface CarMapper {
@Mapping(target = "numberOfSeats", source = "seatCount")
Car carDtoToCar(CarDto car);
@InheritConfiguration
void carDtoIntoCar(CarDto carDto, @MappingTarget Car car);
}
上面的示例中聲明了carDtoToCar()具有配置的映射方法,在進行updateCar的時候使用注解@InheritConfigurationMapStruct就可以搜索可以繼承的方法進行映射。
如果存在多個可以用來繼承的方法的時候,就需要在當前的映射器中定義需要繼承的方法:@InheritConfiguration( name = "carDtoToCar" )
逆映射
在雙向映射的情況下,正向方法和返現方法的映射規則通常都是相似的,就可以簡單的切換source和targe加上繼承的注解,進行逆映射。
@Mapping(source = "orders", target = "orderItems")
@Mapping(source = "customerName", target = "name")
Customer toCustomer(CustomerDto customerDto);
@InheritInverseConfiguration
CustomerDto fromCustomer(Customer customer);
總結
總結一下MapStruct的注解關鍵詞
@Mapper 在接口上添加這個注解,MapStruct才會去實現該接口
using: 應用外部映射類
import: 導入表達式使用的類
componentModel: 指定實現類的類型
default:默認,通過Mappers.getMapper(Class)方式獲取實例對象
spring: 自動添加注解@Component,通過@Autowired方式注入
@Mapping: 屬性映射,若source對象與target對象名稱一致,會自動映射對應屬性
source: 源屬性
target: 目標屬性
deteFormat: String 到Date日期之間相互轉換,通過SimpleDateFormat
ignore: 忽略這個字段
@Mappings:配置多個@Mapping
@MappingTarget: 用於更新已有對象
@InheritConfiguration: 用於繼承配置
文章中介紹了MapStruct中個人覺得實用的功能,此外還有很多高級特性在文章中沒有進行描述,感興趣可以去閱讀MapStruct官方文檔了解更多。
相對於我們目前對象轉換的方式來說,MapStruct有着和Lombok一樣提高工作效率的能力。可以使我們從繁瑣的transfer中脫離出來,專心於業務代碼的實現。
MapStruct同樣也兼容Lombok
Maven中配置如下
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.5.1</version> <!-- or newer 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>
<!-- other annotation processors -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
gradle中配置如下
dependencies {
compile "org.mapstruct:mapstruct:${mapstructVersion}", "org.projectlombok:lombok:${lombokVersion}"
testCompile 'junit:junit:4.12'
apt "org.mapstruct:mapstruct-processor:${mapstructVersion}", "org.projectlombok:lombok:${lombokVersion}"
}
對於性能來說MapStruct的性能在映射框架中同樣也屬於佼佼者的地位。

