目录
一、mapstruct简介
mapstruct是一种实体类映射框架,能够通过Java注解将一个实体类的属性安全地赋值给另一个实体类。有了mapstruct,只需要定义一个映射器接口,声明需要映射的方法,在编译过程中,mapstruct会自动生成该接口的实现类,实现将源对象映射到目标对象的效果。
二、mapstruct与其他映射框架对比
实体类映射框架大致有两种:一种是运行期通过java反射机制动态映射;另一种是编译期动态生成getter/setter,在运行期直接调用框架编译好的class类实现实体映射。
由于mapstruct映射是在编译期间实现的,因此相比运行期的映射框架有以下几个优点:
- 安全性高。因为是编译期就实现源对象到目标对象的映射, 如果编译器能够通过,运行期就不会报错。
- 速度快。速度快指的是运行期间直接调用实现类的方法,不会在运行期间使用反射进行转化。
三、mapstruct底层原理解析
mapstruct是基于JSR 269实现的,JSR 269是JDK引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269使用Annotation Processor在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理。想要实现JSR 269,主要有以下几个步骤:
- 继承AbstractProcessor类,并且重写process方法,在process方法中实现自己的注解处理逻辑。
- 在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor
1、Java动态编译
Java程序编译一般经历以下流程:
图1 Java程序编译流程
上图中Java源码到class文件的过程其实是一个比较复杂的过程。其中的经过可以用下图描述:
图2 mapstruct编译过程
上图的流程可以概括为下面几个步骤:
- 生成抽象语法树。Java编译器对Java源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。
- 调用实现了JSR 269 API的程序。只要程序实现了JSR 269 API,就会在编译期间调用实现的注解处理器。
- 修改抽象语法树。在实现JSR 269 API的程序中,可以修改抽象语法树,插入自己的实现逻辑。
- 生成字节码。修改完抽象语法树后,Java编译器会生成修改后的抽象语法树对应的字节码文件件。
2、mapstruct包分析
mapstruct主要有两个包:
- org.mapstruct:mapstruct:包含了映射相关的注解,如@Mapper、@Mapping等。
- org.mapstruct:mapstruct-processor:包含了注解处理器。用于处理注解相关的逻辑,如MappingProcessor等。
两个包大致包含的类如下图所示:
图3 org.mapstruct:mapstruct结构
图4 org.mapstruct:mapstruct-processor结构
3、使用方式
mapstruct的用法很简单,假设我们有两个实体类UserDto和UserVo,类定义如下:
public class UserDTO {
private String name;
private int age;
private Date birthday;
private int gender;
private String idCard;
}
public class UserVo {
private String userName;
private int age;
private Date birthday;
private int gender;
private String idCard;
}
然后需要定义一个用例映射的接口,接口如下:
@Mapper
public interface UserConverter {
@Mappings({
@Mapping(source = "name", target = "userName")
})
UserVo userDtoToVo(UserDto userDto);
}
在UserConverter接口上添加@Mapper注解,在编译的时候,mapstruct自动会生成一个UserConverter的实现类,实现userDtoToVo方法。userDtoToVo方法上的@Mappings主要用于特殊映射,比如上述的UserDto中的name想要映射成userName,则通过@Mappings告诉mapstruct将source中的name映射成target中的userName字段。
通过mapstruct处理后,自动生成UserConverterImpl类,类实现代码如下所示:
@Override
public UserVo userDtoToVo(UserDto userDto) {
if ( userDto == null ) {
return null;
}
UserVo userVo = new UserVo();
if ( userDto.getName() != null ) {
userVo.setUserName( userDto.getName() );
}
userVo.setAge( userDto.getAge() );
if ( userDto.getBirthday() != null ) {
userVo.setBirthday( userDto.getBirthday() );
}
if ( userDto.getGender() != null ) {
userVo.setGender( Integer.parseInt( userDto.getGender() ) );
}
if ( userDto.getIdCard() != null ) {
userVo.setIdCard( userDto.getIdCard() );
}
return userVo;
}
4、实现原理
想要了解原理,可以从mapstruct的源码入手。上文介绍过mapstruct主要有两个jar包,通过根据JSR 269可以知道MappingProcessor就是mapstruct的入口。com.sun.tools.javac.main.JavaCompiler#compile方法的有段代码在编译的时候会调用到MappingProcessor。compile代码如图5所示:
图5 compile方法
MappingProcessor的process方法具体代码如下所示:
@SupportedAnnotationTypes({"org.mapstruct.Mapper"})
@SupportedOptions({"mapstruct.suppressGeneratorTimestamp", "mapstruct.suppressGeneratorVersionInfoComment", "mapstruct.unmappedTargetPolicy", "mapstruct.defaultComponentModel"})
public class MappingProcessor extends AbstractProcessor {
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
if (!roundEnvironment.processingOver()) {
RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
Set<TypeElement> deferredMappers = this.getAndResetDeferredMappers();
this.processMapperElements(deferredMappers, roundContext);
Set<TypeElement> mappers = this.getMappers(annotations, roundEnvironment);
this.processMapperElements(mappers, roundContext);
}
return false;
}
想要用idea在编译的时候调试代码,可以根据下述步骤进行调试:
- 在终端里将路径切换到pom所在路径。例如pom的路径是/User/zhangsan/test/,则cd到该路径下。
- 在终端执行mvnDebug compile。执行该命令后,终端会提示已经监听了8000端口,如图6所示。(注意,执行mvnDebug之前得先配置maven的环境变量,否则无法识别这个命令)
- 在idea中创建添加Remote JVM Debug,端口号是8000,具体如图7所示。
- 在JavaCompiler里的compile方法打断点,再在AbstractProcessor#init和MappingProcessor#init打断点。然后在idea点击debug按钮进行debug。(注意如果本地编译的class已经是最近编译的,则点击debug后无法进入调试,需要修改下代码,如修改下UserDto里的属性,再点击debug按钮)
图6 终端执行mvnDebug命令
图7 idea 创建Remote JVM Debug
通过调试,我们可以看到执行的步骤是这样的:JavaCompiler#compile->AbstractProcessor#init->MappingProcessor#init->MappingProcessor#process。
MappingProcessor#process的代码如下所示:
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
if (!roundEnvironment.processingOver()) {
RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
Set<TypeElement> deferredMappers = this.getAndResetDeferredMappers();
this.processMapperElements(deferredMappers, roundContext);
Set<TypeElement> mappers = this.getMappers(annotations, roundEnvironment);
this.processMapperElements(mappers, roundContext);
}
return false;
}
上述代码的this.processMapperElements(mappers, roundContext);就会执行具体的代码改造工作。具体代码就不做展示,可以自行跟进去查看。从processMapperElements可以看到会执行ModelElementProcessor接口的实现类里的process方法。分析下ModelElementProcessor可以发现它有多个实现类,具体实现类如图8所示。
图8 ModelElementProcessor实现类
ModelElementProcessor的实现类有多个,每个实现类有不同的作用,并且每个实现类都有一个priority,达到执行顺序的效果,实现类大致有如下几个:
-
MethodRetrievalProcessor:解析元素的方法等基本信息。priority=1。
-
MapperCreationProcessor:初始化MapperReference,解析出Mapper。priority=1000。
-
AnnotationBasedComponentModelProcessor:处理ComponentModel相关逻辑。priority=1100。AnnotationBasedComponentModelProcessor又有3个子类,主要用于实现JSR330、Spring component及Cdi 组件等功能。
-
MapperRenderingProcessor:创建接口的具体实现类,比如UserConverter接口,则生成UserConverterImpl类。priority=9999。
-
MapperServiceProcessor:处理spi和META-INF/services/下的相关逻辑。priority=10000。
从MapperRenderingProcessor类里可以看到有个createSourceFile方法,该方法会创建UserConverterImpl类,并写到特定目录下。这样就生成了UserConverter的实现类,里面有UserConverter里的所有方法。
四、小结
本文主要介绍mapstruct的实现原理。先简单介绍mapstruct是什么,然后对比mapstruct与其他框架的优势,最后分析mapstruct底层原理。分析mapstruct从JavaCompiler入手,一步一步往mapstruct里调试,能够清晰地了解底层的实现原理。想要了解某个框架的原理,最好还是从源码入手,自己亲自调试跟踪执行过程才能对原理了解得更透彻。
参考
https://blog.csdn.net/ni_hao_fan/article/details/99445073
https://www.freesion.com/article/9992598813/