copyProperties()
在项目中经常需要进行对象之间的转换,比如将Entity转换成VO传给前端。
我们可以使用Mybatis-plus中page自带的convert方法来进行转换
IPage<User> userPage = this.baseMapper.selectPage(new Page<>(pageNum, pageSize), wrapper);
IPage<UserVO> convert = userPage.convert(user -> {
UserVO vo = new UserVO();
BeanUtils.copyProperties(user, vo);
return vo;
});
通过 BeanUtils.copyProperties() 方法可以将我们的User实体类转换成UserVO,但是该方法是使用反射的方式,对性能的消耗比较大。而且会将我们UserPage中存储的User对象转换成UserVO。
MapStruct
MapStruct就是一个属性映射工具。与手动编写映射代码相比,MapStruct通过生成繁琐且易于出错的代码来节省时间。约定优于配置的方式。
和动态映射框架相比,MapStruct具有以下优点:
- 通过使用普通方法调用而不是反射来快速执行
- 编译时类型安全性
- 构建是清除错误报告
- 映射不完整(并非所有target属性都被映射)
- 映射不正确(找不到正确的映射方法或类型转换)
导入依赖
如果项目同时导入了lombok依赖,则lombok依赖必须放在mapstruct依赖上面
若导入了swagger依赖,则需要将swagge依赖中关于mapstruct的依赖剔除
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.3.1.Final</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>1.3.1.Final</version>
</dependency>
package com.yang.mapstruct;
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
@Mapping(target = "userName", source = "name")
UserVO convertUserToUserVO(User user);
List<UserVO> convertUserToUserVOs(List<User> list);
@AfterMapping
default void userToUserVO(User user, @MappingTarget UserVO userVO) {
userVO.setAge(user.getAge()+ 999);
user.setAge(1024);
}
}
public PageResVO<UserVO> listByQuery(UserDTO userDTO) {
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
String orderBy = StringUtils.isBlank(userDTO.getIsAsc()) ? "" : userDTO.getIsAsc();
wrapper.like(StringUtils.isNotBlank(userDTO.getName()), User::getName, userDTO.getName())
.like(StringUtils.isNotBlank(userDTO.getProfession()), User::getProfession, userDTO.getProfession())
.ge(Objects.nonNull(userDTO.getCreateTimeMin()), User::getCreateTime, userDTO.getCreateTimeMin())
.lt(Objects.nonNull(userDTO.getCreateTimeMax()), User::getCreateTime, userDTO.getCreateTimeMax())
.last(StringUtils.isNotBlank(userDTO.getOrderByColumn()),
Constant.ORDER_BY_BLANK + com.baomidou.mybatisplus.core.toolkit.StringUtils.camelToUnderline(userDTO.getOrderByColumn()) + " " + orderBy);
IPage<User> page = this.baseMapper.selectPage(new Page<>(userDTO.getPageNum(), userDTO.getPageSize()), wrapper);
List<UserVO> userVOS = UserConvert.INSTANCE.convertUserToUserVOs(page.getRecords());
return PageResVO.getBean(page, userVOS);
}
从debug和执行结果可以看出可以同时改变原对象和目标对象的值
编译过后会生成如下文件
package com.yang.mapstruct;
import com.yang.entity.User;
import com.yang.vo.UserVO;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
@Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-07-17T17:07:16+0800",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_131 (Oracle Corporation)"
)
public class UserConvertImpl implements UserConvert {
@Override
public UserVO convertUserToUserVO(User user) {
if ( user == null ) {
return null;
}
UserVO userVO = new UserVO();
userVO.setUserName( user.getName() );
userVO.setProfession( user.getProfession() );
userVO.setAge( user.getAge() );
userVO.setStatus( user.getStatus() );
userVO.setCreateTime( user.getCreateTime() );
userToUserVO( user, userVO );
return userVO;
}
@Override
public List<UserVO> convertUserToUserVOs(List<User> list) {
if ( list == null ) {
return null;
}
List<UserVO> list1 = new ArrayList<UserVO>( list.size() );
for ( User user : list ) {
list1.add( convertUserToUserVO( user ) );
}
return list1;
}
}
该方法调用了我们的set方法来进行对应的属性注入,然后再调用由@AfterMapping修饰的方法,
多个类源
有时,单个类不足以构建VO,我们可能希望将多个类中的值聚合为一个VO,供终端用户使用。这也可以通过@Mapping注解中设置适当的标志来完成。
@Mapping(source = "user.name", target = "name")
@Mapping(source = "sku.age", target = "age")
UserVO convertToUserVO(User user, Sku sku);
如果 User 类和 Sku 类包含同名的字段,我们必须让映射器知道使用哪一个,否则它会抛出一个异常。举例来说,如果两个模型都包含一个 name 字段,我们就要选择将哪个类中的 name 映射到VO属性中。
子对象映射
在多数情况下,VO还会包含其它类,例如,一个 Doctor 类中会包含 Patient
@Data
public class Patient {
private int id;
private String name;
}
@Data
public class PatientDto {
private int id;
private String name;
}
@Data
public class Doctor {
private int id;
private String name;
private String specialty;
private List<Patient> patientList;
}
@Data
public class DoctorVO {
private int id;
private String name;
private String degree;
private String specialization;
private List<PatientDto> patientDtoList;
}
对于Patient,我们需要创建一个 Patient 到PatientVO的转换
@Mapper
public interface PatientMapper {
PatientMapper INSTANCE = Mappers.getMapper(PatientMapper.class);
PatientDto toDto(Patient patient);
}
Docter转换器
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor);
}
public class DoctorMapperImpl implements DoctorMapper {
private final PatientMapper patientMapper = Mappers.getMapper( PatientMapper.class );
@Override
public DoctorDto toDto(Doctor doctor) {
if ( doctor == null ) {
return null;
}
DoctorDtoBuilder doctorDto = DoctorDto.builder();
doctorDto.patientDtoList( patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.specialization( doctor.getSpecialty() );
doctorDto.id( doctor.getId() );
doctorDto.name( doctor.getName() );
return doctorDto.build();
}
protected List<PatientDto> patientListToPatientDtoList(List<Patient> list) {
if ( list == null ) {
return null;
}
List<PatientDto> list1 = new ArrayList<PatientDto>( list.size() );
for ( Patient patient : list ) {
list1.add( patientMapper.toDto( patient ) );
}
return list1;
}
}
显然,除了toDto()映射方法外,最终实现中还添加了一个新的映射方法——patientListToPatientDtoList()。这个方法是在没有显式定义的情况下添加的,只是因为我们把PatientMapper添加到了DoctorMapper中。
该方法会遍历一个Patient列表,将每个元素转换为PatientDto,并将转换后的对象添加到DoctorDto对象内中的列表中。
数据类型转换
MapStruct支持source和target属性之间的数据类型转换。它还提供了基本类型及其相应的包装类之间的自动转换。
自动类型转换适用于:
- 基本类型及其对应的包装类之间。比如, int 和 Integer, float 和 Float, long 和 Long,boolean 和 Boolean 等。
- 任意基本类型与任意包装类之间。如 int 和 long, byte 和 Integer 等。
- 所有基本类型及包装类与String之间。如 boolean 和 String, Integer 和 String, float 和 String 等。
- 枚举和String之间。
- Java大数类型(java.math.BigInteger, java.math.BigDecimal) 和Java基本类型(包括其包装类)与String之间。
- 其它情况详见MapStruct官方文档。
因此,在生成映射器代码的过程中,如果源字段和目标字段之间属于上述任何一种情况,则MapStrcut会自行处理类型转换。
@Mapper
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
}
public class PatientMapperImpl implements PatientMapper {
@Override
public Patient toModel(PatientDto patientDto) {
if (patientDto == null) {
return null;
}
PatientBuilder patient = Patient.builder();
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ofPattern("dd/MMM/yyyy")
.format(patientDto.getDateOfBirth()));
}
patient.id(patientDto.getId());
patient.name(patientDto.getName());
return patient.build();
}
}
可以看到,这里使用了 dateFormat 声明的日期格式。如果我们没有声明格式的话,MapStruct会使用 LocalDate的默认格式,大致如下:
if (patientDto.getDateOfBirth() != null) {
patient.dateOfBirth(DateTimeFormatter.ISO_LOCAL_DATE
.format(patientDto.getDateOfBirth()));
}
依赖注入
到目前为止,我们一直在通过getMapper()方法访问生成的映射器:
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
但是,如果你使用的是Spring,只需要简单修改映射器配置,就可以像常规依赖项一样注入映射器。
修改 DoctorMapper 以支持Spring框架:
@Mapper(componentModel = "spring")
public interface DoctorMapper {}
在@Mapper注解中添加(componentModel = "spring"),是为了告诉MapStruct,在生成映射器实现类时,我们希望它能支持通过Spring的依赖注入来创建。现在,就不需要在接口中添加 INSTANCE 字段了。
这次生成的 DoctorMapperImpl 会带有 @Component 注解:
@Component
public class DoctorMapperImpl implements DoctorMapper {}
只要被标记为@Component,Spring就可以把它作为一个bean来处理,你就可以在其它类(如控制器)中通过@Autowire注解来使用它:
@Controller
public class DoctorController() {
@Autowired
private DoctorMapper doctorMapper;
}
添加默认值
@Mapping 注解有两个很实用的标志就是常量 constant 和默认值 defaultValue 。无论source如何取值,都将始终使用常量值; 如果source取值为null,则会使用默认值。
修改一下 DoctorMapper ,添加一个 constant 和一个 defaultValue :
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(target = "id", constant = "-1")
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization", defaultValue = "Information Not Available")
DoctorDto toDto(Doctor doctor);
}
如果specialty不可用,我们会替换为"Information Not Available"字符串,此外,我们将id硬编码为-1。
生成代码如下:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDto(Doctor doctor) {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
if (doctor.getSpecialty() != null) {
doctorDto.setSpecialization(doctor.getSpecialty());
}else {
doctorDto.setSpecialization("Information Not Available");
}
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor.getPatientList()));
doctorDto.setName(doctor.getName());
doctorDto.setId(-1);
return doctorDto;
}
}
可以看到,如果 doctor.getSpecialty() 返回值为null,则将specialization设置为我们的默认信息。无论任何情况,都会对 id赋值,因为这是一个constant。
@BeforeMapping 和 @AfterMapping
为了进一步控制和定制化,我们可以定义 @BeforeMapping 和 @AfterMapping方法。显然,这两个方法是在每次映射之前和之后执行的。也就是说,在最终的实现代码中,会在两个对象真正映射之前和之后添加并执行这两个方法。
可以在 DoctorCustomMapper中添加两个方法:
@Mapper(uses = {PatientMapper.class}, componentModel = "spring")
public abstract class DoctorCustomMapper {
@BeforeMapping
protected void validate(Doctor doctor) {
if(doctor.getPatientList() == null){
doctor.setPatientList(new ArrayList<>());
}
}
@AfterMapping
protected void updateResult(@MappingTarget DoctorDto doctorDto) {
doctorDto.setName(doctorDto.getName().toUpperCase());
doctorDto.setDegree(doctorDto.getDegree().toUpperCase());
doctorDto.setSpecialization(doctorDto.getSpecialization().toUpperCase());
}
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
public abstract DoctorDto toDoctorDto(Doctor doctor);
}
基于该抽象类生成一个映射器实现类:
@Component
public class DoctorCustomMapperImpl extends DoctorCustomMapper {
@Autowired
private PatientMapper patientMapper;
@Override
public DoctorDto toDoctorDto(Doctor doctor) {
validate(doctor);
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(doctor.getId());
doctorDto.setName(doctor.getName());
updateResult(doctorDto);
return doctorDto;
}
}
可以看到, validate() 方法会在 DoctorDto 对象实例化之前执行,而updateResult()方法会在映射结束之后执行。
映射异常处理
异常处理是不可避免的,应用程序随时会产生异常状态。MapStruct提供了对异常处理的支持,可以简化开发者的工作。
考虑这样一个场景,我们想在 Doctor 映射为DoctorDto之前校验一下 Doctor 的数据。我们新建一个独立的 Validator 类进行校验:
public class Validator {
public int validateId(int id) throws ValidationException {
if(id == -1){
throw new ValidationException("Invalid value in ID");
}
return id;
}
}
我们修改一下 DoctorMapper 以使用 Validator 类,无需指定实现。跟之前一样, 在@Mapper使用的类列表中添加该类。我们还需要做的就是告诉MapStruct我们的 toDto() 会抛出 throws ValidationException:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctor.patientList", target = "patientDtoList")
@Mapping(source = "doctor.specialty", target = "specialization")
DoctorDto toDto(Doctor doctor) throws ValidationException;
}
最终生成的映射器代码如下:
@Component
public class DoctorMapperImpl implements DoctorMapper {
@Autowired
private PatientMapper patientMapper;
@Autowired
private Validator validator;
@Override
public DoctorDto toDto(Doctor doctor) throws ValidationException {
if (doctor == null) {
return null;
}
DoctorDto doctorDto = new DoctorDto();
doctorDto.setPatientDtoList(patientListToPatientDtoList(doctor
.getPatientList()));
doctorDto.setSpecialization(doctor.getSpecialty());
doctorDto.setId(validator.validateId(doctor.getId()));
doctorDto.setName(doctor.getName());
doctorDto.setExternalId(doctor.getExternalId());
doctorDto.setAvailability(doctor.getAvailability());
return doctorDto;
}
}
MapStruct自动将doctorDto的id设置为Validator实例的方法返回值。它还在该方法签名中添加了一个throws子句。
注意,如果映射前后的一对属性的类型与Validator中的方法出入参类型一致,那该字段映射时就会调用Validator中的方法,所以该方式请谨慎使用。
映射配置
MapStruct为编写映射器方法提供了一些非常有用的配置。多数情况下,如果我们已经定义了两个类型之间的映射方法,当我们要添加相同类型之间的另一个映射方法时,我们往往会直接复制已有方法的映射配置。
其实我们不必手动复制这些注解,只需要简单的配置就可以创建一个相同/相似的映射方法。
继承配置
我们回顾一下“更新现有实例”,在该场景中,我们创建了一个映射器,根据DoctorDto对象的属性更新现有的Doctor对象的属性值:
@Mapper(uses = {PatientMapper.class})
public interface DoctorMapper {
DoctorMapper INSTANCE = Mappers.getMapper(DoctorMapper.class);
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
假设我们还有另一个映射器,将 DoctorDto转换为 Doctor :
@Mapper(uses = {PatientMapper.class, Validator.class})
public interface DoctorMapper {
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
@Mapping(source = "doctorDto.specialization", target = "specialty")
Doctor toModel(DoctorDto doctorDto);
}
这两个映射方法使用了相同的注解配置, source和 target都是相同的。其实我们可以使用@InheritConfiguration注释,从而避免这两个映射器方法的重复配置。
如果对一个方法添加 @InheritConfiguration 注解,MapStruct会检索其它的已配置方法,寻找可用于当前方法的注解配置。一般来说,这个注解都用于mapping方法后面的update方法,如下所示:
@Mapper(uses = {PatientMapper.class, Validator.class}, componentModel = "spring")
public interface DoctorMapper {
@Mapping(source = "doctorDto.specialization", target = "specialty")
@Mapping(source = "doctorDto.patientDtoList", target = "patientList")
Doctor toModel(DoctorDto doctorDto);
@InheritConfiguration
void updateModel(DoctorDto doctorDto, @MappingTarget Doctor doctor);
}
继承逆向配置
还有另外一个类似的场景,就是编写映射函数将 Model 转为 DTO,以及将 DTO 转为 Model。如下面的代码所示,我们必须在两个函数上添加相同的注释。
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
PatientDto toDto(Patient patient);
}
两个方法的配置不会是完全相同的,实际上,它们应该是相反的。将***Model*** 转为 DTO,以及将 DTO 转为 Model——映射前后的字段相同,但是源属性字段与目标属性字段是相反的。
我们可以在第二个方法上使用@InheritInverseConfiguration注解,避免写两遍映射配置:
@Mapper(componentModel = "spring")
public interface PatientMapper {
@Mapping(source = "dateOfBirth", target = "dateOfBirth", dateFormat = "dd/MMM/yyyy")
Patient toModel(PatientDto patientDto);
@InheritInverseConfiguration
PatientDto toDto(Patient patient);
}
这两个Mapper生成的代码是相同的。