mapstruct使用詳解


我們都知道,隨着一個工程的越來越成熟,模塊划分會越來越細,其中實體類一般存於 domain 之中,但 domain 工程最好不要被其他工程依賴,所以其他工程想獲取實體類數據時就需要在各自工程寫 model,自定義 model 可以根據自身業務需要映射相應的實體屬性。這樣一來,這個映射工程貌似並不簡單了。阿森差點就犯難了……

所以阿淼今天就要給大家安利一款叫 mapstruct 的插件,它就是專門用來處理 domin 實體類與 model 類的屬性映射的,我們只需定義 mapper 接口,mapstruct 在編譯的時候就會自動的幫我們實現這個映射接口,避免了麻煩復雜的映射實現。

那可能有的小伙伴就要問了?為啥不用 BeanUtilscopyProperties 方法呢?不也照樣可以實現屬性的映射么?

這個啊,阿淼我開始也是好奇,所以就和 BeanUtils 深入交流了一番,最后才發現,BeanUtils 就是一個大老粗,只能同屬性映射,或者在屬性相同的情況下,允許被映射的對象屬性少;但當遇到被映射的屬性數據類型被修改或者被映射的字段名被修改,則會導致映射失敗。而 mapstruct 就是一個巧媳婦兒了,她心思細膩,把我們可能會遇到的情況都給考慮到了(要是阿淼我也能找一個這樣的媳婦兒該多好,內心笑出了豬聲)

如下是這個插件的開源項目地址和各種例子:

一、准備工作

接下來,阿淼將和大家一起去解開這個巧媳婦兒的真正面紗,所以我們還需要做一點准備工作。

1.1、了解@Mapper 注解

從 mybatis3.4.0 開始加入的 @Mapper 注解,目的就是為了不再寫mapper映射文件。

我們只需要在 dao 層定義的接口上使用注解就可以實現sql語句的編寫,例如:

@Select("select * from user where name = #{name}")
public User find(String name);

如上就是一個簡單的使用,雖然簡單,但也確實體現出了這個注解的優越性,至少少寫了一個xml文件。

但阿淼我今天可不是想跟你探討 @Mapper 注解,我主要是想去看我的巧媳婦兒 mapstruct ,所以我就只是想說下 @Mapper 注解的 componentModel 屬性,componentModel 屬性用於指定自動生成的接口實現類的組件類型,這個屬性支持四個值:

  • default: 這是默認的情況,mapstruct 不使用任何組件類型, 可以通過Mappers.getMapper(Class)方式獲取自動生成的實例對象。
  • cdi: the generated mapper is an application-scoped CDI bean and can be retrieved via @Inject
  • spring: 生成的實現類上面會自動添加一個@Component注解,可以通過Spring的 @Autowired方式進行注入
  • jsr330: 生成的實現類上會添加@javax.inject.Named 和@Singleton注解,可以通過 @Inject注解獲取

1.2、依賴包

首先需要把依賴包導入,主要由兩個包組成:

  • org.mapstruct:mapstruct:包含了一些必要的注解,例如@Mapping。r若我們使用的JDK版本高於1.8,當我們在pom里面導入依賴時候,建議使用坐標是:org.mapstruct:mapstruct-jdk8,這可以幫助我們利用一些Java8的新特性。
  • org.mapstruct:mapstruct-processor:注解處理器,根據注解自動生成mapper的實現。
    <dependency>
        <groupId>org.mapstruct</groupId>
        <!-- jdk8以下就使用mapstruct -->
        <artifactId>mapstruct-jdk8</artifactId>
        <version>1.2.0.Final</version>
    </dependency>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.2.0.Final</version>
    </dependency>

好了,准備工作做完了,接下來我們就看看巧媳婦兒巧在什么地方吧。

二、先簡單玩一把

2.1、定義實體類以及被映射類

// 實體類
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
    private Integer id;
    private String name;
    private String createTime;
    private LocalDateTime updateTime;
}

