mapstruct最佳實踐

来源:https://www.cnblogs.com/vcmq/archive/2020/03/21/12542336.html
-Advertisement-
Play Games

"本文原文鏈接地址:http://nullpointer.pw/mapstruct%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html" 前言 按照日常開發習慣,對於不同領域層使用不同JavaBean對象傳輸數據,避免相互影響,因此基於資料庫實體對象User衍生出比如U ...


本文原文鏈接地址:http://nullpointer.pw/mapstruct%E6%9C%80%E4%BD%B3%E5%AE%9E%E8%B7%B5.html

前言

按照日常開發習慣,對於不同領域層使用不同JavaBean對象傳輸數據,避免相互影響,因此基於資料庫實體對象User衍生出比如UserDto、UserVo等對象,於是在不同層之間進行數據傳輸時,不可避免地需要將這些對象進行互相轉換操作。

常見的轉換方式有:

  • 調用getter/setter方法進行屬性賦值
  • 調用BeanUtil.copyPropertie進行反射屬性賦值

第一種方式不必說,屬性多了就需要寫一大坨getter/setter代碼。第二種方式比第一種方式要簡便很多,但是坑巨多,比如sources與target寫反,難以定位某個欄位在哪裡進行的賦值,同時因為用到反射,導致性能也不佳。

鑒於此,今天寫一寫第三種對象轉換方式,本文使用的是 MapStruct 工具進行轉換,MapStruct 原理也很簡單,就是在代碼編譯階段生成對應的賦值代碼,底層原理還是調用getter/setter方法,但是這是由工具替我們完成,MapStruct在不影響性能的情況下,解決了前面兩種方式弊端,很贊~

準備工作

為了講解 MapStruct 工具的使用,本文使用常見的 User 類以及對應 UserDto 對象來演示。

@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格式存儲
  	private String test; // 測試欄位
}

@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;
	  private String test; // 測試欄位
    @Data
    public static class UserConfig {
        private String field1;
        private Integer field2;
    }
}

註意觀察這兩個類的區別。

一、MapStruct 配置以及基礎使用

項目中引入 MapStruct 的依賴

<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct</artifactId>
  <version>1.3.1.Final</version>
</dependency>
<dependency>
  <groupId>org.mapstruct</groupId>
  <artifactId>mapstruct-processor</artifactId>
  <version>1.3.1.Final</version>
</dependency>

因為項目中的對象轉換操作基本都一樣,因此抽取除了一個轉換基類,不同對象如果只是簡單轉換可以直接繼承該基類,而無需覆寫基類任何方法,即只需要一個空類即可。如果子類覆寫了基類的方法,則基類上的 @Mapping 會失效。

@MapperConfig
public interface BaseMapping<SOURCE, TARGET> {

    /**
     * 映射同名屬性
     */
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    TARGET sourceToTarget(SOURCE var1);

    /**
     * 反向,映射同名屬性
     */
    @InheritInverseConfiguration(name = "sourceToTarget")
    SOURCE targetToSource(TARGET var1);

    /**
     * 映射同名屬性,集合形式
     */
    @InheritConfiguration(name = "sourceToTarget")
    List<TARGET> sourceToTarget(List<SOURCE> var1);

    /**
     * 反向,映射同名屬性,集合形式
     */
    @InheritConfiguration(name = "targetToSource")
    List<SOURCE> targetToSource(List<TARGET> var1);

    /**
     * 映射同名屬性,集合流形式
     */
    List<TARGET> sourceToTarget(Stream<SOURCE> stream);

    /**
     * 反向,映射同名屬性,集合流形式
     */
    List<SOURCE> targetToSource(Stream<TARGET> stream);
}

實現 User 與 UserVo 對象的轉換器

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper(componentModel = "spring")
public interface UserMapping extends BaseMapping<User, UserVo> {

    @Mapping(target = "gender", source = "sex")
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Override
    UserVo sourceToTarget(User var1);

    @Mapping(target = "sex", source = "gender")
    @Mapping(target = "password", ignore = true)
    @Mapping(target = "createTime", dateFormat = "yyyy-MM-dd HH:mm:ss")
    @Override
    User targetToSource(UserVo var1);

    default List<UserConfig> strConfigToListUserConfig(String config) {
        return JSON.parseArray(config, UserConfig.class);
    }

    default String listUserConfigToStrConfig(List<UserConfig> list) {
        return JSON.toJSONString(list);
    }
}

本文示例使用的是 Spring 的方式,@Mapper 註解的 componentModel 屬性值為 spring,不過應該大多數都用的此模式進行開發。

@Mapping用於配置對象的映射關係,示例中 User 對象性別屬性名為 sex,而UserVo對象性別屬性名為gender,因此需要配置 target 與 source 屬性。

password 欄位不應該返回到前臺,可以採取兩種方式不進行轉換,第一種就是在vo對象中不出現password欄位,第二種就是在@Mapping中設置該欄位 ignore = true。

MapStruct 提供了時間格式化的屬性 dataFormat,支持Date、LocalDate、LocalDateTime等時間類型與String的轉換。示例中birthday 屬性為 LocalDate 類型,可以無需指定dataFormat自動完成轉換,而LocalDateTime類型預設使用的是ISO格式時間,在國內往往不符合需求,因此需要手動指定一下 dataFormat。

