写点什么

Java 实体映射利器 ---MapStruct

用户头像
是小毛吖
关注
发布于: 2021 年 02 月 20 日

背景

由于代码分层原因,导致代码中会有多种形如 XXXVO、XXXDTO、XXXDO 的类,并且经常发生各种 VO/DTO/DO 之后转换。从而产生很多 vo.setXXX(dto.getXXX()) 的代码。当字段多了之后不仅容易出错,而且有些浪费时间。也会有人使用 BeanUtils.copyProperties() 进行转换,这样虽然节省了代码。但是依旧存在一些问题。具体体现在以下 2 个方面:

  1. 使用反射影响性能

  2. 无法映射不同名称的字段


本文将介绍一款 Java 实体对象映射框架---MapStruct。

介绍

官方文档:https://mapstruct.org/documentation/dev/reference/html/

首页:https://mapstruct.org/

MapStruct 是一种基于 Java JSR 269 注释处理器,用于生成类型安全,高性能和无依赖的 Bean 映射代码。MapStruct 特点有:

  1. 通过 getter/setter 进行字段拷贝,而不是反射

  2. 字段名称相同直接转换,名称不同使用@Mapping注解标识


与动态映射框架相比,MapStruct 的优势:

  1. 使用普通的 getter/setter 方法而非反射,执行更快

  2. 编译时类型安全

  3. 清晰的错误提示信息

使用

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 ,代码如下所示

@Mapperpublic interface MapStruct101 {   @Mappings({})   StudentVO toStudentVO(Student student);}
复制代码

代码编译之后,生成 MapStruct101 的实现类。生成代码如下:

@Overridepublic 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;}
复制代码

通过上面代码得出以下几个结论:

  1. 同名称的自动转换,如果类型不同也会进行隐式转换。题外音:类型不一致,字段名称一致的情况可能出错,需要注意。

  2. 字段之间的拷贝是通过 getter/setter 方法,而不是通过反射。题外音:类必需有 getter/setter 方法

  3. 名称不同的不会转换(displayStuNumber 未转换)

不同名称字段的映射

上面在映射接口我们直接使用了 @Mappings({}),未进行特殊处理,所以只对同名的进行了转换。现在我们增加注解,从而实现名称不同的字段之间的转换。

接口类代码修改如下:

@Mapperpublic interface MapStruct101 {   @Mappings({	     @Mapping(source = "stuNumber", target = "displayStuNumber")	 })   StudentVO toStudentVO(Student student);}
复制代码

再次编译之后生成代码如下:

@Overridepublic 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  注解的 sourcetarget 进行不同名字段的映射。其中 source 代表源字段,target 表示 source 字段映射到的字段。

字段转换时,需要简单处理

上面我们发现 Student 类的 genderint 类型(0 表示女,1 表示男),StudentVOgenderString(男或女)。此时并不是直接的字段转换,而是需要映射。 此时我们再次修改映射接口代码如下:

@Mapperpublic interface MapStruct101 {  @Mappings({	     @Mapping(source = "stuNumber", target = "displayStuNumber"),	     @Mapping(target = "gender", expression = "java(student.getGender() == 1 ? \"男\" : \"女\")")	 })   StudentVO toStudentVO(Student student);}
复制代码

编译之后生成代码如下:

@Overridepublic 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 字段显然不合理。我们可以这样处理,修改映射接口如下:(此处还是以性别映射举例)

@Mapperpublic 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 ? "男" : "女";	}}
复制代码

编译之后生成代码如下:

@Overridepublic 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>,则只需写出 ProjectProjecgVO 的映射即可。代码如下:

类定义如下:

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;}
复制代码

映射接口代码:

@Mapperpublic 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);}
复制代码

编译之后生成代码如下:

@Overridepublic 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)

@Testpublic 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注入 @Autowiredprivate MapStruct102 mapStruct102;
@Testpublic void test() { Teacher teacher = Teacher.builder() .teacherName("张老师") .address("西二旗") .mobilePhone("123445") .build(); TeacherVO teacherVO = mapStruct102.toTeacherVO(teacher); System.out.println(teacherVO);}
复制代码


发布于: 2021 年 02 月 20 日阅读数: 29
用户头像

是小毛吖

关注

心之所向,素履以往 2018.12.28 加入

一枚正在成长的程序猿

评论

发布
暂无评论
Java实体映射利器---MapStruct