// 被映射類VO1:和實體類一模一樣
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO1 {
    private Integer id;
    private String name;
    private String createTime;
    private LocalDateTime updateTime;
}

// 被映射類VO1:比實體類少一個字段
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO2 {
    private Integer id;
    private String name;
    private String createTime;

}

2.2、定義接口:

當實體類和被映射對象屬性相同或者被映射對象屬性值少幾個時:

@Mapper(componentModel = "spring")
public interface UserCovertBasic {
    UserCovertBasic INSTANCE = Mappers.getMapper(UserCovertBasic.class);

    /**
     * 字段數量類型數量相同,利用工具BeanUtils也可以實現類似效果
     * @param source
     * @return
     */
    UserVO1 toConvertVO1(User source);
    User fromConvertEntity1(UserVO1 userVO1);

    /**
     * 字段數量類型相同,數量少:僅能讓多的轉換成少的,故沒有fromConvertEntity2
     * @param source
     * @return
     */
    UserVO2 toConvertVO2(User source);
}

從上面的代碼可以看出:接口中聲明了一個成員變量INSTANCE,母的是讓客戶端可以訪問 Mapper 接口的實現。

2.3、使用

@RestController
public class TestController {

    @GetMapping("convert")
    public Object convertEntity() {
        User user = User.builder()
                .id(1)
                .name("張三")
                .createTime("2020-04-01 11:05:07")
                .updateTime(LocalDateTime.now())
                .build();
        List<Object> objectList = new ArrayList<>();

        objectList.add(user);

        // 使用mapstruct
        UserVO1 userVO1 = UserCovertBasic.INSTANCE.toConvertVO1(user);
        objectList.add("userVO1:" + UserCovertBasic.INSTANCE.toConvertVO1(user));
        objectList.add("userVO1轉換回實體類user:" + UserCovertBasic.INSTANCE.fromConvertEntity1(userVO1));
        // 輸出轉換結果
        objectList.add("userVO2:" + " | " + UserCovertBasic.INSTANCE.toConvertVO2(user));
        // 使用BeanUtils
        UserVO2 userVO22 = new UserVO2();
        BeanUtils.copyProperties(user, userVO22);
        objectList.add("userVO22:" + " | " + userVO22);

        return objectList;
    }
}

2.4、查看編譯結果

通過IDE的反編譯功能查看編譯后自動生成 UserCovertBasic 的實現類 UserCovertBasicImpl ,內容如下:

@Component
public class UserCovertBasicImpl implements UserCovertBasic {
    public UserCovertBasicImpl() {
    }

    public UserVO1 toConvertVO1(User source) {
        if (source == null) {
            return null;
        } else {
            UserVO1 userVO1 = new UserVO1();
            userVO1.setId(source.getId());
            userVO1.setName(source.getName());
            userVO1.setCreateTime(source.getCreateTime());
            userVO1.setUpdateTime(source.getUpdateTime());
            return userVO1;
        }
    }

    public User fromConvertEntity1(UserVO1 userVO1) {
        if (userVO1 == null) {
            return null;
        } else {
            User user = new User();
            user.setId(userVO1.getId());
            user.setName(userVO1.getName());
            user.setCreateTime(userVO1.getCreateTime());
            user.setUpdateTime(userVO1.getUpdateTime());
            return user;
        }
    }

    public UserVO2 toConvertVO2(User source) {
        if (source == null) {
            return null;
        } else {
            UserVO2 userVO2 = new UserVO2();
            userVO2.setId(source.getId());
            userVO2.setName(source.getName());
            userVO2.setCreateTime(source.getCreateTime());
            return userVO2;
        }
    }
}

2.5、瀏覽器查看結果

好了,一個流程就走完了,是不是感覺賊簡單呢?

而且呀,阿淼溫馨提醒:
如果是要轉換一個集合的話,只需要把這里的實體類換成集合就行了,例如:

    List<UserVO1> toConvertVOList(List<User> source);

三、不簡單的情況

