經常使用開發庫 - MapStruct工具庫詳解

經常使用開發庫 - MapStruct工具庫詳解

MapStruct是一款很是實用Java工具,主要用於解決對象之間的拷貝問題,好比PO/DTO/VO/QueryParam之間的轉換問題。區別於BeanUtils這種經過反射,它經過編譯器編譯生成常規方法,將能夠很大程度上提高效率。@pdaihtml

爲何會引入MapStruct這類工具

首先看下這類工具出現的背景。@pdaijava

JavaBean 問題引入

在開發的時候常常會有業務代碼之間有不少的 JavaBean 之間的相互轉化,好比PO/DTO/VO/QueryParam之間的轉換問題。以前咱們的作法是:git

  • 拷貝技術github

    • org.apache.commons.beanutils.PropertyUtils.copyProperties
    • org.apache.commons.beanutils.BeanUtils.copyProperties
    • org.springframework.beans.BeanUtils.copyProperties
    • net.sf.cglib.beans.BeanCopier
  • 純get/setspring

    • 輔助IDE插件拷貝對象時能夠自動set全部方法字段 (這種方式可能有些開發人員不清楚)
    • 不只看上去冗餘添加新的字段時依然須要手動
    • 開發效率比較低

MapStruct 帶來的改變

MapSturct 是一個生成類型安全, 高性能且無依賴的 JavaBean 映射代碼的註解處理器(annotation processor)。apache

工具能夠幫咱們實現 JavaBean 之間的轉換, 經過註解的方式。json

同時, 做爲一個工具類,相比於手寫, 其應該具備便捷, 不容易出錯的特色。api

MapStruct入門例子

這裏展現最基本的PO轉VO的例子,使用的是IDEA + Lombok + MapStruct緩存

Pom.xml

注意:基於當前IDEA設置並不須要mapstruct-processor的依賴安全

通常來講會加載兩個包:

  • org.mapstruct:mapstruct: 包含Mapstruct核心,好比註解等;若是是mapstruct-jdk8會引入一些jdk8的語言特性;
  • org.mapstruct:mapstruct-processor: 處理註解用的,能夠根據註解自動生成mapstruct的mapperImpl類

以下示例基於IDEA實現,能夠在build階段的annotationProcessorPaths中配置mapstruct-processor的path。