二、自定義屬性類型轉換方法

一般常用的類型欄位轉換 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當匹配到合適類型的欄位時,會調用我們自定義的轉換方法進行轉換。

三、單元測試

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class MapStructTest {

  @Resource
  private UserMapping userMapping;

  @Test
  public void tetDomain2DTO() {
    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 = userMapping.sourceToTarget(user);
    log.info("User: {}", user);
    //        User: User(id=1, username=zhangsan, password=abc123, sex=1, birthday=1999-09-27, createTime=2020-01-17T17:46:20.316, config=[{"field1":"Test Field1","field2":500}])
    log.info("UserVo: {}", userVo);
    //        UserVo: UserVo(id=1, username=zhangsan, gender=1, birthday=1999-09-27, createTime=2020-01-17 17:46:20, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
  }

  @Test
  public void testDTO2Domain() {
    UserConfig userConfig = new 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 = userMapping.targetToSource(userVo);
    log.info("UserVo: {}", userVo);
    //        UserVo: UserVo(id=1, username=zhangsan, gender=2, birthday=1999-09-27, createTime=2020-01-18 15:32:54, config=[UserVo.UserConfig(field1=Test Field1, field2=500)])
    log.info("User: {}", user);
    //        User: User(id=1, username=zhangsan, password=null, sex=2, birthday=1999-09-27, createTime=2020-01-18T15:32:54, config=[{"field1":"Test Field1","field2":500}])
  }

四、常見問題

  1. 當兩個對象屬性不一致時,比如User對象中某個欄位不存在與UserVo當中時,在編譯時會有警告提示,可以在@Mapping中配置 ignore = true,當欄位較多時,可以直接在@Mapper中設置unmappedTargetPolicy屬性或者unmappedSourcePolicy屬性為 ReportingPolicy.IGNORE即可。
  2. 如果項目中也同時使用到了 Lombok,一定要註意 Lombok的版本要等於或者高於1.18.10,否則會有編譯不通過的情況發生,筆者掉進這個坑很久才爬了出來,希望各位不要重覆踩坑。

代碼下載

本文涉及代碼已上傳到 Github,以供參考。

參考


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 【原創】Java併發編程系列2:線程概念與基礎操作 偉大的理想只有經過忘我的鬥爭和犧牲才能勝利實現。 本篇為【Dali王的技術博客】Java併發編程系列第二篇,講講有關線程的那些事兒。主要內容是如下這些: 線程概念 線程基礎操作 線程概念 進程代表了運行中的程式,一個運行的Java程式就是一個進程。 ...
  • BigDecimal介紹及BigDecimal實現四捨五入 1. BigDecimal是什麼? 我們知道float最大精度是7 8位有效數字,而double的最大精度是16 17位有效數字,那麼大於16位的我們怎麼來表示呢?這就需要用到BigDecimal,用來對超過16位有效位的數進行精確的運算。 ...
  • 100個不同類型的python語言趣味編程題 在求解的過程中培養編程興趣,拓展編程思維,提高編程能力。 第一部分:趣味演算法入門;第八題: 8.冒泡排序:對N個整數(數據由鍵盤輸入)進行升序排序 冒泡排序的思想:首先從表頭開始往後掃描數組,在掃描的過程中逐對比較相鄰的倆個元素的大小。若相鄰的兩個元素中 ...
  • 1 引子 Fork/Join框架是從Java1.7開始提供的一個並行處理任務的框架(本篇博客基於JDK1.8分析),它的基本思路是將一個大任務分解成若幹個小任務,並行處理多個小任務,最後再彙總合併這些小任務的結果便可得到原來的大任務結果。 從字面意思來理解Fork/Join框架,"Fork"表示“分 ...
  • JDK8提供的Stream雖然好用,Lambda雖然簡潔,但一定不能 濫用 ,我舉一個實際遇到的例子(已做脫敏處理): 試問誰能看得懂?難道是沒有換行格式化? 換行格式化後,前面的流操作還能勉強讀懂,遇到最後的lambda表達式實在沒辦法讀下去了,根本不知道他想表達什麼意思。 但是,如果我們真正遇到 ...
  • 在家實在閑的沒事兒乾,翻出來了大三上學期的EDA課的小實驗,也就是設計一個二愣子交通燈啦,只會自己按設定好的時間閃,紅燈、綠燈,黃燈和轉向燈; 各燈顯示時長:哎呀~ 懶得寫了,後面程式里都有。 晶元:FPGA、Cylone IV E 系列的 EP4CE6E22C8,144引腳。 外置時鐘:1Hz 以 ...
  • Android車載地圖測試,涉及:高德地圖100m比例尺下,拖動地圖進行移圖操作2個小時, 預期結果:移圖正常,地圖渲染正常,不會出現卡死卡滯界面異常等情況。 準備階段 1. 在高德地圖App界面,調整比例尺到100m 2. adb shell input swipe x1 y1 x2 y2 , 可 ...
  • 根據老師講的思路寫的,沒有百度,所以也不知道完不完全正確,但目前測試都還好,都可以正常排序。 1 #include <iostream> 2 using namespace std; 3 4 void quickSort(double *q ,int n) //一個double型數組還有一個代表這個 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...