通常,在后端项目开发中,因为会有项目分层的设计,例如MVC架构,以及最近很火热的DDD架构中,会在不同的层级,有对应的DO,BO,VO,DTO等各种各样的POJO类,而我们在层级之间进行调用的数据传递时,通常要进行对象属性之间的映射。对于一些简单的对象,可能会直接使用get,set方法完成,或者使用BeanUtils工具类来完成属性之间的映射。
这些代码往往是枯燥、无聊的,并且在不同的业务处理类中可能需要重复地对两个对象进行互相转换。导致代码里充斥着大量的get,set转换,如果使用BeanUtils,可能会因为字段名称不一致,导致在运行时才能发现问题。
那有没有什么方案能解决这个问题呢?
答案就是使用MapStruct,可以优雅地解决上面的这些问题。
MapStruct是一种代码生成器组件,它遵循约定优于配置的原则,可以让我们的Bean对象之间的转换变得更简单。
如前文中描述,在多层应用设计中,需要在不同的对象模型之间进行转换,属性映射,手动编写这些代码不仅繁琐,而且很容易出错,MapStruct的目的是让这项工作变得简单,自动化。
相比其他的映射框架,比如BeanUtils,或者Json序列化反序列化等方式,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;
想要在转换完成之后进行一些数据结果的修改,比如根据更加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官方文档