玖叶教程网

前端编程开发入门

MapStruct架构设计

MapStruct架构原理及改造


一、前言 4

二、什么是语法树(AST) 4

2.1 Java编译时的三个阶段 4

三、什么是JSR269 5

3.1 使用步骤 5

3.2 流程图 6

四、源码架构分析 6

4.1 MappingProcessor 7

4.2 MethodRetrievalProcessor 10

4.3 MapperCreationProcessor 11

4.3.1 ValueProvider 13

4.3.2 MappingResolverImpl 13

4.4 MappingRenderingProcessor 14


一、前言

为什么用MapStruct?

MapStruct 是一个生成类型安全, 高性能且无依赖的 JavaBean 映射代码的注解处理器(annotation processor)。

抓一下重点:

  • 注解处理器
  • 可以生成 JavaBean 之间那的映射代码
  • 类型安全、高性能、无依赖性

从字面的理解,我们可以知道,该工具可以帮我们实现JavaBean之间的转换,通过注解的方式。同时,作为一个工具类,相比于手写, 其应该具有便捷, 不容易出错的特点。

MapStruct是基于JSR 269的Java注解处理器,因此可以在命令行构建中使用(javac、Ant、Maven等等),可以在IDE内使用。用于生成类型安全的bean映射类的Java注解处理器。属于编译时注解,如果转换bean内容有变化。需要手动clean下才能将变化的内容体现到class文件中。说白了就是通过注解的形式帮我们生成set,get方法。

MapStruct的核心是在编译期生成基于转换规则的Impl文件,运行时直接调用Impl文件中的函数,整个MapStruct的过程分为三个部分:

  • 自定义注解,指定转换规则,例如:source,target等。
  • freemarker模板,用来生成Impl文件。
  • 基于 javax.annotation.processing 的处理模块

二、什么是语法树(AST)

AST是javac编译器阶段对源代码进行词法语法分析之后,语义分析之前进行的操作。

用一个树形的结构表示源代码,源代码的每个元素映射到树上的节点。

2.1 Java编译时的三个阶段

Java源文件---->词法,语法分析----> 生成AST ---->语义分析 ----> 编译字节码,二进制文件。

通过操作 AST 可以实现 java 源代码的功能。

Rewrite、JavaParser 等开源工具可以帮助你更简单的操作AST。

1、所有源文件会被解析成语法树。

2、调用注解处理器。如果注解处理器产生了新的源文件,新文件也要进行编译。

3、最后,语法树会被分析并转化成类文件。


三、什么是JSR269

插件化注解处理(Pluggable Annotation Processing)APIJSR 269提供一套标准API来处理AnnotationsJSR 175,实际上JSR 269不仅仅用来处理Annotation,我觉得更强大的功能是它建立了Java 语言本身的一个模型,它把method、package、constructor、type、variable、enum、annotation等Java语言元素映射为Types和Elements,从而将Java语言的语义映射成为对象,我们可以在javax.lang.model包下面可以看到这些类。所以我们可以利用JSR 269提供的API来构建一个功能丰富的元编程(metaprogramming)环境。

JSR 269用Annotation Processor在编译期间而不是运行期间处理Annotation, Annotation Processor相当于编译器的一个插件,所以称为插入式注解处理。如果Annotation Processor处理Annotation时(执行process方法)产生了新的Java代码,编译器会再调用一次Annotation Processor,如果第二次处理还有新代码产生,就会接着调用Annotation Processor,直到没有新代码产生为止。每执行一次process()方法被称为一个"round",这样整个Annotation processing过程可以看作是一个round的序列。

JSR 269主要被设计成为针对Tools或者容器的API。这个特性虽然在JavaSE 6已经存在,但是很少人知道它的存在。lombok就是使用这个特性实现编译期的代码插入的。另外,如果没有猜错,像IDEA在编写代码时候的标记语法错误的红色下划线也是通过这个特性实现的。KAPT(Annotation Processing for Kotlin),也就是Kotlin的编译也是通过此特性的。

