MapStruct的引入与使用

1 引入

在实际开发中,一个web应用通常会被分为三层,分别为持久层,业务层,控制层,每层各有各的职责,所以针对同一个请求,每层返回的对象应该是不同的。

  • 持久层:主要负责访问数据库中的数据,并将数据库中的数据封装为PO、DO、Entity 对象,对上层屏蔽数据访问细节。持久层不关心业务,只关心数据,因此对象属性和数据库表字段一一对应
  • 业务层:业务层从持久层获取封装数据,并根据具体的业务逻辑计算,得到业务层的计算结果,用DTO对象来封装。DTO对象的全称是 Data Tranfer Object。
  • 控制层:控制层获取业务层的业务处理结果之后,还可能需要将其加工成前端所需要的格式,封装成VO对象返回给前端显示。

MapStruct引入

所以,在处理一个请求的时候业务层需要完成PO—>DTO对象的转化,控制层需要完成DTO—>VO对象的转化。而对象转化本身就是纯粹的“体力活”没有任何技术含量。

  // 待转化的PO对象 
  XxxPO sourcePO = ...
  
  // 目标DTO对象
  XxxDTO destDTO = new XxxDTO();
  
  //通过一堆get/set方法完成转化
  destDTO.setXxx(sourcePO.getXxx());
  ...

对于这种没有技术含量的活,有追求的程序员是不屑做的,但是在项目中我们又必须要完成,怎么办呢?于是就有了Mapstruct来帮我们完成对象转化的工作。

2 使用

2.1 导入依赖

    <dependencies>
        <!--spring boot-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.3.3.RELEASE</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.28</version>
        </dependency>
        <!--mapstruct-->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-jdk8</artifactId>
            <version>1.3.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <version>1.3.0.Final</version>
        </dependency>
    </dependencies>

2.2 定义转化器接口

@Mapper(componentModel = "spring") 
public interface XxxConverter {
   // 定义转化方法将source原对象转化为目标对象
   目标类 方法名(待转化类 source);
}

2.3 使用定义的转化器接口

   @Autowired
   XxxConverter xxxConverter;

    @Test
    public void testMapStruct() {
     目标类 dest = xxxConverter.方法(users);
    }

mapstruct底层是怎么做的呢?
其实mapstruct是一个编译时的技术
可以在编译的时候,帮助我们生成接口的实例对象,并且实现接口中的方法

3 代码案例

3.1 简单对象转化

@Data
public class Doctor {
    private int id;
    private String name;
}
@Data
public class DoctorDTO {
    private int id;
    private String name;
}

在源对象(Doctor)和目标对象(DoctorDTO)的属性完全相同,我们可以简单定义转化器接口如下

@Mapper(componentModel = "spring")
public interface DoctorConverter {

    DoctorDTO convertDoctorToDTO(Doctor doctor);
}

然后再需要的地方注入Converter对象,调用转化方法即可

@SpringBootTest(classes = MapStructApplication.class)
@RunWith(SpringRunner.class)
public class BeseTest {

    @Autowired
    DoctorConverter doctorConverter;

    /**
     * 简单对象转化
     */
    @Test
    public void testDoctor2DTO(){
        Doctor doctor = new Doctor();
        doctor.setId(1001);
        doctor.setName("关羽");

        DoctorDTO doctorDTO = doctorConverter.convertDoctorToDTO(doctor);
        System.out.println("doctorDTO = " + doctorDTO);

    }
}

3.2 不同属性名的映射

注意事项:

  1. 默认情况下,是通过名字来映射的
    如果名字一致,那么可以直接转化
    如果名字不一致,那么需要在对应的方法上 加上 @Mapping(source=“”, target=“”)注解

假设我们给医生增加一个昵称属性,该属性在Doctor类中叫nickname,在DoctorDTO中叫username,属性名不一致,我们仍然可以完成转化

@Data
public class Doctor {
    private int id;
    private String name;
    private String nickname;
}
@Data
public class DoctorDTO {
    private int id;
    private String name;
    private String username;
}

定义转化器接口

@Mapper(componentModel = "spring")
public interface DoctorConverter {

    @Mapping(source = "nickname", target = "username")
    DoctorDTO convertDoctorToDTO(Doctor doctor);
}

然后再需要的地方注入Converter对象,调用转化方法

@SpringBootTest(classes = MapStructApplication.class)
@RunWith(SpringRunner.class)
public class BeseTest {

    @Autowired
    DoctorConverter doctorConverter;

    /**
     * 不同属性名的映射
     */
    @Test
    public void testDoctor2DTO(){
        Doctor doctor = new Doctor();
        doctor.setId(1001);
        doctor.setName("关羽");
        doctor.setNickname("武圣");

        DoctorDTO doctorDTO = doctorConverter.convertDoctorToDTO(doctor);
        System.out.println("doctorDTO = " + doctorDTO);

    }
}

3.3 多个不同类型源对象的转化

有时候在转化一个对象的时候,涉及另外的多个对象的属性值,此时我们就可以把多个对象的属性值,赋值给目标对象

多个不同类型源

@Data
public class Education {
    // 学位
    private String degreeName;
    // 学校
    private String institute;
    // 毕业年份
    private Integer yearOfPassing;
}
@Data
public class Doctor {
    private int id;
    private String name;
    private String nickname;
}
@Data
public class DoctorDTO {
    private int id;
    private String name;

    private String username;

    // 从 education对象中获取
    private String degree;
}

定义转化器接口

@Mapper(componentModel = "spring")
public interface DoctorConverter {

    // 多个源对象的话,在指定源对象属性时 通过对象名.属性名的方式指定
    @Mapping(source = "doctor.nickname", target = "username")
    @Mapping(source = "education.degreeName", target = "degree")
    DoctorDTO converEduAndDoctorToDTO(Education education, Doctor doctor);
}

然后再需要的地方注入Converter对象,调用转化方法

@SpringBootTest(classes = MapStructApplication.class)
@RunWith(SpringRunner.class)
public class BeseTest {

    @Autowired
    DoctorConverter doctorConverter;

    /**
     * 多个不同类型源对象的转化
     */
    @Test
    public void testConvertEduAndDoctor2DTO(){

        Doctor doctor = new Doctor();
        doctor.setId(1002);
        doctor.setName("汪峰");
        doctor.setNickname("汪半壁");

        Education education = new Education();
        education.setInstitute("加里顿皇家医学院");
        education.setDegreeName("本科");
        education.setYearOfPassing(2008);

        DoctorDTO doctorDTO = doctorConverter.converEduAndDoctorToDTO(education, doctor);
        System.out.println("doctorDTO = " + doctorDTO);
    }
}

3.4 转化复杂对象

如果一个对象持有了另外一个对象,或者另外一个对象的List,Mapstruct还可以帮我们实现类似“深度克隆”的”深度转化“。

@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 nickname;
    // 医生有患者
    private Patient patient;
}
@Data
public class DoctorDTO {
    private int id;
    private String name;

    private String username;

    // 医生有患者
    private PatientDTO patientDTO;
}

定义转化器Converter

@Mapper(componentModel = "spring")
public interface DoctorConverter {

    @Mapping(source = "nickname", target = "username")
    @Mapping(source = "patient", target = "patientDTO")
    DoctorDTO convert2DTOWithPatient(Doctor doctor);

    /**
     *自己定义一个内部持有对象的转化方法
     * 如果自己定义了,那么就会使用自己定义的方法
     */
    PatientDTO convert2PatientDTO(Patient patient);
}

然后再需要的地方注入Converter对象,调用转化方法

@SpringBootTest(classes = MapStructApplication.class)
@RunWith(SpringRunner.class)
public class BeseTest {

    @Autowired
    DoctorConverter doctorConverter;


    /**
     * 持有对象的转化
     */
    @Test
    public void testConvert2DTOWithPatient() {
        Doctor doctor = new Doctor();
        doctor.setId(1003);
        doctor.setName("萧亚轩");
        doctor.setNickname("小天才");

        Patient patient = new Patient();
        patient.setId(2001);
        patient.setName("吴彦祖");

        doctor.setPatient(patient);

        DoctorDTO doctorDTO = doctorConverter.convert2DTOWithPatient(doctor);
        System.out.println(doctorDTO);

    }
}

