mapstruct常见用法
概述
在服务端开发实践中,代码一般会进行分层,比如比较常见的controller、service、dao等。为了降低不同层级之间耦合,一般会定义不同实体对象,比如dao层一般定义xxxDO,controller层一般定义xxxVO等,这些不同层次的DO和VO,一般比较相似,会有一些相同的属性,但又不完全相同(例如数据库存储时间可能会使用long类型的时间戳,或者是date类型,但是vo层可能会需要按照约定格式化后再返回,直接用于页面渲染)。对于不同层之间,实体对象的转换,一直没有特别优雅的实现方案:
- 使用类似beanUtils.copy()的方式,一般底层实现依赖反射,牺牲了一些执行效率
- 直接手写convert方法,调用源对象的get方法获取属性值,然后调用目标对象的set方法去设置值,这样代码影响整体代码的可读性,而且新增属性后也容易忘记同步变更转换器
而 mapstruct 恰好能以比较优雅的方法,来解决上面这个两难的问题:
- 一方面,仅在编译阶段生效,实现不依赖反射,不会影响执行效率
- 另一方面,属性映射依赖于一些简单的注解,而且同名同类型的属性,自动会映射上,不影响对代码可读性
在使用了一段时间 mapstruct 后,本文总结了一些在日常开发中的常见用法,让我们的开发变得更加简单高效。不过本文并不深入解析 mapstruct 的底层实现,这部分内容,可以直接参考官网。
基础用法
最基础用法,往往已经可以解决80%以上的问题。这里仅需要3步,跟把大象放进冰箱一样简单。
属性同类型同名
我们要转换的对象之间,相同的属性同类型同名,这是我们最常见的情况。
引入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%的问题,总还会有一些额外特殊的情况需要处理,下面介绍几种常用的进阶用法。
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的,不然就不会生效了。
看到这里了,点个赞再走呗
转载自:https://juejin.cn/post/7383258697469919282