MapStruct对象映射转换
前言
2024.05.26,项目中用到了MapStruct,今天对项目中的一个实体类进行改动,发现不起作用,一顿排查下来发现是MapStruct搞错的,因此打算系统整理一下MapStruct的用法。
介绍
在实际开发中我们经常需要做DTO、VO、Entity对象之间的转换,在开发中常见的做法有两种:
- 手动get、set
- 使用BeanUtils工具进行转换
手动进行set、get甚是麻烦,如果对象属性过多,则代码量很大;BeanUtils的对象转换工具是作用在运行期间,效率低下。
MapStruct是一款基于注解的Java的对象映射工具,与Lombok相似,作用于编译期间,编译后直接生成对应的class文件,因此运行期间的性能较高。
官网MapStruct – Java bean mappings, the easy way!
GitHub仓库GitHub - mapstruct/mapstruct: An annotation processor for generating type-safe bean mappers
MapStruct可以单独使用,也可以配合Spring框架一起使用
MapStruct实现对象之间转换的原理是基于get、set来实现的,来查看一下示例的生成的代码,非常简单
起步
- 导入依赖
org.mapstruct.mapstruct
包含必要的注解,例如@Mapping
org.mapstruct.mapstruct-processor
中包含注解生成器的实现类
注意:因为Lombok会生成setter、getter,所以如果项目中使用了lombok,需要在pom.xml
将lombok写在mapstruct的前面
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.5.5.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.5.5.Final</version>
</dependency>
- 这是我们的类,此处使用lombok生成getter、setter,如果不使用lombok需要手动写getter、setter
若源类型中的属性名和类型与目标类型中的相同,则可以自动完成转换,不需要额外的标注。
User.java
@Data
public class User {
private String userName;
private String password;
private String sex;
private Integer age;
}
UserVo.java
@Data
public class UserVo {
private String userName;
private String sex;
private Integer age;
}
- 定义一个Mapper接口
接口中的方法就是要实现对象类型转换的方法
注意:
- 方法参数是源对象类型
- 方法返回值类型是要转换成的目标对象类型
在这个接口中我们定义了一个方法toUserVo
,参数类型是源对象类型User
,方法返回值类型是我们需要转换成的目标对象类型UserVo
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;
@Mapper
public interface ObjectConventor {
UserVo toUserVo(User user);
}
- 示例
public static void main(String[] args) {
User user = new User();
user.setUserName("XiaoMing");
user.setAge(18);
user.setSex("M");
user.setPassword("123456");
// 获取自动生成的映射器的实现类
ObjectConventor mapper = Mappers.getMapper(ObjectConventor.class);
// 调用对象类型转换方法
UserVo userVo = mapper.toUserVo(user);
System.out.println(userVo);
}
在编译期间,会自动根据Mapper接口中的方法的参数和返回值类型自动生成实现类
我们使用Mappers.getMapper
即可获取到指定的Mapper接口的实现类,调用方法即可完成对象之间的转换。
我们来看一下UserVo toUserVo(User user)
这个方法的实现,就是利用setter、getter生成的
在Spring中使用
普通使用MapStruct我们需要手动获取mapper的实现类,但是在Spring中,我们可以直接从容器中通过依赖住的方式来获取Mapper的实现类。
只需要在Mapper注解中设置属性componentModel
为字符串spring
@Mapper(componentModel = "spring")
public interface ObjectConventor {
UserVo toUserVo(User user);
}
当然也可以使用MapStruct中内置的常量,如下所示
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ObjectConventor {
UserVo toUserVo(User user);
}
在需要使用的地方通过@Autowired
注入即可使用,示例
@Service
public class UserServiceImpl implements UserService {
@Autowired
ObjectConventor objectConventor;
}
注意:@Mapper.component
的值有多个,说两个常用的:
- default:默认值,需要手动通过
Mappers.getMapper()
来获取mapper的实现类 - spring:可以通过Spring容器依赖注入的方式获取mapper的实现类
常规使用
mapping
在MapStruct中,若两个类型中的属性名称相同,则会自动完成转换。
如果遇到对象类型的属性名称不一致,则可以利用@Mapping
在接口方法上进行标注,
例如User.image
要转换UserVo.avatarUrl
target用来指定要处理的目标类型的属性,source用来指定源类型的属性名
@Mapper
public interface ObjectConventor {
@Mapping(target = "avatarUrl", source = "image")
UserVo toUserVo(User user);
}
mapping.expression
如果属性名称相同,但属性类型不相同:
- 如果是基本数据类型和包装类之间转换,mapstruct会进行拆箱、装箱,我们不需要处理
- 包装类与String之间的转换,mapStruct自动处理,不需要处理
- 日期与String之间的转换
除了以上情况之外的类型不相同,我们需要借助@Mapping.expression
手动进行处理,
使用@Mapping.expression
可以完成对目标属性的特殊处理
例如:User.address
是Object类型,要转换为UserVo.address
为String类型
@Mapper
public interface ObjectConventor {
@Mapping(target = "avatarUrl", source = "image")
@Mapping(target = "address", expression = "java(source.getAddress().getProvince() + source.getAddress().getCity() )")
UserVo toUserVo(User source);
}
查看生成的代码
注意:在@Mapping
中不可以同时出现source
和expression
,因为MapStruct会把整个express作为整体去xxx.setTarget(expression)
执行
常量赋值
如果要给对象中的属性赋值常量,则可以使用constant
属性
注意:constant与source不可同时出现
@Mapper
public interface ObjectConventor {
@Mapping(target = "avatarUrl", source = "image")
@Mapping(target = "country", constant = "China")
UserVo toUserVo(User source);
}
空值处理
当source中的属性为null时,可以我target赋值默认值
defaultValue
默认值defaultExpression
默认表达式
@Mapper
public interface ObjectConventor {
@Mapping(target = "avatarUrl", source = "image")
@Mapping(target = "sex", source = "sex", defaultValue = "未知")
@Mapping(target = "status", source = "disable", defaultExpression = "java( com.demo.utils.UserUtils.getDefaultStatus() )")
UserVo toUserVo(User source);
}
@Mappings
一个方法上有多个Mapping,可以统一放到@Mappings
中,
@Mapper
public interface ObjectConventor {
@Mappings({
@Mapping(target = "avatarUrl", source = "image"),
@Mapping(target = "sex", source = "sex", defaultValue = "未知"),
@Mapping(target = "status", source = "disable", defaultExpression = "java( com.demo.utils.UserUtils.getDefaultStatus() )")
})
UserVo toUserVo(User source);
}
嵌套类型处理
通过属性.属性
的方式即可
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ObjectConventor {
@Mapping(target = "country", source = "address.country")
UserVo toUserVo(User source);
}
如果是复杂的嵌套类型,则推荐额外再定义一个类型转换方法,MapStruct会自动去调用
SpringBean注入
在Mapper中注入Spring Bean,实现在mapper调用SpringBean去处理
由于interfac无法实现实现依赖注入,因此需要修改Mapper为抽象类abstract class
,所有的方法也都需要修改成abstract
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public abstract class ObjectConventor {
@Autowired
UserService userService;
@Mapping(target = "roles", expression = "java( userService.getRoles(source.getUserName()) )")
abstract UserVo toUserVo(User source);
}
自定义映射逻辑
由于Java8中的interface支持default方法,因此可以在Mapper中自定义映射逻辑
示例:
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ObjectConventor {
default UserVo toUserVo(User user) {
UserVo userVo = new UserVo();
userVo.setUserName(user.getUserName());
userVo.setAge(user.getAge());
return userVo;
}
}
日期格式化
Date与String之间的转换,可以规定日期格式
生成的代码会自动去调用SimpleDateFormat去格式化日期
@Mapper(componentModel = MappingConstants.ComponentModel.SPRING)
public interface ObjectConventor {
@Mapping(target = "createDate", dateFormat = "yyyy-MM-dd HH:mm:ss")
UserVo toUserVo(User source);
}
踩坑点注意
-
修改了属性类型或新增、减少了属性,生成的实现类中并没有变化
解决方法:需要先手动删除maven打包后的文件,然后再重新启动程序即可
-
生成的对象中的属性全部为null,并且生成的实现类中并没有使用getter、setter
解释:由于lombok和MapStruct都是作用于编译期间,由于MapStruct和Lombok的工作顺序问题,MapStruct在Lombok之前执行,MapStruct检测此时的参数类型和返回值类型还没有getter、setter,导致没有去调用getter、setter
解决方式:调整pom.xml
中Lombok和MapStruct的引入顺序,要保证Lombok写在MapStruct之前
- 要完成集合元素类型之间的转换,为保证MapStruct能够正确生成目标对象,需要先定义单个对象转换的方法。
示例:要完成List<User>
到List<UserVo>
的转换,不可以在Mapper只定义这一个方法,也需要将单个元素之间的转换User
转换UserVo
的方法定义出来
正确示例: