likes
comments
collection
share

mapstruct常见用法

作者站长头像
站长
· 阅读数 23

概述

在服务端开发实践中,代码一般会进行分层,比如比较常见的controller、service、dao等。为了降低不同层级之间耦合,一般会定义不同实体对象,比如dao层一般定义xxxDO,controller层一般定义xxxVO等,这些不同层次的DO和VO,一般比较相似,会有一些相同的属性,但又不完全相同(例如数据库存储时间可能会使用long类型的时间戳,或者是date类型,但是vo层可能会需要按照约定格式化后再返回,直接用于页面渲染)。对于不同层之间,实体对象的转换,一直没有特别优雅的实现方案:

  • 使用类似beanUtils.copy()的方式,一般底层实现依赖反射,牺牲了一些执行效率
  • 直接手写convert方法,调用源对象的get方法获取属性值,然后调用目标对象的set方法去设置值,这样代码影响整体代码的可读性,而且新增属性后也容易忘记同步变更转换器

而 mapstruct 恰好能以比较优雅的方法,来解决上面这个两难的问题:

  • 一方面,仅在编译阶段生效,实现不依赖反射,不会影响执行效率
  • 另一方面,属性映射依赖于一些简单的注解,而且同名同类型的属性,自动会映射上,不影响对代码可读性

在使用了一段时间 mapstruct 后,本文总结了一些在日常开发中的常见用法,让我们的开发变得更加简单高效。不过本文并不深入解析 mapstruct 的底层实现,这部分内容,可以直接参考官网

mapstruct常见用法

基础用法

最基础用法,往往已经可以解决80%以上的问题。这里仅需要3步,跟把大象放进冰箱一样简单。

mapstruct常见用法

属性同类型同名

我们要转换的对象之间,相同的属性同类型同名,这是我们最常见的情况。

引入maven依赖

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

创建convert接口

创建之前,我们先简单介绍一下来源类UserDO和目标类UserVO,我们这里是想把从db查询出来的UserDO转换为返回的UserVO

@Data
public class UserDO {

    private Long id;

    private String name;
}
@Data
public class UserVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1149477891337237770L;

    private Long id;

    private String name;
}

转换接口UserConvert

@Mapper
public interface UserConvert {

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

    UserVO do2vo(UserDO userDO);
}

使用convert进行转换

@RequestMapping("/user")
    public UserVO user() {
        UserDO userDO = getUserDO();
        log.info("userDO = {}", JSON.toJSONString(userDO));
        UserVO userVO = UserConvert.INSTANCE.do2vo(userDO);
        log.info("userVO = {}", JSON.toJSONString(userDO));
        return userVO;
    }

这样,我们就可以看到我们的userDO对象成功转换成了userVO对象

2024-06-22T22:19:58.856+08:00  INFO 71391 --- [nio-8080-exec-2] c.p.m.demos.web.BasicController          : userDO = {"id":123,"name":"scott"}
2024-06-22T22:19:58.858+08:00  INFO 71391 --- [nio-8080-exec-2] c.p.m.demos.web.BasicController          : userVO = {"id":123,"name":"scott"}

属性同类型不同名

我们要转换的对象之间,虽然大部分属性是同类型同名,但是由于我们要转换的实体类往往服务于不同的层,相同的属性,在各层之间名字有差异也是比较常见的情况,例如上面的例子中,我们把UserVO的name属性名修改成userName,这样就跟UserDO的name不同名了。

UserVO定义

@Data
public class UserVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1149477891337237770L;

    private Long id;

    private String userName;
}

转换接口UserConvert

@Mapper
public interface UserConvert {

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

    @Mapping(target = "userName", source = "name")
    UserVO do2vo(UserDO userDO);
}

跟上面1个例子有所不同,这里需要UserDO的name转换成UserVO的userName属性,mapstruct无法自动对这2个不同名的属性进行映射,需要通过@Mapping注解进行指定。 源码底下无秘密,我们在编译后,可以通过查看自动生成的UserConvert接口的实现类,来看下这次转换是怎么实现的:

public class UserConvertImpl implements UserConvert {

    @Override
    public UserVO do2vo(UserDO userDO) {
        if ( userDO == null ) {
            return null;
        }

        UserVO userVO = new UserVO();

        userVO.setUserName( userDO.getName() );
        userVO.setId( userDO.getId() );

        return userVO;
    }
}

通过源码可以明显观察到@Mapping注解的效果,将UserDO的name转换成UserVO的userName属性。

进阶用法

除了使用上面最基础的用法来解决80%的问题,总还会有一些额外特殊的情况需要处理,下面介绍几种常用的进阶用法。

mapstruct常见用法

dateFormat

我们通过下面这个例子来说明:UserDO中有个属性是birthday,类型是Date,通过UserVO返回给前端展示的时候,需要转换成yyyy-MM-dd的格式:

来源类和目标类

@Data
public class UserDO {

    private Long id;

    private String name;

    private Date birthday;
}
@Data
public class UserVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1149477891337237770L;

    private Long id;

    private String userName;

    private String birthday;
}

转换接口定义

@Mapping(target = "userName", source = "name")
@Mapping(target = "birthday", source = "birthday", dateFormat = "yyyy-MM-dd")
UserVO do2vo(UserDO userDO);

生成的转换方法

@Override
public UserVO do2vo(UserDO userDO) {
    if ( userDO == null ) {
        return null;
    }

    UserVO userVO = new UserVO();

    if ( userDO != null ) {
        userVO.setUserName( userDO.getName() );
        if ( userDO.getBirthday() != null ) {
            userVO.setBirthday( new SimpleDateFormat( "yyyy-MM-dd" ).format( userDO.getBirthday() ) );
        }
        userVO.setId( userDO.getId() );
    }

    return userVO;
}

qualifiedByName

这个是比较万金油的做法,通过qualifiedByName来指定源对象和目标对象之间特定属性的转换方法,自由度很高。 上面例子中的日期对象格式化,如果使用qualifiedByName,可以做更多的自定义处理。

@Named注解

编写formatBirthday方法,使用@Named注解指定名字。注意,这里的名字可以指定的跟方法原始名称不一样,有一定灵活性。

@Named("formatBirthday")
default String formatBirthday(Date birthday) {
    SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
    return format.format(birthday);
}

@Mapper注解指定属性转换的时候使用的方法

@Mapping(target = "userName", source = "name")
@Mapping(target = "birthday", source = "birthday", qualifiedByName = "formatBirthday")
UserVO do2vo(UserDO userDO);

指定了来源类的birthday属性,转换成目标类的birthday属性的时候,通过formatBirthday进行转换,我们看编译后自动生成的源码:

@Override
public UserVO do2vo(UserDO userDO) {
    if ( userDO == null ) {
        return null;
    }

    UserVO userVO = new UserVO();

    userVO.setUserName( userDO.getName() );
    userVO.setBirthday( formatBirthday( userDO.getBirthday() ) );
    userVO.setId( userDO.getId() );

    return userVO;
}

多来源和嵌套

如果我们的目标对象不是简单对象,而是对象中包含了其他的自定义对象,而且不同的信息来源于多个不同的来源对象,那么我们还可以进行多来源和嵌套映射。 以下这个例子:UserVO中包含1个address属性,是AddressVO类型,来源于1个单独的AddressDO对象

来源类和目标类

@Data
public class UserVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1149477891337237770L;

    private Long id;

    private String userName;

    private String birthday;

    private AddressVO address;
}
@Data
public class AddressDO {

    private String province;

    private String city;

    private String locationDetail;
}
@Data
public class AddressVO {

    private String province;

    private String city;

    private String locationDetail;
}

定义AddressDO转换成AddressVO的方法

AddressVO do2vo(AddressDO addressDO);

指定UserVO的address对象来源于addressDO

@Mapping(target = "userName", source = "userDO.name")
@Mapping(target = "birthday", source = "userDO.birthday", qualifiedByName = "formatBirthday")
@Mapping(target = "address", source = "addressDO")
UserVO do2vo(UserDO userDO, AddressDO addressDO);

