映射框架MapStruct

一、MapStruct

開發(fā)中,我們經(jīng)常需要將PO轉(zhuǎn)DTO、DTO轉(zhuǎn)PO等一些實(shí)體間的轉(zhuǎn)換。比較出名的有BeanUtil 和ModelMapper等,它們使用簡(jiǎn)單,但是在稍顯復(fù)雜的業(yè)務(wù)場(chǎng)景下力不從心。MapStruct這個(gè)插件可以用來處理domin實(shí)體類與model類的屬性映射,可配置性強(qiáng)。只需要定義一個(gè) Mapper 接口,MapStruct 就會(huì)自動(dòng)實(shí)現(xiàn)這個(gè)映射接口,避免了復(fù)雜繁瑣的映射實(shí)現(xiàn)。MapStruct官網(wǎng)地址: http://mapstruct.org/

二、MapStruct優(yōu)勢(shì)

為什么推薦這個(gè)框架呢,下面說說原因和他的優(yōu)勢(shì)

  • 原因一: 很多項(xiàng)目大量映射通過手動(dòng)get、set,這種寫法非常繁瑣無味,而且沒有技術(shù)含量。甚至中間還牽涉了很多類型轉(zhuǎn)換,嵌套之類的繁瑣操作,非常的愚蠢。

  • 原因二: 有人說apache的BeanUtil. copyProperties可以實(shí)現(xiàn),但是性能差而且容易出異常,很多規(guī)范嚴(yán)禁使用這種途徑。以下是對(duì)幾種對(duì)象映射框架的對(duì)比,大多數(shù)情況下MapStruct性能最高。原理類似于lombok,MapStruct都是在編譯期進(jìn)行實(shí)現(xiàn),而且基于Getter、Setter,,沒有 使用反射所以-般不存在運(yùn)行時(shí)性能問題。

  • 原因三: 方便的映射操作,對(duì)我們平臺(tái)統(tǒng)一代碼有極其重大的意義。簡(jiǎn)化一套代碼真正兼容多個(gè)平臺(tái)的代碼實(shí)現(xiàn)。使得開發(fā)人員多年對(duì)于多個(gè)聯(lián)網(wǎng)平臺(tái)統(tǒng)一代碼的幻想又進(jìn)了一步。

  • 原因四: 在一個(gè)成熟的工程中,尤其是現(xiàn)在的分布式系統(tǒng)中,應(yīng)用與應(yīng)用之間,還有單獨(dú)的應(yīng)用細(xì)分模塊之后,DO 一般不會(huì)讓外部依賴,這時(shí)候需要在提供對(duì)外接口的模塊里放 DTO 用于對(duì)象傳輸,也即是 DO 對(duì)象對(duì)內(nèi),DTO對(duì)象對(duì)外,DTO 可以根據(jù)業(yè)務(wù)需要變更,并不需要映射 DO 的全部屬性。MapStruct 就是這樣的一個(gè)屬性映射工具,只需要定義一個(gè) Mapper 接口,MapStruct 就會(huì)自動(dòng)實(shí)現(xiàn)這個(gè)映射接口,避免了復(fù)雜繁瑣的映射實(shí)現(xiàn)。

  • 原因五: 對(duì)于包裝類是自動(dòng)拆箱封箱操作的,并且是線程安全的。MapStruct不單單有這些功能,還有其他一些復(fù)雜的功能:設(shè)置轉(zhuǎn)換默認(rèn)值和常量。當(dāng)目標(biāo)值是null時(shí)我們可以設(shè)置其默認(rèn)值。

三、MapStruct使用