上面已經把整個流程都給過了一遍了,相信大家對 mapstruct 也有了一個基礎的了解了,所以接下來的情況我們就不展示全部代碼了,畢竟篇幅也有限,所以就直接上關鍵代碼(因為不關鍵的和上面內容一樣,哈哈)

3.1、類型不一致

實體類我們還是沿用 User;被映射對象 UserVO3 改為:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO3 {
    private String id;
    private String name;
    // 實體類該屬性是String
    private LocalDateTime createTime;
    // 實體類該屬性是LocalDateTime
    private String updateTime;
}

那么我們定義的接口就要稍稍修改一下了:

    @Mappings({
            @Mapping(target = "createTime", expression = "java(com.java.mmzsblog.util.DateTransform.strToDate(source.getCreateTime()))"),
    })
    UserVO3 toConvertVO3(User source);

    User fromConvertEntity3(UserVO3 userVO3);

上面 expression 指定的表達式內容如下:

public class DateTransform {
    public static LocalDateTime strToDate(String str){
        DateTimeFormatter df = DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss");
        return LocalDateTime.parse("2018-01-12 17:07:05",df);
    }

}

通過IDE的反編譯功能查看編譯后的實現類,結果是這樣子的:

從圖中我們可以看到,編譯時使用了expression中定義的表達式對目標字段 createTime 進行了轉換;然后你還會發現 updateTime 字段也被自動從 LocalDateTime 類型轉換成了 String 類型。

阿淼小結

當字段類型不一致時,以下的類型之間是 mapstruct 自動進行類型轉換的:

  • 1、基本類型及其他們對應的包裝類型。
    此時 mapstruct 會自動進行拆裝箱。不需要人為的處理
  • 2、基本類型的包裝類型和string類型之間

除此之外的類型轉換我們可以通過定義表達式來進行指定轉換。

3.2、字段名不一致

實體類我們還是沿用 User;被映射對象 UserVO4 改為:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO4 {
    // 實體類該屬性名是id
    private String userId;
    // 實體類該屬性名是name
    private String userName;
    private String createTime;
    private String updateTime;
}

那么我們定義的接口就要稍稍修改一下了:

    @Mappings({
            @Mapping(source = "id", target = "userId"),
            @Mapping(source = "name", target = "userName")
    })
    UserVO4 toConvertVO(User source);
    
    User fromConvertEntity(UserVO4 userVO4);

通過IDE的反編譯功能查看編譯后的實現類,編譯后的結果是這樣子的:

很明顯, mapstruct 通過讀取我們配置的字段名對應關系,幫我們把它們賦值在了相對應的位置上,可以說是相當優秀了,但這也僅僅是優秀,而更秀的還請繼續往下看:

阿淼小結

當字段名不一致時,通過使用 @Mappings 注解指定對應關系,編譯后即可實現對應字段的賦值。

3.3、屬性是枚舉類型

實體類我們還是改用 UserEnum:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserEnum {
    private Integer id;
    private String name;
    private UserTypeEnum userTypeEnum;
}

被映射對象 UserVO5 改為:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserVO5 {
    private Integer id;
    private String name;
    private String type;
}

枚舉對象是:

@Getter
@AllArgsConstructor
public enum UserTypeEnum {
    Java("000", "Java開發工程師"),
    DB("001", "數據庫管理員"),
    LINUX("002", "Linux運維員");
    
    private String value;
    private String title;

}

那么我們定義的接口還是照常定義,不會受到它是枚舉就有所變化:

    @Mapping(source = "userTypeEnum", target = "type")
    UserVO5 toConvertVO5(UserEnum source);

    UserEnum fromConvertEntity5(UserVO5 userVO5);

通過IDE的反編譯功能查看編譯后的實現類,編譯后的結果是這樣子的:

很明顯, mapstruct 通過枚舉類型的內容,幫我們把枚舉類型轉換成字符串,並給type賦值,可謂是小心使得萬年船啊。看來這巧媳婦兒不僅僅優秀還心細啊……

源代碼

文章中的所有例子已上傳github:https://github.com/mmzsblog/mapstructDemo


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM