業務程式碼的救星——Java 對象轉換框架 MapStruct 妙用
- 2019 年 10 月 3 日
- 筆記
簡介
在業務項目的開發中,我們經常需要將 Java 對象進行轉換,比如從將外部微服務得到的對象轉換為本域的業務對象 domain object,將 domain object 轉為數據持久層的 data object,將 domain object 轉換為 DTO 以便返回給外部調用方等。在轉換時大部分屬性都是相同的,只有少部分的不同,如果手工編寫轉換程式碼,會很繁瑣。這時我們可以通過一些對象轉換框架來更方便的做這件事情。
這樣的對象轉換框架有不少,比較有名的有 ModelMapper 和 MapStruct。它們所使用的實現技術不同,ModelMapper 是基於反射的,通過反射來查找實體對象的欄位,並讀取或寫入值,這樣的方式實現原理簡單,但性能很差。與 ModelMapper 框架不同的是,MapStruct 是基於編譯階段程式碼生成的,生成的轉換程式碼在運行的時候跟一般的程式碼一樣,沒有額外的性能損失。本文重點介紹 MapStruct。
業務場景
假設現在有這麼個場景,從資料庫查詢出來了一個 user 對象(包含 id,用戶名,密碼,手機號,郵箱,角色這些欄位)和一個對應的角色對象 role(包含 id,角色名,角色描述這些欄位),現在在 controller 需要用到 user 對象的 id,用戶名,和角色對象的角色名三個屬性。一種方式是直接把兩個對象傳遞到 controller 層,但是這樣會多出很多沒用的屬性。更通用的方式是需要用到的屬性封裝成一個類(DTO),通過傳輸這個類的實例來完成數據傳輸。
實現方式之使用傳統方式
如下:
User.java
@AllArgsConstructor @Data public class User { private Long id; private String username; private String password; private String phoneNum; private String email; private Role role; } Role.java
@AllArgsConstructor @Data public class Role { private Long id; private String roleName; private String description; } UserRoleDto.java
@Data public class UserRoleDto { /** * 用戶id */ private Long userId; /** * 用戶名 */ private String name; /** * 角色名 */ private String roleName; } MainTest.java
測試類,模擬將 user 對象轉換成 UserRoleDto 對象
public class MainTest { User user = null; /** * 模擬從資料庫中查出 user 對象 */ @Before public void before() { Role role = new Role(2L, "administrator", "超級管理員"); user = new User(1L, "zhangsan", "12345", "17677778888", "[email protected]", role); } /** * 模擬把 user 對象轉換成 UserRoleDto 對象 */ @Test public void test1() { UserRoleDto userRoleDto = new UserRoleDto(); userRoleDto.setUserId(user.getId()); userRoleDto.setName(user.getUsername()); userRoleDto.setRoleName(user.getRole().getRoleName()); System.out.println(userRoleDto); } } 運行結果

上邊的程式碼或許暫時看起來還是比較簡潔的,但是我們需要注意的一點就是平時業務開發中的對象屬性遠不是上述程式碼中簡簡單單的幾個欄位,有可能會有數十個欄位,同理也會數十個對象需要轉換,我們如果還是通過 getter、setter 的方式把一個對象屬性值複製到另一個對象中去還是非常麻煩的,不過不用擔心,今天要介紹給大家的 MapStruct 就是用於解決這種問題的。
實現方式之使用 MapStruct
這裡我們沿用上述程式碼中的基本對象 User.java、Role.java、UserRoleDto.java。
然後新建一個 UserRoleMapper.java,這個來用來定義 User.java、Role.java和UserRoleDto.java之間屬性對應規則。
在這之前我們需要引入 MapStruct 的 pom 引用:
<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-jdk8</artifactId> <version>1.3.0.Final</version> </dependency> UserRoleMapper.java
import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.Mappings; import org.mapstruct.factory.Mappers; /** * @Mapper 定義這是一個MapStruct對象屬性轉換介面,在這個類裡面規定轉換規則 * 在項目構建時,會自動生成改介面的實現類,這個實現類將實現對象屬性值複製 */ @Mapper public interface UserRoleMapper { /** * 獲取該類自動生成的實現類的實例 * 介面中的屬性都是 public static final 的 * 方法都是public abstract 的 */ UserRoleMapper INSTANCES = Mappers.getMapper(UserRoleMapper.class); /** * 這個方法就是用於實現對象屬性複製的方法 * * @Mapping 用來定義屬性複製規則 * source 指定源對象屬性 * target 指定目標對象屬性 * * @param user 這個參數就是源對象,也就是需要被複制的對象 * @return 返回的是目標對象,就是最終的結果對象 */ @Mappings({ @Mapping(source = "id", target = "userId"), @Mapping(source = "username", target = "name"), @Mapping(source = "role.roleName", target = "roleName") }) UserRoleDto toUserRoleDto(User user); } 測試一下結果
MainTest.java
/** * 模擬通過MapStruct把user對象轉換成UserRoleDto對象 */ @Test public void test2() { UserRoleDto userRoleDto = UserRoleMapper.INSTANCES.toUserRoleDto(user); System.out.println(userRoleDto); } 呃,很明顯,運行竟然報錯了,具體異常如下:

