场景
通常,在后端项目开发中,因为会有项目分层的设计,例如 MVC 架构,以及最近很火热的 DDD 架构中,会在不同的层级,有对应的 DO,BO,VO,DTO 等各种各样的 POJO 类,而我们在层级之间进行调用的数据传递时,通常要进行对象属性之间的映射。对于一些简单的对象,可能会直接使用 get,set 方法完成,或者使用 BeanUtils 工具类来完成属性之间的映射。
这些代码往往是枯燥、无聊的,并且在不同的业务处理类中可能需要重复地对两个对象进行互相转换。导致代码里充斥着大量的 get,set 转换,如果使用 BeanUtils,可能会因为字段名称不一致,导致在运行时才能发现问题。
那有没有什么方案能解决这个问题呢?
答案就是使用 MapStruct,可以优雅地解决上面的这些问题。
MapStruct 是一种代码生成器组件,它遵循约定优于配置的原则,可以让我们的 Bean 对象之间的转换变得更简单。
为什么要使用 MapStruct?
如前文中描述,在多层应用设计中,需要在不同的对象模型之间进行转换,属性映射,手动编写这些代码不仅繁琐,而且很容易出错,MapStruct 的目的是让这项工作变得简单,自动化。
相比其他的映射框架,比如 BeanUtils,或者 Json 序列化反序列化等方式,MapStruct 能在编译时就生成映射,确保程序运行性能,并且能在编译时就发现错误。
MapStruct 怎么用?
MapStruct 本质上是一个注解处理器,可以直接在 Maven 或 Gradle 等编译工具中集成。
以 Maven 为例,我们需要先在依赖中添加 MapStruct 依赖,并将mapstruct-processor
配置在 maven 插件中。
<properties>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
复制代码
接下来,便可以在代码中使用 MapStruct。
同名字段映射
比如,我们现在有一个功能是要从持久层查询学生对象 Student,然后将它转换为 StudentDTO 传递给业务层。这里我们需要在 Student 对象和 StudentDTO 对象之间进行转换。
Student.java
@Data
public class Student{
private int no;
private String name;
}
复制代码
StudentDTO.java
@Data
public class StudentDTO {
private int no;
private String name;
}
复制代码
针对这种对象之间的转换,我们需要创建一个 Mapper 类进行映射。
@Mapper(componentModel = "spring")
public interface StudentMapper {
StudentDTO toDto(Student student);
}
复制代码
@Mapper :是 MapStruct 的注解,用于创建和生成映射的实现。
componentModel = "spring":该属性的含义是将 StudentMapper 的实例作为 Spring 的 Bean 对象,放在 Spring 容器中,这样就可以在其他业务代码中方便的注入。
因为 Student 和 StudentDTO 的属性名相同,所以我们不需要任何其他代码显式映射。
不同名字段映射
假如 DTO 和 PO 之间的字段名称不同,应该如何处理呢?
Student.java
@Data
public class Student{
private int no;
private String name;
private String gender;
}
复制代码
StudentDTO.java
@Data
public class StudentDTO {
private int no;
private String name;
private String sex;
}
复制代码
如上代码所示,在 Student 和 StudentDTO 中,性别字段的名称不一致。要实现这种情况的映射,只需要添加如下的 @Mapping 注解。
@Mapper(componentModel = "spring")
public interface StudentMapper {
@Mapping(source = "gender", target = "sex")
StudentDTO toDto(Student student);
}
复制代码
自定义对象属性映射
假如每个学生有自己的地址信息 Address,那么该如何处理呢?
源对象类
@Data
public class Student{
private int no;
private String name;
private String gender;
private Address address;
}
@Data
public class Address{
private String city;
private String province;
}
复制代码
目标对象类
@Data
public class StudentDTO{
private int no;
private String name;
private String sex;
private AddressDTO address;
}
@Data
public class AddressDTO{
private String city;
private String province;
}
复制代码
这种要对内部对象进行映射,我们需要对内部对象也创建一个 Mapper,然后将内部对象的 Mapper 导入到 StudentMapper 中。
@Mapper(componentModel = "spring")
public interface AddressMapper {
AddressDTO toDto(Address address);
}
@Mapper(componentModel = "spring",uses = {AddressMapper.class})
public interface StudentMapper {
@Mapping(source = "gender", target = "sex")
StudentDTO toDto(Student student);
}
复制代码
自定义转换逻辑映射
如果在对象转换时,不仅是简单的属性之间的映射,需要按照某种业务逻辑进行转换,比如每个 Student 中的地址信息 Address,在 StudentDTO 中只需要地址信息中的 city。
源对象类
@Data
public class Student{
private int no;
private String name;
private String gender;
private Address address;
}
@Data
public class Address{
private String city;
private String province;
}
复制代码
目标对象类
@Data
public class StudentDTO{
private int no;
private String name;
private String sex;
private String city;
}
复制代码
针对这种情况,我们可以直接在 source 中使用address.city
,也可以通过自定义方法来完成逻辑转换。
@Mapper(componentModel = "spring",uses = {AddressMapper.class})
public interface StudentMapper {
@Mapping(source = "gender", target = "sex")
// @Mapping(source = "address.city", target = "city")
@Mapping(source = "address", target = "city",qualifiedByName = "getAddressCity")
StudentDTO toDto(Student student);
@Named("getAddressCity")
default String getChildCircuits(Address address) {
if(address == null) {
return "";
}
return address.getCity();
}
}
复制代码
集合映射
使用 MapStruct 进行集合字段的映射也很简单。比如每个 Student 中有多门选修的课程List<Course>
,要映射到 StudentDTO 中的List<CourseDTO>
中。
@Mapper(componentModel = "spring")
public interface CourseMapper {
CourseDTO toDto(Course port);
List<CourseDTO> toCourseDtoList(List<Course> courses);
}
@Mapper(componentModel = "spring",uses = {CourseMapper.class})
public interface StudentMapper {
@Mapping(source = "gender", target = "sex")
@Mapping(source = "address", target = "city",qualifiedByName = "getAddressCity")
CircuitDto toDto(Circuit circuit);
}
复制代码
其他特殊情况映射
除了常见的对象映射外,有些情况我们可能需要在转换时设置一些固定值,假设 StudentDTO 中有学历字段 degree,但是暂时该数据还未录入,所以这里要设置默认值“未知”。可以使用 @Mapping 注解完成。
@Mapping(target = "degree", constant = "未知")
复制代码
@BeforeMapping 和 @AfterMapping
@BeforeMapping
和@AfterMapping
是两个很重要的注解,看名字基本就可以猜到,可以用来在转换之前和之后做一些处理。
比如我们想要在转换之前做一些数据验证,集合初始化等功能,可以使用@BeforeMapping
;
想要在转换完成之后进行一些数据结果的修改,比如根据更加 StudentDTO 中选修课程List<CourseDTO>
的数量来给是否有选修课字段haveCourse
设置布尔值等。
@Mapper(componentModel = "spring",uses = {PortMapper.class})
public interface StudentMapper {
@BeforeMapping
default void setCourses(Student student) {
if(student.getCourses() == null){
student.setCourses(new ArrayList<Course>());
}
}
@Mapping(source = "gender", target = "sex")
StudentDTO toDto(Student student);
@AfterMapping
default void setHaveCourse(StudentDTO studentDto) {
if(studentDto.getCourses()!=null && studentDto.getCourses() >0){
studentDto.setHaveCourse(true);
}
}
}
复制代码
总结
在本文中介绍了如何使用 MapStruct 来优雅的实现对象属性映射,减少我们代码中的 get,set 代码,避免对象属性之间映射的错误。在以上示例代码中可以看出,MapStruct 大量使用了注解,让我们可以轻松完成对象映射。
如果你想了解更多 MapStruct 相关信息,可以继续阅读官方的文档。MapStruct官方文档
我是小黑,一名在互联网“苟且”的程序员
流水不争先,贵在滔滔不绝
评论