映射框架MapStruct

一、MapStruct

开发中,我们经常需要将PO转DTO、DTO转PO等一些实体间的转换。比较出名的有BeanUtil 和ModelMapper等,它们使用简单,但是在稍显复杂的业务场景下力不从心。MapStruct这个插件可以用来处理domin实体类与model类的属性映射,可配置性强。只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。MapStruct官网地址: http://mapstruct.org/

二、MapStruct优势

为什么推荐这个框架呢,下面说说原因和他的优势

  • 原因一: 很多项目大量映射通过手动get、set,这种写法非常繁琐无味,而且没有技术含量。甚至中间还牵涉了很多类型转换,嵌套之类的繁琐操作,非常的愚蠢。

  • 原因二: 有人说apache的BeanUtil. copyProperties可以实现,但是性能差而且容易出异常,很多规范严禁使用这种途径。以下是对几种对象映射框架的对比,大多数情况下MapStruct性能最高。原理类似于lombok,MapStruct都是在编译期进行实现,而且基于Getter、Setter,,没有 使用反射所以-般不存在运行时性能问题。

  • 原因三: 方便的映射操作,对我们平台统一代码有极其重大的意义。简化一套代码真正兼容多个平台的代码实现。使得开发人员多年对于多个联网平台统一代码的幻想又进了一步。

  • 原因四: 在一个成熟的工程中,尤其是现在的分布式系统中,应用与应用之间,还有单独的应用细分模块之后,DO 一般不会让外部依赖,这时候需要在提供对外接口的模块里放 DTO 用于对象传输,也即是 DO 对象对内,DTO对象对外,DTO 可以根据业务需要变更,并不需要映射 DO 的全部属性。MapStruct 就是这样的一个属性映射工具,只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。

  • 原因五: 对于包装类是自动拆箱封箱操作的,并且是线程安全的。MapStruct不单单有这些功能,还有其他一些复杂的功能:设置转换默认值和常量。当目标值是null时我们可以设置其默认值。

三、MapStruct使用

3.1 单个对象转换
3.1.1 新建maven项目,引入依赖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版本匹配,用同一时间的版本,不然会出现各种问题 -->
        <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版本匹配,用同一时间的版本,不然会出现各种问题 -->
        <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 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public interface UserConvert {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({@Mapping(source = "name",target = "userName")})
    UserVO toVO(User user);

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

@Mapper 只有在接口加上这个注解, MapStruct 才会去实现该接口

@Mapper 里有个 componentModel 属性,主要是指定实现类的类型,一般用到两个:

  • default: 默认,可以通过 Mappers.getMapper(Class) 方式获取实例对象

  • spring: 在接口的实现类上自动添加注解 @Component,可通过 @Autowired 方式注入

@Mapping:属性映射,若源对象属性与目标对象名字一致,会自动映射对应属性

测试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);
    }
}

运行结果

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)

启动运行,会在target目录下自动生成转换实现类

实现类
3.1.3 跳坑

一开始没有注意MapStruct与Lombok匹配的问题,导出出现了一些问题,遇到的问题有以下两个

问题一:MapStruct生成的实现类缺失Entity转VO的具体的实现

没有设置属性

问题二:出现java: No property named “XXX“ exists in source parameter(s). Did you mean “null“

MapStruct与Lombok的版本怎么匹配,我也不太清楚,我尝试着采用MapStruct、Lombok同一时期的版本,如mapstruct V1.3.0.Finalprojectlombok V1.18.6都是2019年二月份的版本,问题得到解决。

  <!-- lombok要与mapstruct版本匹配,用同一时间的版本,不然会出现各种问题 -->
        <org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
        <org.projectlombok.version>1.18.6</org.projectlombok.version>

问题三:如果对象有继承其他父类,转换也会失败

3.2 对象转换返回List

转化 List<> 集合时必须有 实体转化,因为在实现中,List 转换是 for循环调用 实体转化的。所以当属性名不对应时,应该在 实体转化进行 @Mappings 的属性名映射配置,然后list的转换也会继承这和属性的映射。

@Mapper
public interface UserConvert {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 转换成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 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper
public interface UserConvert {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({@Mapping(source = "name",target = "userName")})
    UserVO toVO(User user);

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

    /**
     * 转换成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

测试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);
    }
}

运行结果

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

3.3 属性类型不同,自定义转换类

现在在User里加一个是否停用的属性private boolean stop;boolean类型 ,但UserVO里是否停用的属性private String stop;为String类型,二者属性不同,我们需要自己写一个转换类

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加注解参数

@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 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper(uses = UserTransform.class)
public interface UserConvert {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @Mappings({@Mapping(source = "name",target = "userName")})
    UserVO toVO(User user);

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

    /**
     * 转换成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

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")
                .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);
    }
}

运行结果

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类里继续加一个生日字段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 定义这是一个MapStruct对象属性转换接口,在这个类里面规定转换规则
 *          在项目构建时,会自动生成改接口的实现类,这个实现类将实现对象属性值复制
 */
@Mapper(uses = UserTransform.class)
public interface UserConvert {

    /**
     * 获取该类自动生成的实现类的实例
     * 接口中的属性都是 public static final 的 方法都是public abstract的
     */
    UserConvert instance = Mappers.getMapper(UserConvert.class);

    /**
     * 这个方法就是用于实现对象属性复制的方法
     *
     * @Mapping 用来定义属性复制规则 source 指定源对象属性 target指定目标对象属性
     *
     * @param user 这个参数就是源对象,也就是需要被复制的对象
     * @return 返回的是目标对象,就是最终的结果对象
     */
    @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);

    /**
     * 转换成List
     * @param users
     * @return
     */
    List<UserVO> toVOList(List<User> users);
}

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")
                .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);
    }
}

运行结果

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: 忽略这个字段

3.6 多对一映射

MapStruct 可以将几种类型的对象映射为另外一种类型,比如将多个entity对象转换为VO。例如:两个entity对象 Item 和 Sku,一个VO对象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 = "动态ID")
     private String id;

   @ApiModelProperty(value = "会员ID")
    private Long memberId;

   @ApiModelProperty(value = "会员手机号")
    private String mobile;
}

五、Demo源码

alanchenyan/mapstruct

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,816评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,729评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,300评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,780评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,890评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,084评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,151评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,912评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,355评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,666评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,809评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,504评论 4 334
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,150评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,882评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,121评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,628评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,724评论 2 351

推荐阅读更多精彩内容