【程式碼優化】Bean對映之MapStruct

風塵部落格發表於2022-01-14

【程式碼優化】Bean對映之MapStruct

一、背景

領域模型相互轉換就只能靠手工的 get()/set()

普遍的做法有以下幾種:

  1. 手工 get()/set()
  2. 構造器;
  3. BeanUtils 工具類(ApacheSpring 都包含該工具類,使用方式稍稍不同);
  4. Builder 模式。

這些方式都存在一些缺點:耦合性強,手工 get()/set() 經常丟引數,或者搞錯引數值....

本文推薦一種效率較高的方式:MapStruct

二、理論基礎

MapStruct 是一個自動生成 Bean 對映類的程式碼生成器MapStruct 還能夠在不同的資料型別之間進行轉換。

2.1 pom.xml

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.4.2.Final</version>
</dependency>

2.2 註解關鍵詞

  • @Mapper:只有在介面加上這個註解, MapStruct 才會去實現該介面;
  • @Mapping:屬性對映,若源物件屬性與目標物件名字一致,會自動對映對應屬性:
    1. source:源屬性;
    2. target:目標屬性;
    3. dateFormat:字串與日期之間相互轉換;
    4. ignore: 某個屬性不想對映,可以加上 ignore=true
    5. expression:自定義指定的對映方法;
  • @Mappings:配置多個@Mapping
  • @MappingTarget:對映到現有示例。

2.3 工作原理

我們要做的就是定義一個 mapper 介面,該介面宣告任何所需的對映方法。在編譯期間,MapStruct 將生成此介面的實現。此實現使用普通的Java方法呼叫來在源物件和目標物件之間進行對映。

三、MapStruct 實踐

3.1 基本準備

  • 新增三個資料庫 DO 類:

使用者資訊:

@Data
public class UserInfoDO {

    private Long id;

    private String userName;

    private String password;

    private String phoneNum;

    private Date gmtBroth;

    private RoleDO role;

    public UserInfoDO() {

    }

    public UserInfoDO(RoleDO role,Long id,String userName,String password,String phoneNum,Date gmtBroth) {
        this.role = role;
        this.id = id;
        this.userName = userName;
        this.password = password;
        this.phoneNum = phoneNum;
        this.gmtBroth = gmtBroth;
    }
}

使用者補充資訊:

@Data
public class UserExtInfoDO {

    private String favorite;

    public UserExtInfoDO() {

    }

    public UserExtInfoDO(String favorite) {
        this.favorite = favorite;
    }
}

角色資訊:

@Data
public class RoleDO {

    private Long id;
    private String roleName;
    private String description;

    public RoleDO() {

    }

    public RoleDO(Long id, String roleName, String description) {
        this.id = id;
        this.roleName = roleName;
        this.description = description;
    }
}
  • 新增一個資料傳輸 DTO 類:
@Data
public class UserInfoDTO {
    /**
     * 使用者id
     */
    private Long userId;
    /**
     * 使用者名稱
     */
    private String userName;
    /**
     * 使用者名稱
     */
    private String password;

    /**
     * 生日
     */
    private String brothStr;

    /**
     * 手機號
     */
    private String phoneNum;

    /**
     * 角色名
     */
    private String roleName;

    /**
     * 喜好
     */
    private String favorite;

}
  • 新增一個加密工具類
public class Base64Util {

    public static String encode(String str) {
        BASE64Encoder encoder = new BASE64Encoder();
        String encode = encoder.encode(str.getBytes());
        return encode;
    }
}
  • 新增對映介面
@Mapper
public interface MapstructConvert {

    /**
     * 獲取該類自動生成的實現類的例項
     */
    MapstructConvert INSTANCE = Mappers.getMapper(MapstructConvert.class);
}
  1. 新增一個 interface 介面,使用 MapStruct@Mapper 註解修飾;
  2. 使用 Mappers 新增一個 INSTANCE 例項(也可以使用 Spring 注入,後面會擴充套件)。

3.2 一對一對映

  • 自定義轉換時間格式