3.1 單個(gè)對(duì)象轉(zhuǎn)換
3.1.1 新建maven項(xiàng)目,引入依賴MapStruct、Lombok

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.alanchen</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- lombok要與mapstruct版本匹配,用同一時(shí)間的版本,不然會(huì)出現(xiàn)各種問題 -->
        <org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
        <org.projectlombok.version>1.18.6</org.projectlombok.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>

        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>${org.mapstruct.version}</version>
        </dependency>

        <!-- 要與mapstruct版本匹配,用同一時(shí)間的版本,不然會(huì)出現(xiàn)各種問題 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${org.projectlombok.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
3.1.2 編寫代碼

entity

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {

    private Integer id;

    private String name;

    private Integer age;

    private String sex;
}

vo

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserVO {

    private Integer id;

    /**
     * User的屬性是name
     */
    private String userName;

    private Integer age;

    private String sex;
}

convert

import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

/**
 * @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
 *          在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
 */
@Mapper
public interface UserConvert {

    /**
     * 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
     * 接口中的屬性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
     *
     * @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
     *
     * @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
     * @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
     */
    @Mappings({@Mapping(source = "name",target = "userName")})
    UserVO toVO(User user);

    @Mappings({@Mapping(source = "userName",target = "name")})
    User toEntity(UserVO userVO);
}

@Mapper 只有在接口加上這個(gè)注解, MapStruct 才會(huì)去實(shí)現(xiàn)該接口

@Mapper 里有個(gè) componentModel 屬性,主要是指定實(shí)現(xiàn)類的類型,一般用到兩個(gè):

  • default: 默認(rèn),可以通過 Mappers.getMapper(Class) 方式獲取實(shí)例對(duì)象

  • spring: 在接口的實(shí)現(xiàn)類上自動(dòng)添加注解 @Component,可通過 @Autowired 方式注入

@Mapping:屬性映射,若源對(duì)象屬性與目標(biāo)對(duì)象名字一致,會(huì)自動(dòng)映射對(duì)應(yīng)屬性

測(cè)試Client

public class Client {

    public static void main(String[] args) {
        poToVO();

        System.out.println();

        voTOPo();
    }

    private static void poToVO(){
        User user = User.builder()
                .id(1)
                .name("AlanChen")
                .age(18)
                .sex("1")
                .build();
        System.out.println("user:"+user);

        UserVO userVO = UserConvert.instance.toVO(user);
        System.out.println("userVO:"+userVO);
    }

    private static void voTOPo(){
        UserVO userVO = UserVO.builder()
                .id(1)
                .userName("AlanChen")
                .age(18)
                .sex("1")
                .build();
        System.out.println("userVO:"+userVO);

        User user = UserConvert.instance.toEntity(userVO);
        System.out.println("user:"+user);
    }
}

運(yùn)行結(jié)果

user:User(id=1, name=AlanChen, age=18, sex=1)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1)

userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1)
user:User(id=1, name=AlanChen, age=18, sex=1)

啟動(dòng)運(yùn)行,會(huì)在target目錄下自動(dòng)生成轉(zhuǎn)換實(shí)現(xiàn)類

實(shí)現(xiàn)類
3.1.3 跳坑

一開始沒有注意MapStruct與Lombok匹配的問題,導(dǎo)出出現(xiàn)了一些問題,遇到的問題有以下兩個(gè)

問題一:MapStruct生成的實(shí)現(xiàn)類缺失Entity轉(zhuǎn)VO的具體的實(shí)現(xiàn)

沒有設(shè)置屬性

問題二:出現(xiàn)java: No property named “XXX“ exists in source parameter(s). Did you mean “null“

MapStruct與Lombok的版本怎么匹配,我也不太清楚,我嘗試著采用MapStruct、Lombok同一時(shí)期的版本,如mapstruct V1.3.0.Finalprojectlombok V1.18.6都是2019年二月份的版本,問題得到解決。

  <!-- lombok要與mapstruct版本匹配,用同一時(shí)間的版本,不然會(huì)出現(xiàn)各種問題 -->
        <org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
        <org.projectlombok.version>1.18.6</org.projectlombok.version>

問題三:如果對(duì)象有繼承其他父類,轉(zhuǎn)換也會(huì)失敗

3.2 對(duì)象轉(zhuǎn)換返回List

轉(zhuǎn)化 List<> 集合時(shí)必須有 實(shí)體轉(zhuǎn)化,因?yàn)樵趯?shí)現(xiàn)中,List 轉(zhuǎn)換是 for循環(huán)調(diào)用 實(shí)體轉(zhuǎn)化的。所以當(dāng)屬性名不對(duì)應(yīng)時(shí),應(yīng)該在 實(shí)體轉(zhuǎn)化進(jìn)行 @Mappings 的屬性名映射配置,然后list的轉(zhuǎn)換也會(huì)繼承這和屬性的映射。

