• mapstruct原理解析


    目录

    一、mapstruct简介

    二、mapstruct与其他映射框架对比

    三、mapstruct底层原理解析

    1、Java动态编译

    2、mapstruct包分析

    ?3、使用方式

    4、实现原理

    四、小结

    参考


    一、mapstruct简介

    mapstruct是一种实体类映射框架,能够通过Java注解将一个实体类的属性安全地赋值给另一个实体类。有了mapstruct,只需要定义一个映射器接口,声明需要映射的方法,在编译过程中,mapstruct会自动生成该接口的实现类,实现将源对象映射到目标对象的效果。

    二、mapstruct与其他映射框架对比

    实体类映射框架大致有两种:一种是运行期通过java反射机制动态映射;另一种是编译期动态生成getter/setter,在运行期直接调用框架编译好的class类实现实体映射。

    由于mapstruct映射是在编译期间实现的,因此相比运行期的映射框架有以下几个优点:

    1. 安全性高。因为是编译期就实现源对象到目标对象的映射, 如果编译器能够通过,运行期就不会报错。
    2. 速度快。速度快指的是运行期间直接调用实现类的方法,不会在运行期间使用反射进行转化。

    三、mapstruct底层原理解析

    mapstruct是基于JSR 269实现的,JSR 269是JDK引进的一种规范。有了它,能够实现在编译期处理注解,并且读取、修改和添加抽象语法树中的内容。JSR 269使用Annotation Processor在编译期间处理注解,Annotation Processor相当于编译器的一种插件,因此又称为插入式注解处理。想要实现JSR 269,主要有以下几个步骤:

    1. 继承AbstractProcessor类,并且重写process方法,在process方法中实现自己的注解处理逻辑。
    2. 在META-INF/services目录下创建javax.annotation.processing.Processor文件注册自己实现的Annotation Processor

    1、Java动态编译

    Java程序编译一般经历以下流程:

    图1 Java程序编译流程

    上图中Java源码到class文件的过程其实是一个比较复杂的过程。其中的经过可以用下图描述:

    图2 mapstruct编译过程

    上图的流程可以概括为下面几个步骤:

    1. 生成抽象语法树。Java编译器对Java源码进行编译,生成抽象语法树(Abstract Syntax Tree,AST)。
    2. 调用实现了JSR 269 API的程序。只要程序实现了JSR 269 API,就会在编译期间调用实现的注解处理器。
    3. 修改抽象语法树。在实现JSR 269 API的程序中,可以修改抽象语法树,插入自己的实现逻辑。
    4. 生成字节码。修改完抽象语法树后,Java编译器会生成修改后的抽象语法树对应的字节码文件件。

    2、mapstruct包分析

    mapstruct主要有两个包:

    1. org.mapstruct:mapstruct:包含了映射相关的注解,如@Mapper、@Mapping等。
    2. org.mapstruct:mapstruct-processor:包含了注解处理器。用于处理注解相关的逻辑,如MappingProcessor等。

    两个包大致包含的类如下图所示:

    图3org.mapstruct:mapstruct结构

    图4org.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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后需要定义一个用例映射的接口,接口如下:

    @Mapper
    public interface UserConverter {
    
        @Mappings({
                @Mapping(source = "name", target = "userName")
        })
        UserVo userDtoToVo(UserDto userDto);
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    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 annotations, RoundEnvironment roundEnvironment) {
            if (!roundEnvironment.processingOver()) {
                RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
                Set deferredMappers = this.getAndResetDeferredMappers();
                this.processMapperElements(deferredMappers, roundContext);
                Set mappers = this.getMappers(annotations, roundEnvironment);
                this.processMapperElements(mappers, roundContext);
            }
    
            return false;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    想要用idea在编译的时候调试代码,可以根据下述步骤进行调试:

    1. 在终端里将路径切换到pom所在路径。例如pom的路径是/User/zhangsan/test/,则cd到该路径下。
    2. 在终端执行mvnDebug compile。执行该命令后,终端会提示已经监听了8000端口,如图6所示。(注意,执行mvnDebug之前得先配置maven的环境变量,否则无法识别这个命令)
    3. 在idea中创建添加Remote JVM Debug,端口号是8000,具体如图7所示。
    4. 在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 annotations, RoundEnvironment roundEnvironment) {
            if (!roundEnvironment.processingOver()) {
                RoundContext roundContext = new RoundContext(this.annotationProcessorContext);
                Set deferredMappers = this.getAndResetDeferredMappers();
                this.processMapperElements(deferredMappers, roundContext);
                Set mappers = this.getMappers(annotations, roundEnvironment);
                this.processMapperElements(mappers, roundContext);
            }
            return false;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    上述代码的this.processMapperElements(mappers, roundContext);就会执行具体的代码改造工作。具体代码就不做展示,可以自行跟进去查看。从processMapperElements可以看到会执行ModelElementProcessor接口的实现类里的process方法。分析下ModelElementProcessor可以发现它有多个实现类,具体实现类如图8所示。

    图8ModelElementProcessor实现类

    ModelElementProcessor的实现类有多个,每个实现类有不同的作用,并且每个实现类都有一个priority,达到执行顺序的效果,实现类大致有如下几个:

    1. MethodRetrievalProcessor:解析元素的方法等基本信息。priority=1。

    2. MapperCreationProcessor:初始化MapperReference,解析出Mapper。priority=1000。

    3. AnnotationBasedComponentModelProcessor:处理ComponentModel相关逻辑。priority=1100。AnnotationBasedComponentModelProcessor又有3个子类,主要用于实现JSR330、Spring component及Cdi 组件等功能。

    4. MapperRenderingProcessor:创建接口的具体实现类,比如UserConverter接口,则生成UserConverterImpl类。priority=9999。

    5. 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/

  • 相关阅读:
    【华为机试真题 JAVA】数列描述-100
    【计算机毕业设计】Node.js商城APP-97200,免费送源码,【开题选题+程序定制+论文书写+答辩ppt书写-原创定制程序】
    启动Dubbo项目注册Zookeeper时提示zookeeper not connected异常原理解析
    1. 概述
    Openssl X509 v3 AuthorityKeyIdentifier实验与逻辑分析
    MySQL查询进阶——从函数到表连接的使用你还记得吗
    2023 年 Web 安全最详细学习路线指南,从入门到入职(含书籍、工具包)【建议收藏】
    milvus upsert流程源码分析
    LeetCode —— 复写零(双指针)
    项目实战(依旧还是登录认证,JWT解析异常处理,授权信息处理)
  • 原文地址:https://blog.csdn.net/begefefsef/article/details/126434950