这里要注意的是,在Doctor对象持有了一个Patient,但是当我们调用Converter转化器的doctorPO2DTO方法时,Mapstruct在转化Doctor对象的时候,也会把Patient对象转化为PatientDTO对象。原因是:

  • 我们在Converter转化器中定义了如下转化方法: PatientDTO patientPO2DTO(Patient patient);

  • 当转化器在Converter在执行complicatedDoctorPO2DTO方法转化Doctor对象的过程中,遇到Patient patient属性时,Converter会“自动发现”patientPO2DTO方法,将源对象中的Patient 对象转化为PatientDTO对象

  • “自动发现”其实就是用Doctor的源对象目标属性patient的类型,和某个Converter转化器中方法的入参做类型匹配,同时,用目标对象的目标属性patientDTO和该方法的返回值类型做类型匹配

  • 如果类型都匹配上了,就会自动使用这个转化器方法来完成源对象属性和目标对象属性之间的转化

3.5 转化List

@Data
public class Doctor {
    private int id;
    private String name;
    private String nickname;
    // 医生有患者
    private Patient patient;
}
@Data
public class DoctorDTO {
    private int id;
    private String name;

    private String username;

    // 从 education对象中获取
    private String degree;

    // 医生有患者
    private PatientDTO patientDTO;
}

定义转化器Converter

@Mapper(componentModel = "spring")
public interface DoctorConverter {

    @Mapping(source = "nickname", target = "username")
    DoctorDTO convertDoctorToDTO(Doctor doctor);

    @Mapping(source = "doctor.nickname", target = "username")
    @Mapping(source = "education.degreeName", target = "degree")
    DoctorDTO converEduAndDoctorToDTO(Education education, Doctor doctor);

    @Mapping(source = "nickname", target = "username")
    @Mapping(source = "patient", target = "patientDTO")
    DoctorDTO convert2DTOWithPatient(Doctor doctor);

    /**
     *自己定义一个内部持有对象的转化方法
     * 如果自己定义了,那么就会使用自己定义的方法
     */
    PatientDTO convert2PatientDTO(Patient patient);


    /**集合与集合的转化,依赖与单个bean的转化方法
    *比如当前的方法,依赖于 doctor 转化为 doctorDTO的转化方法
     * */
    List<DoctorDTO> convert2List(List<Doctor> doctorList);
}

然后再需要的地方注入Converter对象,调用转化方法

@SpringBootTest(classes = MapStructApplication.class)
@RunWith(SpringRunner.class)
public class BeseTest {

    @Autowired
    DoctorConverter doctorConverter;


    /**
     * list的转化
     */
    @Test
    public void testListConvert(){

        Doctor doctor1 = new Doctor();
        Doctor doctor2 = new Doctor();
        Doctor doctor3 = new Doctor();

        doctor1.setId(1001);
        doctor1.setName("孙悟空");
        doctor1.setNickname("大圣");

        doctor2.setId(1002);
        doctor2.setName("猪悟能");
        doctor2.setNickname("呆子");

        doctor3.setId(1003);
        doctor3.setName("沙悟净");
        doctor3.setNickname("沙和尚");

        List<Doctor> doctorList = Arrays.asList(doctor1, doctor2, doctor3);

        List<DoctorDTO> doctorDTOList = doctorConverter.convert2List(doctorList);

        System.out.println(doctorDTOList);
    }
}

注意:此时运行程序可能会报错如下所属错误:

方法模糊匹配导致报错

发生错误的原因是集合与集合的转化,依赖与单个bean的转化方法,而当前的方法依赖于 doctor 转化为 doctorDTO的转化方法,在DoctorConverter接口中,定义了两个doctor 转化为 doctorDTO的转化方法,导致转化方法的模糊匹配。

两个doctor 转化为 doctorDTO的转化方法

解决方案:注释掉其中任一个 doctor 转化为 doctorDTO的转化方法,重新运行程序,获取成功。

获取成功

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值