通過 dateFormat = "xx" 指定對映的日期格式。

  • 指定預設值

如果該值為空,則使用指定的預設值,如:defaultValue = "-"

  • 忽略不對映的欄位

可以通過 ignore = true 指定不需要對映的屬性,如: @Mapping(target = "password", ignore = true)

  • 巢狀對映

如果一個 DTO 中的值都是從一個物件中的多個巢狀物件對映時,如果不想一個個寫對映,目標可以用 . 表示,如:

@Mapping(source = "role.roleName", target = "roleName")
  • 自定義對映

當我們對映 DTO 的時候,如果某些引數的值 MapStruct 的對映配置不能滿足要求,可以使用自定義方法,例如我們對手機號欄位藉助工具類進行加密後返回:

@Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))")
  • 完整程式碼如下:
@Mappings({
        @Mapping(source = "id", target = "userId"),
        // 自定義轉換時間格式
        @Mapping(source = "gmtBroth", target = "brothStr", dateFormat = "yyyy-MM-dd",defaultValue = "-"),
        // 巢狀對映
        @Mapping(source = "role.roleName", target = "roleName"),
        // 忽略不對映的欄位
        @Mapping(target = "password", ignore = true),
        // 自定義對映
        @Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))"),
})
UserInfoDTO doToDTO(UserInfoDO userInfoDO);

3.3 多引數對映

MapStruct 可以將幾種型別的物件對映為另外一種型別,比如將多個 DO 物件轉換為一個 DTO

@Mappings({
            @Mapping(source = "userInfoDO.id", target = "userId"),
            @Mapping(source = "userInfoDO.gmtBroth", target = "brothStr", dateFormat = "yyyy-MM-dd",defaultValue = "-"),
            @Mapping(source = "userInfoDO.role.roleName", target = "roleName"),
            // 忽略不對映的欄位
            @Mapping(target = "password", ignore = true),
            // 自定義對映
            @Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))"),
            @Mapping(source = "userExtInfoDO.favorite", target = "favorite"),
    })
    UserInfoDTO doToDtoMulti(UserInfoDO userInfoDO, UserExtInfoDO userExtInfoDO);

這樣,我們就可以把 UserInfoDOUserExtInfoDO 對映為 UserInfoDTO

3.4 集合對映

屬性對映關係基於一對一的對映關係。

List<UserInfoDTO> doSToDTOS(List<UserInfoDO> userInfoDOS);

3.5 對映到現有例項

上面都是對映並生成一個新的例項,如果是想對映到已有的現有例項呢?

我們只需用 @MappingTarget 修飾。

3.6 注入 Spring

上面的示例呼叫時都是手動建立了一個 MapstructConvert 例項,
現在都是 Spring 的生態,MapStruct 也可以通過 Spring 注入

@Mapper(componentModel = "spring")
public interface SpringMapstructConvert {

    /**
     * 一對一對映
     * @param userInfoDO
     * @return
     */
    @Mappings({
            @Mapping(source = "id", target = "userId"),
            // 自定義轉換時間格式,如果為空,給予預設值 "-"
            @Mapping(source = "gmtBroth", target = "brothStr", dateFormat = "yyyy-MM-dd",defaultValue = "-"),
            // 巢狀對映
            @Mapping(source = "role.roleName", target = "roleName"),
            // 忽略不對映的欄位
            @Mapping(target = "password", ignore = true),
            // 自定義對映
            @Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))"),
    })
    UserInfoDTO doToDTO(UserInfoDO userInfoDO);
}

相較於前者:幹掉了初始化的 INSTANCE@Mapper 註解加入了 componentModel = "spring"

注意:預設是以覆蓋原有值的方式對映的,如果要保留原有的值,使用 ignore 忽略欄位即可。

四、總結

  • 與手工編寫對映程式碼相比

MapStruct通過生成繁瑣且易於編寫的程式碼來節省時間。遵循約定優於配置方法,MapStruct使用合理的預設值,但在配置或實現特殊行為時

相關文章