百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

MapStruct架构设计(mapstruct的坑)

wptr33 2025-03-24 00:21 17 浏览

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 retrieveMethods(TypeElement usedMapper, TypeElement mapperToImplement, MapperOptions mapperOptions, List prototypeMethods)

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

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

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


4.3 MapperCreationProcessor

这个类的入口方法是process,核心方法是private Mapper getMapper(TypeElement element, MapperOptions mapperOptions, List 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 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生成写入。

相关推荐

台积电提出SRAM存内计算新方法,能效比可达89TOPS/W

芯东西(公众号:aichip001)编译|高歌编辑|云鹏芯东西3月16日消息,近期,台积电的研究人员在ISSCC2021会议上公布了一种改良的SRAM存储器阵列,该SRAM阵列采用22nm工...

Golang中如何判断两个slice是否相等?

在Golang中,要判断两个slice是否相等是不能直接使用==运算符的(==只能说明两个slice是否指向同一个底层数组)。如果两个slice的底层数组相同,但长度或容量不同...

JS入门基础知识(js基础知识总结笔记)

JS对象操作对象增删改查创建对象letobj={}新增属性obj.a=1修改属性obj.a='a'...

趣谈JS二进制:File、Blob、FileReader、ArrayBuffer、Base64

大家好,我是Echa。好久没跟粉丝们细聊JavaScript那点事了。做一名全栈工程师,JS基础还是要打牢,这样的话不管底层业务逻辑以及第三方框架怎么变化,都离不开基础。本文文章属于基础篇,阅读有点...

告别 substr() 和 substring()?更可靠的 JavaScript 字符串截取方法

JavaScript提供了三个主要的字符串截取方法:...

golang第九天,切片(slice)介绍(golang 切片作为参数)

什么是切片golang切片是对数组的抽象。go的数组长度不可改变,在特定场景中这样的集合就不太适用,go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追...

Go语言零到一:数组(go struct数组)

引言...

你说你熟悉Slice,这道slice题你能答对吗?

每当你花费大量时间使用某种特定工具时,深入了解它并了解如何高效地使用它是很值得的。...

Python 3.14七大新特性总结:从t-string模板到GIL并发优化

Python3.14已进入测试阶段,根据PEP745发布计划,该版本已停止引入新功能,也就是说新特征就应该已经固定下来了。所以本文基于当前最新的beta2版本,深入分析了Python3.14中...

Python 幕后:Python导入import的工作原理

更多互联网精彩资讯、工作效率提升关注【飞鱼在浪屿】(日更新)Python最容易被误解的方面其中之一是import。...

Python元类实现自动化编程的正确姿势

元类是Python中用于创建类的类。通过元类机制,开发者可在运行时动态创建和修改类,为框架开发、设计模式实现和高级架构设计提供核心支持。在Python语言的高级特性中,元类占据着独特而重要的地位。作...

Python字符串详解与示例(python字符串类型及操作)

艾瑞巴蒂字符串的干货来了,字符串是程序中最常见的数据类型之一,用来表示数据文本,下面就来介绍下字符串的特性,操作和方法,和一些示例来吧道友:1.字符串的创建在python中字符串可以永单引号(...

恕我直言!你对Python里的import一无所知

文章来源:https://mp.weixin.qq.com/s/4WAOU_Lzy651IE-2zZSFfQ原文作者:写代码的明哥...

Python基础:字符串操作(python字符串的用法)

字符串是Python中最常用的数据类型之一,用于表示文本数据。我们将学习如何对字符串进行常见的操作,包括创建、访问、修改和处理字符串。通过掌握这些技巧,您将能够更好地处理和操作文本数据。让我们开始吧!...

Python 中 字符串处理的高效方法,不允许你还不知道

以下是Python中字符串处理的高效方法...