Pluggable Annotation Processing API的核心是Annotation Processor即注解处理器,一般需要继承抽象类javax.annotation.processing.AbstractProcessor。注意,与运行时注解RetentionPolicy.RUNTIME不同,注解处理器只会处理编译期注解,也就是RetentionPolicy.SOURCE的注解类型,处理的阶段位于Java代码编译期间。


3.1 使用步骤

  1. 自定义一个Annotation Processor,需要继承java.annotation.processing.AbstractProcessor并覆写process方法。
  2. 自定义一个注解,注解的元注解需要指定@Retention(RetentionPolicy.SOURCE)。

需要在声明的自定义Annotation Processor中使用如下注解javax.annotation.processing.SupportedAnnotationTypes指定在第2步创建的注解类型的名称(注意需要全类名,“包名.注解类型名称”,否则会不生效)。

  1. 需要在声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedSourceVersion指定编译版本。
  2. 可选操作,可以通过声明的自定义Annotation Processor中使用javax.annotation.processing.SupportedOptions指定编译参数。


3.2 流程图

四、源码架构分析

打开MapStruct源码后,看到如下包结构图:

本文涉及修改MapStruct源码,所以只分析processor包结构,这是MapStruct的核心代码,用来通过freeMarker生成代码的引擎。


4.1 MappingProcessor

MappingProcessor遵循了JSR269规范,负责生成使用了@Mapper注解的映射器接口实现,然后将其写入到Java源文件中。而模型实例化和处理是通过一系列的Processor责任链来实现,这些Processor是使用Java的ClassLoader机制进行加载的。

下图是用serviceClassLoader去加载所有定义好的Process类,形成类似于处理链,类似于责任链的一种方式(用数组记录执行节点而不是用链表)

加载的Processor类是从META-INF中加载文件是org.mapstruct.ap.internal.processor.ModelElementProcessor,内容如下图所示:

各Processor之间的调用如下图所示:

再回到MappingProcessor类中,整个流程的入口方法是process(),从process方法中先来看如下图所示代码:

这个类对象的接口是MappingResovler,主要是解析我们方法中的元素(比如property,iterable等),从源映射到目标,有两个基本的操作,一个是转换,一个是方法。转换就是将方法中的参数,比如String映射到Integer,或是将Integer转换到Long这种。我们在构建MappingResolverImpl类的时候,通过typeFactory构建了Conversion类,如下图所示:

在Conversions注册了所有的类型映射转换,如下图所示:

我们的新类型将List转换为String的操作就是在这个类中进行,映射类是ListToStringConversion。之后返回到MappingProcessor类中进入到processMapperElements方法如下所图示:

在这个process方法中启动前面提到过的责任链,如下图所示:

这七个执行器形成一个调用链,后面的核心流程主要也是围绕这七个运行。这七个运行器的作用如下:

  • MethodRetrievalProcessor:解析元素的方法等基本信息。priority=1。
  • MapperCreationProcessor:初始化MapperReference,解析出Mapper。priority=1000。
  • AnnotationBasedComponentModelProcessor:处理ComponentModel相关逻辑。priority=1100。AnnotationBasedComponentModelProcessor又有3个子类,主要用于实现JSR330、Spring component及Cdi 组件等功能,这个类是CdiComponentProcessor和SpringComponentProcessor以及JSr330ComponentProcessor的父类。
  • MapperRenderingProcessor:创建接口的具体实现类,比如UserConverter接口,则生成UserConverterImpl类。priority=9999。从MapperRenderingProcessor类里可以看到有个createSourceFile方法,该方法会创建UserConverterImpl类,并写到特定目录下。这样就生成了UserConverter的实现类,里面有UserConverter里的所有方法。
  • MapperServiceProcessor:处理spi和META-INF/services/下的相关逻辑。priority=10000。


4.2 MethodRetrievalProcessor

这个Processor的核心方法是:

private List<SourceMethod> retrieveMethods(TypeElement usedMapper, TypeElement mapperToImplement, MapperOptions mapperOptions, List<SourceMethod> prototypeMethods)

这个方法的作用是通过给定的Mapper类型,检索需要映射的方法。

