高效、優雅的物件copy之MapStruct入門到精通,實戰踩坑版

小王寫部落格發表於2023-02-14

一、前言

大家在開發中,最讓人頭疼的就是:物件之間的複製,前端的VO和資料庫的Entity不一致!

效能最好的就是手動set,主要是枯燥且無技術含量,不僅耗費大量時間而且很容易出錯;

所以我們要成為優秀的程式設計師,要多借助輪子,開發效率事半功倍,開發技能也是增長不少!

如果系統效能沒有要求,怎麼實現都是好的,但是我們要有追求哈,追求高質量!

每個東西都有存在的價值,不要捧一踩一哈!

二、MapStruct簡介

MapStruct是基於JSR 269的Java註釋處理器,用於生成型別安全的 Bean 對映類。

您所要做的就是定義一個對映器介面,該介面宣告任何所需的對映方法。在編譯過程中,MapStruct將生成此介面的實現。此實現使用純 Java 方法呼叫在源物件和目標物件之間進行對映,即無反射或類似內容。

與手動編寫對映程式碼相比,MapStruct透過生成繁瑣且容易出錯的程式碼來節省時間。遵循配置方法的約定,MapStruct使用合理的預設值,但在配置或實現特殊行為時會步入歧途。

在這裡插入圖片描述

三、優勢

與動態對映框架相比,MapStruct具有以下優點:

  • 透過使用普通方法呼叫而不是反射快速執行

  • 編譯時型別安全:只能對映彼此對映的物件和屬性,不會意外地將訂單實體對映到客戶 DTO 等。

  • 在構建時清除錯誤報告,如果

    • 對映不完整(並非所有目標屬性都已對映)

    • 對映不正確(找不到正確的對映方法或型別轉換)

效能圖大家可以看一下:
在這裡插入圖片描述

四、整合實戰

0. 使用

@Mapper將介面標記為對映介面
對於源物件和目標物件中具有不同名稱的屬性,可以使用註釋來配置名稱:@Mapping
按照約定,介面宣告一個成員Mappers INSTANCE,為客戶端提供對對映器實現的訪問。
下面我們來具體使用!

1. 匯入依賴

這裡使用最新的,如果引入了lombok可能會有問題,就是他們倆都是在編譯期執行的,mapstruct如果比lombok先執行,就會找不到get、set方法,所以會有問題,官網已經有了解決方案!現在是啟動不會報錯!

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.3.Final</version>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.24</version>
</dependency>

2. 錯誤總結

  1. 不會自動生成impl實現類?

我們需要加上依賴:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct-processor</artifactId>
    <version>1.5.3.Final</version>
</dependency>
  1. 重新啟動就會出現和lombok的衝突問題:
java: No property named "name" exists in source parameter(s).
 Type "UserVO" has no properties.

官網解決文章地址

<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>
                  <annotationProcessorPaths>
                      <path>
                          <groupId>org.mapstruct</groupId>
                          <artifactId>mapstruct-processor</artifactId>
                          <version>1.5.3.Final</version>
                      </path>
                      <path>
                          <groupId>org.projectlombok</groupId>
                          <artifactId>lombok</artifactId>
                          <version>1.18.24</version>
                      </path>
                      <path>
                          <groupId>org.projectlombok</groupId>
                          <artifactId>lombok-mapstruct-binding</artifactId>
                          <version>0.2.0</version>
                      </path>
                  </annotationProcessorPaths>
              </configuration>
          </plugin>
      </plugins>
  </pluginManagement>
</build>

3. 常用實戰1

使用者表:

@Data
public class User {
    
    private Integer id;
    
    private String username;
    
    private Integer age;
}

前端使用者VO:

@Data
public class UserVO {

    private Integer id;

    private String name;

    private Integer age;
}

我們建立介面進行兩個物件之間的對映:

import com.example.demo.mapstruct.entity.User;
import com.example.demo.mapstruct.entity.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
 * @author wangzhenjun
 * @date 2023/1/28 16:05
 */
@Mapper
public interface UserMapper {

    UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);

    @Mapping(source ="name",target = "username")
    User userVOToUser(UserVO userVO);

}

屬性多了可以巢狀:

@Mappings({
        @Mapping(source ="name",target = "username"),
        @Mapping(source ="name1",target = "username1")
})

也可以:

@Mapping(source ="name",target = "username")
@Mapping(source ="name1",target = "username1")

編寫測試類:

@SpringBootTest
class DemoApplicationTests {

    @Test
    void demoMapstruct(){
        UserVO userVO = new UserVO(1,"小紅",18);
        User user = UserMapper.INSTANCE.userVOToUser(userVO);
        System.out.println(user);

    }

}

在這裡插入圖片描述
我們看到複製沒有任何問題!

我們看看是怎麼實現的:

