MapStruct

导读:本篇文章讲解 MapStruct,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

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

MapStruct

MapStruct MapStruct 

从debug和执行结果可以看出可以同时改变原对象和目标对象的值

编译过后会生成如下文件

MapStruct

 

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生成的代码是相同的。

参考:MapStruct使用指南 – 知乎

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/95904.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!