<packaging>jar</packaging> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version> <org.projectlombok.version>1.18.12</org.projectlombok.version> </properties> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <!-- lombok dependencies should not end up on classpath --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${org.projectlombok.version}</version> <scope>provided</scope> </dependency> <!-- fastjson --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.71</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> </dependencies> <build> <pluginManagement> <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> <!-- See https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html --> <!-- Classpath elements to supply as annotation processor path. If specified, the compiler --> <!-- will detect annotation processors only in those classpath elements. If omitted, the --> <!-- default classpath is used to detect annotation processors. The detection itself depends --> <!-- on the configuration of annotationProcessors. --> <!-- --> <!-- According to this documentation, the provided dependency processor is not considered! --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> <path> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${org.projectlombok.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </pluginManagement> </build> 

Entity

這裏面假設基於一些業務需求採用的是MySQL,且將一些擴展的數據放在了config字段中,並以JSON轉String存儲。

@Data @Accessors(chain = true) public class User { private Long id; private String username; private String password; // 密碼 private Integer sex; // 性別 private LocalDate birthday; // 生日 private LocalDateTime createTime; // 建立時間 private String config; // 其餘擴展信息,以JSON格式存儲 } 

VO 類

最後真正展現的應該:

  • 不顯示密碼;
  • 將日期轉換;
  • config要轉成對象的list;
@Data @Accessors(chain = true) public class UserVo { private Long id; private String username; private String password; private Integer gender; private LocalDate birthday; private String createTime; private List<UserConfig> config; @Data public static class UserConfig { private String field1; private Integer field2; } } 

mapper(或者converter)

注意:

  • 這裏沒用@Mappings,且看最後編譯出的類文件,會自動加
  • 密碼須要ignore
@Mapper public interface UserConverter { UserConverter INSTANCE = Mappers.getMapper(UserConverter.class); @Mapping(target = "gender", source = "sex") @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") UserVo do2vo(User var1); @Mapping(target = "sex", source = "gender") @Mapping(target = "password", ignore = true) @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss") User vo2Do(UserVo var1); List<UserVo> do2voList(List<User> userList); default List<UserVo.UserConfig> strConfigToListUserConfig(String config) { return JSON.parseArray(config, UserVo.UserConfig.class); } default String listUserConfigToStrConfig(List<UserVo.UserConfig> list) { return JSON.toJSONString(list); } } 

測試類

@Test public void do2VoTest() { User user = new User() .setId(1L) .setUsername("zhangsan") .setSex(1) .setPassword("abc123") .setCreateTime(LocalDateTime.now()) .setBirthday(LocalDate.of(1999, 9, 27)) .setConfig("[{\"field1\":\"Test Field1\",\"field2\":500}]"); UserVo userVo = UserConverter.INSTANCE.do2vo(user); // asset assertNotNull(userVo); assertEquals(userVo.getId(), user.getId()); // print System.out.println(user); System.out.println(userVo); // User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-08-17T14:54:01.528, config=[{"field1":"Test Field1","field2":500}]) // UserVo(id=1, username=zhangsan, password=abc123, gender=1, birthday=1999-09-27, createTime=2020-08-17 14:54:01, config=[UserVo.UserConfig(field1=Test Field1, field2=500)]) } @Test public void vo2DoTest() { UserVo.UserConfig userConfig = new UserVo.UserConfig(); userConfig.setField1("Test Field1"); userConfig.setField2(500); UserVo userVo = new UserVo() .setId(1L) .setUsername("zhangsan") .setGender(2) .setCreateTime("2020-01-18 15:32:54") .setBirthday(LocalDate.of(1999, 9, 27)) .setConfig(Collections.singletonList(userConfig)); User user = UserConverter.INSTANCE.vo2Do(userVo); // asset assertNotNull(userVo); assertEquals(userVo.getId(), user.getId()); // print System.out.println(user); System.out.println(userVo); } 

MapStrcut實現的原理?

MapStruct 來生成的代碼, 其相似於人手寫。 速度上能夠獲得保證。

前面例子中生成的代碼能夠在編譯後看到, 在 target/generated-sources/annotations 裏能夠看到; 同時真正在代碼包執行的能夠在target/classes包中看到。

編譯後的類

  • 編譯後的class位置

  • 編譯後的內容
public class UserConverterImpl implements UserConverter { @Override public UserVo do2vo(User var1) { if ( var1 == null ) { return null; } UserVo userVo = new UserVo(); userVo.setGender( var1.getSex() ); if ( var1.getCreateTime() != null ) { userVo.setCreateTime( DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ).format( var1.getCreateTime() ) ); } userVo.setId( var1.getId() ); userVo.setUsername( var1.getUsername() ); userVo.setPassword( var1.getPassword() ); userVo.setBirthday( var1.getBirthday() ); userVo.setConfig( strConfigToListUserConfig( var1.getConfig() ) ); return userVo; } @Override public User vo2Do(UserVo var1) { if ( var1 == null ) { return null; } User user = new User(); user.setSex( var1.getGender() ); if ( var1.getCreateTime() != null ) { user.setCreateTime( LocalDateTime.parse( var1.getCreateTime(), DateTimeFormatter.ofPattern( "yyyy-MM-dd HH:mm:ss" ) ) ); } user.setId( var1.getId() ); user.setUsername( var1.getUsername() ); user.setBirthday( var1.getBirthday() ); user.setConfig( listUserConfigToStrConfig( var1.getConfig() ) ); return user; } @Override public List<UserVo> do2voList(List<User> userList) { if ( userList == null ) { return null; } List<UserVo> list = new ArrayList<UserVo>( userList.size() ); for ( User user : userList ) { list.add( do2vo( user ) ); } return list; } } 

這裏面用了什麼機制?

這和Lombok實現機制一致。

核心之處就是對於註解的解析上。JDK5引入了註解的同時,也提供了兩種解析方式。

  • 運行時解析

運行時可以解析的註解,必須將@Retention設置爲RUNTIME, 好比@Retention(RetentionPolicy.RUNTIME),這樣就能夠經過反射拿到該註解。java.lang,reflect反射包中提供了一個接口AnnotatedElement,該接口定義了獲取註解信息的幾個方法,Class、Constructor、Field、Method、Package等都實現了該接口,對反射熟悉的朋友應該都會很熟悉這種解析方式。

  • 編譯時解析

編譯時解析有兩種機制,分別簡單描述下:

1)Annotation Processing Tool

