mapstruct原理解析

mapstruct原理解析目录一、mapstruct简介二、mapstruct与其他映射框架对比三、mapstruct底层原理解析1、Java动态编译2、mapstruct源码分析四、小结一、mapstruct简介mapstruct是一种实体类映射框架,能够通过Java注解将一个实体类的属性安全地赋值给另一个实体类。有了mapstruct,只需要定义一个映射器接口,声明需要映射的方法,在编译过程中,mapstruct会自动生成该接口的实现类,实现将源对象映射到目标对象的效果。…

大家好,欢迎来到IT知识分享网。

目录

一、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程序编译一般经历以下流程:

mapstruct原理解析

图1 Java程序编译流程

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

mapstruct原理解析

图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等。

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

mapstruct原理解析

图3 org.mapstruct: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所示:

mapstruct原理解析

图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在编译的时候调试代码,可以根据下述步骤进行调试:

  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按钮)

mapstruct原理解析

图6 终端执行mvnDebug命令
mapstruct原理解析
图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所示。

mapstruct原理解析

图8  ModelElementProcessor实现类

        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/

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://yundeesoft.com/12564.html

(0)

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

关注微信