需要注意的是,如果来源类有多个,那么我们在使用@Mapping注解指定source的时候,需要指定来源于哪个类,例如上面使用userDO.name代替了原来的name

生成的转换方法

public class UserConvertImpl implements UserConvert {

    @Override
    public UserVO do2vo(UserDO userDO, AddressDO addressDO) {
        if ( userDO == null && addressDO == null ) {
            return null;
        }

        UserVO userVO = new UserVO();

        if ( userDO != null ) {
            userVO.setUserName( userDO.getName() );
            userVO.setBirthday( formatBirthday( userDO.getBirthday() ) );
            userVO.setId( userDO.getId() );
        }
        userVO.setAddress( do2vo( addressDO ) );

        return userVO;
    }

    @Override
    public AddressVO do2vo(AddressDO addressDO) {
        if ( addressDO == null ) {
            return null;
        }

        AddressVO addressVO = new AddressVO();

        addressVO.setProvince( addressDO.getProvince() );
        addressVO.setCity( addressDO.getCity() );
        addressVO.setLocationDetail( addressDO.getLocationDetail() );

        return addressVO;
    }
}

ignore 和 constant

这2个注解比较容易理解,见名知意,ignore代表这个属性忽略,constant代表给属性赋固定的值。 例如,UserVO中,我们给属性id设置ignore,给属性hobby设置固定值swimming:

目标类

@Data
public class UserVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1149477891337237770L;

    private Long id;

    private String userName;

    private String birthday;

    private AddressVO address;

    private String hobby;
}

转换接口定义

@Mapping(target = "userName", source = "userDO.name")
@Mapping(target = "birthday", source = "userDO.birthday", qualifiedByName = "formatBirthday")
@Mapping(target = "address", source = "addressDO")
@Mapping(target = "id", ignore = true)
@Mapping(target = "hobby", constant = "swimming")
UserVO do2vo(UserDO userDO, AddressDO addressDO);

生成的转换方法

@Override
public UserVO do2vo(UserDO userDO, AddressDO addressDO) {
    if ( userDO == null && addressDO == null ) {
        return null;
    }

    UserVO userVO = new UserVO();

    if ( userDO != null ) {
        userVO.setUserName( userDO.getName() );
        userVO.setBirthday( formatBirthday( userDO.getBirthday() ) );
    }
    userVO.setAddress( do2vo( addressDO ) );
    userVO.setHobby( "swimming" );

    return userVO;
}

可以看到,id没有赋值,hobby赋了固定值:swimming

常见问题

集成lombok

lombok也是我们开发中常用的工具,但是mapstruct和lombok同时使用时,要注意顺序带来的一些问题:我们一般使用lombok给类自动生成get、set方法,但是如果mapstruct在lombok之前就工作了,就会造成:对象的属性没有get、set方法,mapstruct就不会去设置对应的转换方法,导致转换出来的对象,属性值都为空。 解决的方法也非常简单,把lombok的maven依赖放在mapstruct之前,并设置好annotationProcessorPaths

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>
<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.5.5.Final</version>
</dependency>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.8.1</version>
    <configuration>
        <source>17</source>
        <target>17</target>
        <encoding>UTF-8</encoding>
        <annotationProcessorPaths>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.32</version>
            </path>
            <path>
                <groupId>org.mapstruct</groupId>
                <artifactId>mapstruct-processor</artifactId>
                <version>1.5.5.Final</version>
            </path>
            <path>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok-mapstruct-binding</artifactId>
                <version>0.2.0</version>
            </path>
        </annotationProcessorPaths>
    </configuration>
</plugin>

跟Mybatis的@Mapper注解混用

Mybatis是我们常用的ORM框架,注意mapstruct定义转换接口的@Mapper注解,不要使用成Mybatyis的,不然就不会生效了。

看到这里了,点个赞再走呗

mapstruct常见用法

转载自:https://juejin.cn/post/7383258697469919282
评论
请登录