mapstruct會在編譯期自動生成實現類去幫助我們去賦值,不指定位預設策略,名稱一致進行copy!
不一致可以按上面的進行指定,不指定則不會有set方法!

在這裡插入圖片描述

4. 常用實戰2

下面進行多個源引數的對映方法演示:

我們把user類加上一個欄位:

private BigDecimal score;

新增加一個Score類:

@Data
@AllArgsConstructor
public class Score {

    private Integer studentId;

    private BigDecimal score;
}

調整上面的UserMapper介面:

@Mappings({
       @Mapping(source ="userVO.name",target = "username"),
       @Mapping(source ="score.score",target = "score")
})
User userVOToUser(UserVO userVO, Score score);

測試程式碼:

UserVO userVO = new UserVO(1,"小紅",18);
Score score = new Score(1,new BigDecimal(100));
User user = UserMapper.INSTANCE.userVOToUser(userVO,score);
System.out.println(user);

結果顯示正常:

在這裡插入圖片描述

5. 常用實戰3

我們在來看一個企業級能夠用得上的,就是自定義方法,然後進行賦值:

場景:一個商品有長寬高,我們把長寬高從cm變為m!

還有很多String轉Integer、Float等等,都是按照下面這種自定義方法去實現!

VO和物件都是一樣的哈!

@Data
@AllArgsConstructor
public class ProductVO {

    private Integer id;

    private String name;

    private BigDecimal length;

    private BigDecimal width;

    private BigDecimal high;

}

看清楚,別導錯包了!
qualifiedByName:指定自定義方法的名稱
@Named("cmToM"):起別名,不使用找不到方法

可以寫一起,也可以整一個工具類裡寫方法,在這裡進行引用!
如果是使用spring,我們可以把介面作為bean進行注入呼叫(推薦
加上引數即可開啟:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)

/**
 * @author wangzhenjun
 * @date 2023/1/28 17:13
 */
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ProductMapper {

    @Mapping(source = "length",target = "length",qualifiedByName = "cmToM")
    @Mapping(source = "width",target = "width",qualifiedByName = "cmToM")
    @Mapping(source = "high",target = "high",qualifiedByName = "cmToM")
    Product productVOToPrduct(ProductVO productVO);

    @Named("cmToM")
    default BigDecimal cmToM (BigDecimal oldValue){
        if (oldValue == null) {
            return BigDecimal.ZERO;
        }
        return oldValue.divide(new BigDecimal("100"));
    }
}

測試:

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    private ProductMapper productMapper;

    @Test
    void demoMapstruct(){

        ProductVO productVO = new ProductVO(1,"美麗家園地板",new BigDecimal(100),new BigDecimal(50),new BigDecimal(8));
        Product product = productMapper.productVOToProduct(productVO);

        System.out.println(product);
    }

}

完美轉化!

在這裡插入圖片描述

6. 常用實戰4

在實戰一個LocalDateTime、String相互轉化的,後面大家可以去官網文件去找你需要的:

在剛剛的商品類加個欄位:

private String createdAt;

VO裡也加上一個:

private LocalDateTime createdAt;

編寫個轉化類:
這裡交給spring管理了,因為ProductMapper也交給spring管理,不加的話會找不到此類!

@Component
public class LocalDateTimeMapper {

    public String asString(LocalDateTime date) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return date != null ? date.format(formatter): null;
    }

    public LocalDateTime asLocalDateTime(String date) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        return date != null ? LocalDateTime.parse(date,formatter) : null;
    }
}

ProductMapper修改一下:

uses = LocalDateTimeMapper.class使用我們們上面寫的類即可!

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING,uses = LocalDateTimeMapper.class)
public interface ProductMapper {

    @Mapping(source = "length",target = "length",qualifiedByName = "cmToM")
    @Mapping(source = "width",target = "width",qualifiedByName = "cmToM")
    @Mapping(source = "high",target = "high",qualifiedByName = "cmToM")
    Product productVOToProduct(ProductVO productVO);

    @Named("cmToM")
    default BigDecimal cmToM (BigDecimal oldValue){
        if (oldValue == null) {
            return BigDecimal.ZERO;
        }
        return oldValue.divide(new BigDecimal("100"));
    }
}

測試一下:

ProductVO productVO = new ProductVO(1,"美麗家園地板",
new BigDecimal(100),new BigDecimal(50),
new BigDecimal(8), LocalDateTime.now());
Product product = productMapper.productVOToProduct(productVO);
System.out.println(product);

完美轉化:

在這裡插入圖片描述

六、總結

透過簡介到實戰,這時我們們就是優雅的程式設計師了!

更多的例子可以去官網進行檢視:

mapstruct開發文件

寫作不易,大家給點支援,你的支援是我寫作的動力哈!

對你有幫助,還請不要吝嗇你的發財小手點點關注哈!

關注小編的微信公眾號,一起交流學習!文章首發看哦!

相關文章