@Mapper
public interface UserConvert {

    /**
     * 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
     * 接口中的屬性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 轉(zhuǎn)換成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

完整代碼為

import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

import java.util.List;

/**
 * @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
 *          在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
 */
@Mapper
public interface UserConvert {

    /**
     * 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
     * 接口中的屬性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
     *
     * @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
     *
     * @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
     * @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
     */
    @Mappings({@Mapping(source = "name",target = "userName")})
    UserVO toVO(User user);

    @Mappings({@Mapping(source = "userName",target = "name")})
    User toEntity(UserVO userVO);

    /**
     * 轉(zhuǎn)換成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

測(cè)試Client

public class Client {

    public static void main(String[] args) {
        toList();
    }
    private static void toList(){
        List<User> userList = new ArrayList<User>();

        User user1 = User.builder()
                .id(1)
                .name("AlanChen")
                .age(18)
                .sex("1")
                .build();
        userList.add(user1);

        User user2 = User.builder()
                .id(2)
                .name("AlanChen2")
                .age(20)
                .sex("0")
                .build();
        userList.add(user2);

        List<UserVO> userVOList = UserConvert.instance.toVOList(userList);
        System.out.println("userVOList:"+userVOList);
    }
}

運(yùn)行結(jié)果

userVOList:[UserVO(id=1, userName=AlanChen, age=18, sex=1), UserVO(id=2, userName=AlanChen2, age=20, sex=0)]

3.3 屬性類型不同,自定義轉(zhuǎn)換類

現(xiàn)在在User里加一個(gè)是否停用的屬性private boolean stop;boolean類型 ,但UserVO里是否停用的屬性private String stop;為String類型,二者屬性不同,我們需要自己寫一個(gè)轉(zhuǎn)換類

public class UserTransform {

    public String booleanToString(boolean value){
        if(value){
            return "停用";
        }
        return "未停用";
    }
    public boolean strToBoolean(String str){
        if ("停用".equals(str)) {
            return true;
        }
        return false;
    }
}

UserConvert加注解參數(shù)

@Mapper(uses = UserTransform.class)
import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

import java.util.List;

/**
 * @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
 *          在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
 */
@Mapper(uses = UserTransform.class)
public interface UserConvert {

    /**
     * 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
     * 接口中的屬性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
     *
     * @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
     *
     * @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
     * @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
     */
    @Mappings({@Mapping(source = "name",target = "userName")})
    UserVO toVO(User user);

    @Mappings({@Mapping(source = "userName",target = "name")})
    User toEntity(UserVO userVO);

    /**
     * 轉(zhuǎn)換成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

Client測(cè)試代碼

public class Client {

    public static void main(String[] args) {
        poToVO();

        System.out.println();

        voTOPo();
    }

    private static void poToVO(){
        User user = User.builder()
                .id(1)
                .name("AlanChen")
                .age(18)
                .sex("1")
                .stop(false)
                .build();
        System.out.println("user:"+user);

        UserVO userVO = UserConvert.instance.toVO(user);
        System.out.println("userVO:"+userVO);
    }

    private static void voTOPo(){
        UserVO userVO = UserVO.builder()
                .id(1)
                .userName("AlanChen")
                .age(18)
                .sex("1")
                .stop("停用")
                .build();
        System.out.println("userVO:"+userVO);

        User user = UserConvert.instance.toEntity(userVO);
        System.out.println("user:"+user);
    }
}

運(yùn)行結(jié)果

user:User(id=1, name=AlanChen, age=18, sex=1, stop=false)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=未停用)

userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=停用)
user:User(id=1, name=AlanChen, age=18, sex=1, stop=true)

3.4 dateFormat配置日期格式

在User類里繼續(xù)加一個(gè)生日字段private Date birthday;,UserVO類里加生日字段private String birthday;

在UserConvert里指定dateFormat

import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

import java.util.List;

/**
 * @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
 *          在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
 */
@Mapper(uses = UserTransform.class)
public interface UserConvert {