核心是這一句 :java.lang.ClassNotFoundException: Cannot find implementation for top.zhoudl.mapstruct.UserRoleMapper ,也就是說沒有找到 UserRoleMapper 類的實現類。
通過查閱一些資料可得:
MapStruct 是一個可以處理註解的Java編譯器插件,可以在命令行中使用,也可以在 IDE 中使用。MapStruc t有一些默認配置,但是也為用戶提供了自己進行配置的途徑。缺點就是這玩意在使用工具自帶的編譯器時不會生成實現類,需要通過 maven 的方式來進行編譯,然後才會生成實現類。
所以我們需要增加一個編譯插件到 pom 文件中:
<!-- 引入 processor --> <dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct-processor</artifactId> <version>1.3.0.Final</version> <scope>provided</scope> </dependency> <!--為 Maven compile plugin 設置 annotation processor --> <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>1.2.0.Final</version> </path> </annotationProcessorPaths> </configuration> </plugin> </plugins> 然後我們運行程式就可以得到自己想要的結果了

安裝 MapStruct 插件
使用 MapStruct,還有一個缺點就是,當屬性改名的時候,因為在 Mapper 上註解中配置的名字是在字元串裡面,因此不會自動同步的。所以 MapStruct 提供了一個插件來解決這個問題,同時還提供程式碼自動提示、點擊跳轉到實現等功能。
關於插件的更多資訊,參見 MapStruct support for IntelliJ IDEA
安裝插件的過程
在 IDEA 中依次打開 File - > Settings - > Plugins
然後在 Markeyplace 搜索框中輸入 mapstruct,點擊 install,然後重啟 IDE 即可。

一些可能會出現的問題
找不到注釋處理程式:在 pom.xml 中增加 mapstruct-processor 的依賴
沒有找到實現類:在 pom.xml 中加入對 mapstruct-processor 的依賴
在 IDEA 裡面 enable Annotation Processor
使用 Lombok 的情況下,編譯時報 Data 類的 setter/getter 找不到:把 lombok 加入到annotationProcessorPath,如下圖

總結
MapSturct 是一個生成類型安全, 高性能且無依賴的 JavaBean 映射程式碼的註解處理器(annotation processor)。
作為一個註解處理器, 通過MapStruct 生成的程式碼具有怎麼樣的優勢呢?抓一下重點:
- 註解處理器
- 可以生成
JavaBean之間的映射程式碼 - 類型安全, 高性能, 無依賴性
高性能
這是相對反射來說的, 反射需要去讀取位元組碼的內容, 花銷會比較大。 而通過 MapStruct 來生成的程式碼, 其類似於人手寫,程式碼執行速度上可以得到保證。(前面例子中生成的程式碼可以在編譯後看到,在項目的 target/generated-sources/annotations 目錄里可以看到具體程式碼)。
易於 debug
在我們生成的程式碼中, 我們可以輕易的進行 debug。但是如果是使用反射實現程式碼的時候, 一旦出現了問題, 很多時候是比較難找到原因。
使用相對簡單
如果是完全映射的, 使用起來肯定沒有反射簡單。 用類似 BeanUtils 這些工具一條語句就搞定了。 但是,如果需要進行特殊的匹配(特殊類型轉換, 多對一轉換等), MapStruct 的優勢就比較明顯了,基本上我們只需要在使用的時候聲明一個介面, 介面下寫對應的方法, 就可以使用了(當然, 如果有特殊情況, 是需要額處理一下的)。
程式碼獨立
生成的程式碼是對立的, 沒有運行時的依賴
本文首發於微信公眾號 【程式猿雜貨鋪】,關注公眾號,獲取更多精彩文章!
