MapStruct是一款很是實用Java工具,主要用於解決對象之間的拷貝問題,好比PO/DTO/VO/QueryParam之間的轉換問題。區別於BeanUtils這種經過反射,它經過編譯器編譯生成常規方法,將能夠很大程度上提高效率。@pdaihtml
首先看下這類工具出現的背景。@pdaijava
在開發的時候常常會有業務代碼之間有不少的 JavaBean 之間的相互轉化,好比PO/DTO/VO/QueryParam之間的轉換問題。以前咱們的作法是:git
拷貝技術github
純get/setspring
MapSturct 是一個生成類型安全, 高性能且無依賴的 JavaBean 映射代碼的註解處理器(annotation processor)。apache
工具能夠幫咱們實現 JavaBean 之間的轉換, 經過註解的方式。json
同時, 做爲一個工具類,相比於手寫, 其應該具備便捷, 不容易出錯的特色。api
這裏展現最基本的PO轉VO的例子,使用的是IDEA + Lombok + MapStruct緩存
注意:基於當前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>
這裏面假設基於一些業務需求採用的是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格式存儲 }
最後真正展現的應該:
@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 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); }
MapStruct 來生成的代碼, 其相似於人手寫。 速度上能夠獲得保證。
前面例子中生成的代碼能夠在編譯後看到, 在 target/generated-sources/annotations 裏能夠看到; 同時真正在代碼包執行的能夠在target/classes包中看到。
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點緣由:
2)Pluggable Annotation Processing API
JSR 269: Pluggable Annotation Processing API自JDK6加入,做爲apt的替代方案,它解決了apt的兩個問題,javac在執行的時候會調用實現了該API的程序,這樣咱們就能夠對編譯器作一些加強,這時javac執行的過程以下:
Lombok本質上就是一個實現了「JSR 269 API」的程序。在使用javac的過程當中,它產生做用的具體流程以下:
從上面的Lombok執行的流程圖中能夠看出,在Javac 解析成AST抽象語法樹以後, Lombok 根據本身編寫的註解處理器,動態地修改 AST,增長新的節點(即Lombok自定義註解所須要生成的代碼),最終經過分析生成JVM可執行的字節碼Class文件。使用Annotation Processing自定義註解是在編譯階段進行修改,而JDK的反射技術是在運行時動態修改,二者相比,反射雖然更加靈活一些可是帶來的性能損耗更加大。
:::tip
通常特性和例子最好直接參考官網例子, 這裏會差別化的體現一些常見的用法。@pdai
:::
注意在不一樣的JDK版本中作法不太同樣。@pdai
通常經常使用的類型字段轉換 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); }
若是是 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);
除了UserConverter.INSTANCE這種方式還能夠注入Spring容器中使用。
當添加componentModel="spring"
時,它會在實現類上自動添加@Component
註解,這樣就能被Spring記性component scan,從而加載到springContext中,進而被@Autowird
注入使用。(其它還有jsr330
和cdi
標準,基本上使用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的信息:
@Data public class Address { private String street; private Integer zipCode; private Integer houseNo; private String description; }
@Data public class UserWithAddressVo { private String username; private Integer sex; private String street; private Integer zipCode; private Integer houseNumber; private String description; }
@Mapping(source = "person.description", target = "description") @Mapping(source = "address.houseNo", target = "houseNumber") UserWithAddressVo userAndAddress2Vo(User user, Address address);
注意:在多對一轉換時, 遵循如下幾個原則
屬性也能夠直接從傳入的參數來賦值。
@Mapping(source = "person.description", target = "description") @Mapping(source = "hn", target = "houseNumber") UserWithAddressVo userAndAddressHn2Vo(User user, Integer hn);
:::tip
在瞭解基本的MapStruct使用以後,咱們將從多個角度來深刻理解MapStruct這個工具。@pdai
:::
一般來講IDE對於MapStruct這類工具的支持體如今兩方面,一個是Maven的集成,另外一個是編輯時的提示(Hit); 相關的支持能夠參考官網。@pdai
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>
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中包含
m2e-apt
插件,且儘量的升級這個插件到最新的版本,這個插件主要用於自動應用annotation processor
相關的配置。
同時在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包中有以下屬性拷貝類:
更多測試對比能夠參考這裏
咱們再看下是否有其它相似的框架呢?這裏主要來源這篇文章
Dozer 是一個映射框架,它使用遞歸將數據從一個對象複製到另外一個對象。框架不只可以在 bean 之間複製屬性,還可以在不一樣類型之間自動轉換。
更多關於 Dozer 的內容能夠在官方文檔中找到: http://dozer.sourceforge.net/documentation/gettingstarted.html ,或者你也能夠閱讀這篇文章:https://www.baeldung.com/dozer 。
Orika 是一個 bean 到 bean 的映射框架,它遞歸地將數據從一個對象複製到另外一個對象。
Orika 的工做原理與 Dozer 類似。二者之間的主要區別是 Orika 使用字節碼生成。這容許以最小的開銷生成更快的映射器。
更多關於 Orika 的內容能夠在官方文檔中找到:https://orika-mapper.github.io/orika-docs/,或者你也能夠閱讀這篇文章:https://www.baeldung.com/orika-mapping。
ModelMapper 是一個旨在簡化對象映射的框架,它根據約定肯定對象之間的映射方式。它提供了類型安全的和重構安全的 API。
更多關於 ModelMapper 的內容能夠在官方文檔中找到:http://modelmapper.org/ 。
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,不然會有編譯不經過的狀況發生。
更多文章請參考 Java 全棧知識體系