    /**
     * 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
     * 接口中的屬性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
     *
     * @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
     *
     * @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
     * @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
     */
    @Mappings({@Mapping(source = "name",target = "userName"),
            @Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")})
    UserVO toVO(User user);

    @Mappings({@Mapping(source = "userName",target = "name"),
            @Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")})
    User toEntity(UserVO userVO);

    /**
     * 轉(zhuǎn)換成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

Client測(cè)試類

public class Client {

    public static void main(String[] args) {
        poToVO();

        System.out.println();

        voTOPo();
    }

    private static void poToVO(){
        User user = User.builder()
                .id(1)
                .name("AlanChen")
                .age(18)
                .sex("1")
                .stop(false)
                .birthday(new Date())
                .build();
        System.out.println("user:"+user);

        UserVO userVO = UserConvert.instance.toVO(user);
        System.out.println("userVO:"+userVO);
    }

    private static void voTOPo(){
        UserVO userVO = UserVO.builder()
                .id(1)
                .userName("AlanChen")
                .age(18)
                .sex("1")
                .stop("停用")
                .birthday("1990-05-20")
                .build();
        System.out.println("userVO:"+userVO);

        User user = UserConvert.instance.toEntity(userVO);
        System.out.println("user:"+user);
    }
}

運(yùn)行結(jié)果

user:User(id=1, name=AlanChen, age=18, sex=1, stop=false, birthday=Mon Nov 22 19:02:44 CST 2021)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=未停用, birthday=2021-11-22)

userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=停用, birthday=1990-05-20)
user:User(id=1, name=AlanChen, age=18, sex=1, stop=true, birthday=Sun May 20 00:00:00 CDT 1990)
3.5 ignore

ignore: 忽略這個(gè)字段

3.6 多對(duì)一映射

MapStruct 可以將幾種類型的對(duì)象映射為另外一種類型,比如將多個(gè)entity對(duì)象轉(zhuǎn)換為VO。例如:兩個(gè)entity對(duì)象 Item 和 Sku,一個(gè)VO對(duì)象SkuVO

entity

@NoArgsConstructor
@AllArgsConstructor
@Data
public class Item {

    private Long id;

    private String title;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Sku {

    private String code;

    private Integer price;
}

convert

@Mapper
public interface ItemConvert {

    ItemConvert instance = Mappers.getMapper(ItemConvert.class);

    @Mappings({
            @Mapping(source = "sku.id",target = "skuId"),
            @Mapping(source = "sku.code",target = "skuCode"),
            @Mapping(source = "sku.price",target = "skuPrice"),
            @Mapping(source = "item.id",target = "itemId"),
            @Mapping(source = "item.title",target = "itemName")

    })

    SkuVO toVO(Item item, Sku sku);
}

四、常用代碼

4.1 expression
@Mapper
public interface DynamicConvert {
    DynamicConvert instance = Mappers.getMapper(DynamicConvert.class);

   @Mapping(target = "id", expression="java(calendarEntity.getId().toString())" )
    CalendarDetailDTO toCalendarDTO(CalendarEntity calendarEntity);
}

@Data
@Document(collection = "calendar")
public class CalendarEntity{

@JsonDeserialize(using = ObjectIdJsonDeserializer.class)
    @JsonSerialize(using = ObjectIdJsonSerializer.class)
    @JSONField(serializeUsing = ObjectIDSerializer.class, deserializeUsing = ObjectIDSerializer.class)
    @MongoId
    private ObjectId id;

    @Indexed
    private Long memberId;

    @ApiModelProperty(value = "mobile")
    private String mobile;
}


@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CalendarDetailDTO implements Serializable {

    @ApiModelProperty(value = "動(dòng)態(tài)ID")
     private String id;

   @ApiModelProperty(value = "會(huì)員ID")
    private Long memberId;

   @ApiModelProperty(value = "會(huì)員手機(jī)號(hào)")
    private String mobile;
}

五、Demo源碼

alanchenyan/mapstruct

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容