这个方法主要就是解析映射接口方法:

由图可知,我要转换的方法是fromMap,参数是一个Map参数。


4.3 MapperCreationProcessor

这个类的入口方法是process,核心方法是private Mapper getMapper(TypeElement element, MapperOptions mapperOptions, List<SourceMethod> methods)。

方法流程如下:

  • 通过内部的getMappingMethod方法,判断映射接口方法是不是枚举方法,是不是继承方法,是不是流方法等。
  • 通过BeanMappingMethod.build构建源方法和参数与目标方法和参数的映射,如果映射接口的方法返回不是是isVolid,则获取返回类型,比如返回目标对象是Order类型,通过Type.resultTypeToMap.getPropertyWriteAccessors( cms )获取目标对象的所有可访问的方法,返回的内容如下所示,是一个Map结构:
  • 通过BeanMappingMethod.build方法构建目标属性targetProperties和未处理的目标属性unprocessedTargetProperties,以及unprocessedSourceParamters未处理的源参数。
  • 判断源方法中的参数是否是Map类型(这是我新加的判断)
  • BeanMappingMethod.build().handleDefinedMappings(),这个方法将迭代所有的映射方法,如果这些源和目标的方法之前就已经匹配过了,就从属性对象中删除。
  • BeanMappingMethod.build().applyPropertyNameBasedMapping(),迭代所有目标属性和源参数。方法调用getSourceRefByTargetName(Parameter sourceParameter, String targetPropertyName)这个方法,从目标字段名来匹配源,这个方法很重要继续深挖,判断源参数类型是不是Map,我们的例子中需要转换的对象就是Map(注意这也是新加的方法),如果是Map则获取所以参数。

可以看到typeParameters的类型都是字符串,如下图所示:

如果typeParameters.size等于2,也就是Map中有key和value两个属性,则执行SourceReference.fromMapSource方法。访问返回SourceReference对象,因为是源对象是Map对象,所以SourceReference对象的内容如下图所示:

从图中可以看到,Map对象的key是list,类型是字符串,如果有多个属性则以此类推。

  • 在BeanMappingMethod.build()中调用

applyPropertyNameBasedMapping(List<SourceReference> sourceReferences)方法,在这个方法中构建PropertyMapping对象,这个对象是构建源和目标属性之间的映射,源和目标属性之间的字段名字可能是不同的,如果不同,则通过调用标识注解来做对应关系。

同时通过(String sourceRef = sourceParam.getName() + "." + ValueProvider.of( propertyEntry.getReadAccessor() );代码来拼接属性方法的访问,关于ValueProvider对象的用法参考2.3.1节。返回值参数下图:

4.3.1 ValueProvider

ValueProvider是包装类,提供了模型中需要用到的get,set方法,这是一个模板,最终用来生成代码中用到的。

代码如图所示:

红框部分是我新增的部分,用来判断参数类型是不是Map,如果是的话模板就用get(“xxx”)。如果是普通属性则用getXX()这种方式返回,最终以ValueProvider对象的方式返回。


4.3.2 MappingResolverImpl

通过PropertyMapping中的getTargetAssignment方法找到MappingResolverImpl对象

这个类最重要的代码如下图所示:

通过resolveViaConversion方法可以找到之前注册进来的ListToStringConversion转换器。

注意:红框中的代码是我新加的,可以不断扩展,这段代码判断当前属性类型为List的时候,对应执行ListToStringConversion转换器。将对应的属性与属性类型进行结合,参考map.get(“xxx”),可以参考PrimitiveToStringConversion的例子:

转换完的表达式模板如下图所示:

  • XX

执行完2.3.2小节的内容后,方法返回到BeanMappingMethod->MapperCreationProcessor中.


4.4 MappingRenderingProcessor

这个Processor主要是创建内容并且将内容写入到文件中,从process入口方法中跟踪Mapper对象内容,如下图所示:

可以看到在Mapper中已经有packageName和name,而name已经在映射接口名后面自动加了Impl后缀。最终类文件和内容通过ModelWriter调用FreeMarker生成写入。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言