apt自JDK5產生,JDK7已標記爲過時,不推薦使用,JDK8中已完全刪除,自JDK6開始,可使用Pluggable Annotation Processing API來替換它,apt被替換主要有2點緣由:

  • api都在com.sun.mirror非標準包下
  • 沒有集成到javac中,須要額外運行

2)Pluggable Annotation Processing API

JSR 269: Pluggable Annotation Processing API自JDK6加入,做爲apt的替代方案,它解決了apt的兩個問題,javac在執行的時候會調用實現了該API的程序,這樣咱們就能夠對編譯器作一些加強,這時javac執行的過程以下:

Lombok本質上就是一個實現了「JSR 269 API」的程序。在使用javac的過程當中,它產生做用的具體流程以下:

  • javac對源代碼進行分析,生成了一棵抽象語法樹(AST)
  • 運行過程當中調用實現了「JSR 269 API」的Lombok程序
  • 此時Lombok就對第一步驟獲得的AST進行處理,找到@Data註解所在類對應的語法樹(AST),而後修改該語法樹(AST),增長getter和setter方法定義的相應樹節點
  • javac使用修改後的抽象語法樹(AST)生成字節碼文件,即給class增長新的節點(代碼塊)

從上面的Lombok執行的流程圖中能夠看出,在Javac 解析成AST抽象語法樹以後, Lombok 根據本身編寫的註解處理器,動態地修改 AST,增長新的節點(即Lombok自定義註解所須要生成的代碼),最終經過分析生成JVM可執行的字節碼Class文件。使用Annotation Processing自定義註解是在編譯階段進行修改,而JDK的反射技術是在運行時動態修改,二者相比,反射雖然更加靈活一些可是帶來的性能損耗更加大。

MapStruct更多例子

:::tip
通常特性和例子最好直接參考官網例子, 這裏會差別化的體現一些常見的用法。@pdai
:::

自定義屬性的轉化

注意在不一樣的JDK版本中作法不太同樣。@pdai

  • JDK 8以上版本

通常經常使用的類型字段轉換 MapStruct都能替咱們完成,可是有一些是咱們自定義的對象類型,MapStruct就不能進行字段轉換,這就須要咱們編寫對應的類型轉換方法,筆者使用的是JDK8,支持接口中的默認方法,能夠直接在轉換器中添加自定義類型轉換方法。

上述例子中User對象的config屬性是一個JSON字符串,UserVo對象中是List類型的,這須要實現JSON字符串與對象的互轉。

default List<UserConfig> strConfigToListUserConfig(String config) { return JSON.parseArray(config, UserConfig.class); } default String listUserConfigToStrConfig(List<UserConfig> list) { return JSON.toJSONString(list); } 
  • JDK 8 如下版本

若是是 JDK8如下的,不支持默認方法,能夠另外定義一個 轉換器,而後再當前轉換器的 @Mapper 中經過 uses = XXX.class 進行引用。

定義好方法以後,MapStruct當匹配到合適類型的字段時,會調用咱們自定義的轉換方法進行轉換。

轉爲多個對象

好比上面例子中User能夠轉爲UserQueryParam, 業務功能上好比經過UserQueryParam裏面的參數進行查找用戶的。

@Data @Accessors(chain = true) public class UserQueryParam { private Long id; private String username; } 

添加轉換方法

UserQueryParam vo2QueryParam(User var1); 

Spring中使用MapStruct

除了UserConverter.INSTANCE這種方式還能夠注入Spring容器中使用。

  • componentModel

當添加componentModel="spring"時,它會在實現類上自動添加@Component註解,這樣就能被Spring記性component scan,從而加載到springContext中,進而被@Autowird注入使用。(其它還有jsr330cdi標準,基本上使用componentModel="spring"就夠了)。

@Mapper(componentModel="spring") public interface UserConverter { } 
  • 引入和測試
@Slf4j @RunWith(SpringRunner.class) @SpringBootTest public class UserConverterTest { @Resource private UserConverter userConverter; // test methods } 

多個對象轉一個對象

好比上述例子中User購買了東西,須要郵寄到他的地址Address,這時須要展現UserWithAddress的信息:

  • Address
@Data public class Address { private String street; private Integer zipCode; private Integer houseNo; private String description; } 
  • UserWithAddressVo
@Data public class UserWithAddressVo { private String username; private Integer sex; private String street; private Integer zipCode; private Integer houseNumber; private String description; } 
  • converter方法
@Mapping(source = "person.description", target = "description") @Mapping(source = "address.houseNo", target = "houseNumber") UserWithAddressVo userAndAddress2Vo(User user, Address address); 

注意:在多對一轉換時, 遵循如下幾個原則

  • 當多個對象中, 有其中一個爲 null, 則會直接返回 null
  • 如一對一轉換同樣, 屬性經過名字來自動匹配。 所以, 名稱和類型相同的不須要進行特殊處理
  • 當多個原對象中,有相同名字的屬性時,須要經過 @Mapping 註解來具體的指定, 以避免出現歧義(不指定會報錯)。 如上面的 description

屬性也能夠直接從傳入的參數來賦值。

@Mapping(source = "person.description", target = "description") @Mapping(source = "hn", target = "houseNumber") UserWithAddressVo userAndAddressHn2Vo(User user, Integer hn); 

MapStruct再深刻理解

:::tip
在瞭解基本的MapStruct使用以後,咱們將從多個角度來深刻理解MapStruct這個工具。@pdai
:::

IntelliJ IDEA 中對MapStruct的支持如何?

一般來講IDE對於MapStruct這類工具的支持體如今兩方面,一個是Maven的集成,另外一個是編輯時的提示(Hit); 相關的支持能夠參考官網。@pdai

Maven支持

  • 在IntelliJ 2018.1.1以前, 注意在早期的版本中artifactId還須要加jdk版本,好比mapstruct-jdk8
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </dependency> 
  • 在IntelliJ 2018.1.1以後是能夠不添加mapstruct-processor
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version> </properties> <dependencies> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>${org.mapstruct.version}</version> </dependency> </dependencies> <build> <pluginManagement> <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> <!-- See https://maven.apache.org/plugins/maven-compiler-plugin/compile-mojo.html --> <!-- Classpath elements to supply as annotation processor path. If specified, the compiler --> <!-- will detect annotation processors only in those classpath elements. If omitted, the --> <!-- default classpath is used to detect annotation processors. The detection itself depends --> <!-- on the configuration of annotationProcessors. --> <!-- --> <!-- According to this documentation, the provided dependency processor is not considered! --> <annotationProcessorPaths> <path> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>${org.mapstruct.version}</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> </pluginManagement> </build> 

編輯器支持

  • 編輯器支持:自動補全

  • 編輯器支持:鏈接跳轉

  • 編輯器支持:查找使用方式

Eclipse 中對MapStruct的支持如何?

必須保證你使用的Eclipse中包含m2e-apt插件,且儘量的升級這個插件到最新的版本,這個插件主要用於自動應用annotation processor相關的配置。

Maven支持

同時在pom.xml中推薦你加入以下配置, 緣由請看官方給的以下注釋:

<properties> <!-- automatically run annotation processors within the incremental compilation --> <m2e.apt.activation>jdt_apt</m2e.apt.activation> </properties> 

編輯器支持

  • 自動補全

  • 快速修復

與其它屬性拷貝框架性能到底相差多少?

基於咱們對它原理的理解,咱們知道mapstrcut最後執行時依然是get/set,因此性能是比較高的。同時咱們也知道反射優化是能夠解決一部分性能問題的,那麼經過反射方式進行的屬性拷貝和get/set這種性能相差多少呢?

有哪些屬性拷貝方式呢?

綜合咱們前面的文章,經常使用的util包中有以下屬性拷貝類:

  • org.apache.commons.beanutils.PropertyUtils.copyProperties
  • org.apache.commons.beanutils.BeanUtils.copyProperties
  • org.springframework.beans.BeanUtils.copyProperties
  • net.sf.cglib.beans.BeanCopier

使用屬性拷貝和set/get方式性能差別

  • 10000次

  • 1000次

  • 10次

  • 結論
    • property少,寫起來也不麻煩,就直接用傳統的getter/setter,性能最好
    • property多,轉換不頻繁,那就省點事吧,使用org.apache.commons.beanutils.BeanUtils.copyProperties
    • property多,轉換很頻繁,爲性能考慮,使用net.sf.cglib.beans.BeanCopier.BeanCopier,性能近乎getter/setter。可是BeanCopier的建立時消耗較大,因此不要頻繁建立該實體,最好的處理方式是靜態化或者緩存起來。

更多測試對比能夠參考這裏

和MapStruct相似框架的對比?

咱們再看下是否有其它相似的框架呢?這裏主要來源這篇文章

其它相似方案

  • Dozer

Dozer 是一個映射框架,它使用遞歸將數據從一個對象複製到另外一個對象。框架不只可以在 bean 之間複製屬性,還可以在不一樣類型之間自動轉換。

更多關於 Dozer 的內容能夠在官方文檔中找到: http://dozer.sourceforge.net/documentation/gettingstarted.html ,或者你也能夠閱讀這篇文章:https://www.baeldung.com/dozer 。

  • Orika

Orika 是一個 bean 到 bean 的映射框架,它遞歸地將數據從一個對象複製到另外一個對象。

Orika 的工做原理與 Dozer 類似。二者之間的主要區別是 Orika 使用字節碼生成。這容許以最小的開銷生成更快的映射器。

更多關於 Orika 的內容能夠在官方文檔中找到:https://orika-mapper.github.io/orika-docs/,或者你也能夠閱讀這篇文章:https://www.baeldung.com/orika-mapping。

  • ModelMapper

ModelMapper 是一個旨在簡化對象映射的框架,它根據約定肯定對象之間的映射方式。它提供了類型安全的和重構安全的 API。

更多關於 ModelMapper 的內容能夠在官方文檔中找到:http://modelmapper.org/ 。

  • JMapper

JMapper 是一個映射框架,旨在提供易於使用的、高性能的 Java bean 之間的映射。該框架旨在使用註釋和關係映射應用 DRY 原則。該框架容許不一樣的配置方式:基於註釋、XML 或基於 api。

更多關於 JMapper 的內容能夠在官方文檔中找到:https://github.com/jmapper-framework/jmapper-core/wiki。

性能對比

對於性能測試,咱們可使用 Java Microbenchmark Harness,關於如何使用它的更多信息能夠在 這篇文章:https://www.baeldung.com/java-microbenchmark-harness 中找到。

測試結果(某一種)

全部的基準測試都代表,根據場景的不一樣,MapStruct 和 JMapper 都是不錯的選擇,儘管 MapStruct 對 SingleShotTime 給出的結果要差得多。

其它常見問題?

  • 當兩個對象屬性不一致時,好比User對象中某個字段不存在與UserVo當中時,在編譯時會有警告提示,能夠在@Mapping中配置 ignore = true,當字段較多時,能夠直接在@Mapper中設置unmappedTargetPolicy屬性或者unmappedSourcePolicy屬性爲 ReportingPolicy.IGNORE便可。

  • 若是項目中也同時使用到了 Lombok,必定要注意 Lombok的版本要等於或者高於1.18.10,不然會有編譯不經過的狀況發生。

參考文章

  • https://mapstruct.org/documentation

  • https://www.cnblogs.com/zhaoyanghoo/p/5722113.html

  • https://www.baeldung.com/java-performance-mapping-frameworks

  • https://www.cnblogs.com/javaguide/p/11861749.html

相關文章
相關標籤/搜索