業務代碼的救星——Java 對象轉換框架 MapStruct 妙用

簡介

在業務項目的開發中,我們經常需要將 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.javaRole.javaUserRoleDto.java
然後新建一個 UserRoleMapper.java,這個來用來定義 User.javaRole.javaUserRoleDto.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 生成的代碼具有怎麼樣的優勢呢?抓一下重點:

  1. 註解處理器
  2. 可以生成 JavaBean 之間的映射代碼
  3. 類型安全, 高性能, 無依賴性

高性能

這是相對反射來說的, 反射需要去讀取字節碼的內容, 花銷會比較大。 而通過 MapStruct 來生成的代碼, 其類似於人手寫,代碼執行速度上可以得到保證。(前面例子中生成的代碼可以在編譯後看到,在項目的 target/generated-sources/annotations 目錄裏可以看到具體代碼)。

易於 debug

在我們生成的代碼中, 我們可以輕易的進行 debug。但是如果是使用反射實現代碼的時候, 一旦出現了問題, 很多時候是比較難找到原因。

使用相對簡單

如果是完全映射的, 使用起來肯定沒有反射簡單。 用類似 BeanUtils 這些工具一條語句就搞定了。 但是,如果需要進行特殊的匹配(特殊類型轉換, 多對一轉換等), MapStruct 的優勢就比較明顯了,基本上我們只需要在使用的時候聲明一個接口, 接口下寫對應的方法, 就可以使用了(當然, 如果有特殊情況, 是需要額處理一下的)。

代碼獨立

生成的代碼是對立的, 沒有運行時的依賴


本文首發於微信公衆號 【程序猿雜貨鋪】,關注公衆號,獲取更多精彩文章!

歡迎關注我的公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章