背景
由于代码分层原因,导致代码中会有多种形如 XXXVO、XXXDTO、XXXDO 的类,并且经常发生各种 VO/DTO/DO 之后转换。从而产生很多 vo.setXXX(dto.getXXX())
的代码。当字段多了之后不仅容易出错,而且有些浪费时间。也会有人使用 BeanUtils.copyProperties()
进行转换,这样虽然节省了代码。但是依旧存在一些问题。具体体现在以下 2 个方面:
使用反射影响性能
无法映射不同名称的字段
本文将介绍一款 Java 实体对象映射框架---MapStruct。
介绍
官方文档:https://mapstruct.org/documentation/dev/reference/html/
首页:https://mapstruct.org/
MapStruct 是一种基于 Java JSR 269 注释处理器,用于生成类型安全,高性能和无依赖的 Bean 映射代码。MapStruct 特点有:
通过 getter/setter
进行字段拷贝,而不是反射
字段名称相同直接转换,名称不同使用@Mapping
注解标识
与动态映射框架相比,MapStruct 的优势:
使用普通的 getter/setter 方法而非反射,执行更快
编译时类型安全
清晰的错误提示信息
使用
Maven 配置
...
<properties>
<org.mapstruct.version>1.4.0.Beta3</org.mapstruct.version>
</properties>
...
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
</dependencies>
...
<build>
<plugins>
<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>
</plugins>
</build>
...
复制代码
可配置项
具体使用
基本映射
第一步:定义类,已省略 getter/setter
方法
public class Student {
private String stuName;
private String stuNumber;
private int gender;
}
public class StudentVO {
private String stuName; // 姓名
private String displayStuNumber; // 展示学号
private String gender; // 男 女
}
复制代码
第二步:创建映射器。只需定义 Java 接口,并使用注解@Mapper
,代码如下所示
@Mapper
public interface MapStruct101 {
@Mappings({})
StudentVO toStudentVO(Student student);
}
复制代码
代码编译之后,生成 MapStruct101 的实现类。生成代码如下:
@Override
public StudentVO toStudentVO(Student student) {
if ( student == null ) {
return null;
}
StudentVO studentVO = new StudentVO();
studentVO.setStuName( student.getStuName() );
studentVO.setGender( String.valueOf( student.getGender() ) );
return studentVO;
}
复制代码
通过上面代码得出以下几个结论:
同名称的自动转换,如果类型不同也会进行隐式转换。题外音:类型不一致,字段名称一致的情况可能出错,需要注意。
字段之间的拷贝是通过 getter/setter
方法,而不是通过反射。题外音:类必需有 getter/setter 方法。
名称不同的不会转换(displayStuNumber 未转换)
不同名称字段的映射
上面在映射接口我们直接使用了 @Mappings({})
,未进行特殊处理,所以只对同名的进行了转换。现在我们增加注解,从而实现名称不同的字段之间的转换。
接口类代码修改如下:
@Mapper
public interface MapStruct101 {
@Mappings({
@Mapping(source = "stuNumber", target = "displayStuNumber")
})
StudentVO toStudentVO(Student student);
}
复制代码
再次编译之后生成代码如下:
@Override
public StudentVO toStudentVO(Student student) {
if ( student == null ) {
return null;
}
StudentVO studentVO = new StudentVO();
studentVO.setDisplayStuNumber( student.getStuNumber() );
studentVO.setStuName( student.getStuName() );
studentVO.setGender( String.valueOf( student.getGender() ) );
return studentVO;
}
复制代码
我们通过使用 @Mapping
注解的 source
和 target
进行不同名字段的映射。其中 source
代表源字段,target
表示 source
字段映射到的字段。
字段转换时,需要简单处理
上面我们发现 Student
类的 gender
是 int
类型(0 表示女,1 表示男),StudentVO
的 gender
是 String
(男或女)。此时并不是直接的字段转换,而是需要映射。 此时我们再次修改映射接口代码如下:
@Mapper
public interface MapStruct101 {
@Mappings({
@Mapping(source = "stuNumber", target = "displayStuNumber"),
@Mapping(target = "gender", expression = "java(student.getGender() == 1 ? \"男\" : \"女\")")
})
StudentVO toStudentVO(Student student);
}
复制代码
编译之后生成代码如下:
@Override
public StudentVO toStudentVO(Student student) {
if ( student == null ) {
return null;
}
StudentVO studentVO = new StudentVO();
studentVO.setStuName( student.getStuName() );
studentVO.setGender( student.getGender() == 1 ? "男" : "女" );
studentVO.setDisplayStuNumber( student.getStuNumber());
return studentVO;
}
复制代码
这样gender
字段就变成了 男、女了。我们发现可以使用 @Mapping
注解的 expression
进行字段转换时的简单处理。
字段转换时,需要复杂处理
开发中有时候字段需要进行复杂逻辑处理,多行代码如果写在 expression 字段显然不合理。我们可以这样处理,修改映射接口如下:(此处还是以性别映射举例)
@Mapper
public interface MapStruct101 {
@Mappings({
@Mapping(source = "stuNumber", target = "displayStuNumber"),
@Mapping(target = "gender", source = "gender", qualifiedByName = "transferGender")
})
StudentVO toStudentVO(Student student);
@Named("transferGender")
default String transferGender(int gender) {
return gender == 1 ? "男" : "女";
}
}
复制代码
编译之后生成代码如下:
@Override
public StudentVO toStudentVO(Student student) {
if ( student == null ) {
return null;
}
StudentVO studentVO = new StudentVO();
studentVO.setStuName( student.getStuName() );
studentVO.setGender( transferGender(student.getGender()));
studentVO.setDisplayStuNumber( student.getStuNumber());
return studentVO;
}
default String transferGender(int gender) {
return gender == 1 ? "男" : "女";
}
复制代码
我们可以使用一个 defaut 方法进行复杂逻辑的处理,并使用@Named
注解进行标注,并在 @Mapping
注解中使用 qualifiedByName
表明使用哪个方法进行处理转换。 这样生成代码之后就会调用指定方法进行转换。
类中包含其他类的列表
此处可以自己写 demo 验证看看哦。例如学生类中有List<Project>
,则只需写出 Project
与 ProjecgVO
的映射即可。代码如下:
类定义如下:
public class Student {
private String stuName;
private String stuNumber;
private int gender;
private List<Project> projects;
}
public class Project {
private String projectName;
private double projectScore;
private String teacherName;
}
public class StudentVO {
private String stuName;
private String displayStuNumber;
private String gender;
private List<ProjectVO> projectVOList;
}
public class ProjectVO {
private String projectName;
private double projectScore;
private String teacherName;
}
复制代码
映射接口代码:
@Mapper
public interface MapStruct101 {
@Mappings({
@Mapping(target = "gender", expression = "java(student.getGender() == 1 ? \"男\" : \"女\")")
@Mapping(target = "displayStuNumber", source = "stuNumber")
@Mapping(target = "projectVOList", source = "projects")
})
StudentVO toStudentVO(Student student);
}
复制代码
编译之后生成代码如下:
@Override
public StudentVO toStudentVOWithListObject(Student student) {
if ( student == null ) {
return null;
}
StudentVO studentVO = new StudentVO();
studentVO.setProjectVOList( projectListToProjectVOList( student.getProjects() ) );
studentVO.setStuName( student.getStuName() );
studentVO.setGender( student.getGender() == 1 ? "男" : "女" );
studentVO.setDisplayStuNumber( student.getStuNumber());
return studentVO;
}
复制代码
其实会自动生成包含类的映射关系,很是方便。
引用
映射接口写好了,我们应该如何使用呢?
普通使用
可以通过如下代码:Mappers.getMapper(MapStruct101.class)
@Test
public void test() {
MapStruct101 mapper = Mappers.getMapper(MapStruct101.class);
Teacher teacher = Teacher.builder()
.teacherName("张老师")
.address("西二旗")
.mobilePhone("123445")
.build();
TeacherVO teacherVO = mapper.toTeacherVO(teacher);
System.out.println(teacherVO);
}
复制代码
Spring 使用
spring 使用,需要修改组件模型为 spring,可以通过 pom.xml 的参数修改,也可以通过注解修改。修改之后会把实现类添加@Component
从而成为一个 bean。 此处我们通过修改注解,使用 @Mapper(commentModel = "spring")
@Mapper(componentModel = "spring")
public interface MapStruct102 {
@Mapping(source = "teacherName", target = "name")
@Mapping(source = "mobilePhone", target = "phone")
TeacherVO toTeacherVO(Teacher teacher);
}
// 就可以使用bean注入
@Autowired
private MapStruct102 mapStruct102;
@Test
public void test() {
Teacher teacher = Teacher.builder()
.teacherName("张老师")
.address("西二旗")
.mobilePhone("123445")
.build();
TeacherVO teacherVO = mapStruct102.toTeacherVO(teacher);
System.out.println(teacherVO);
}
复制代码
评论