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

MapStruct架构设计(mapstruct的坑)

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

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生成写入。

相关推荐

每天一个编程技巧!掌握这7个神技,代码效率飙升200%

“同事6点下班,你却为改BUG加班到凌晨?不是你不努力,而是没掌握‘偷懒’的艺术!本文揭秘谷歌工程师私藏的7个编程神技,每天1分钟,让你的代码从‘能用’变‘逆天’。文末附《Python高效代码模板》,...

Git重置到某个历史节点(Sourcetree工具)

前言Sourcetree回滚提交和重置当前分支到此次提交的区别?回滚提交是指将改动的代码提交到本地仓库,但未推送到远端仓库的时候。...

git工作区、暂存区、本地仓库、远程仓库的区别和联系

很多程序员天天写代码,提交代码,拉取代码,对git操作非常熟练,但是对git的原理并不甚了解,借助豆包AI,写个文章总结一下。Git的四个核心区域(工作区、暂存区、本地仓库、远程仓库)是版本控制的核...

解锁人生新剧本的密钥:学会让往事退场

开篇:敦煌莫高窟的千年启示在莫高窟321窟的《降魔变》壁画前,讲解员指着斑驳色彩说:"画师刻意保留了历代修补痕迹,因为真正的传承不是定格,而是流动。"就像我们的人生剧本,精彩章节永远...

Reset local repository branch to be just like remote repository HEAD

技术背景在使用Git进行版本控制时,有时会遇到本地分支与远程分支不一致的情况。可能是因为误操作、多人协作时远程分支被更新等原因。这时就需要将本地分支重置为与远程分支的...

Git恢复至之前版本(git恢复到pull之前的版本)

让程序回到提交前的样子:两种解决方法:回退(reset)、反做(revert)方法一:gitreset...

如何将文件重置或回退到特定版本(怎么让文件回到初始状态)

技术背景在使用Git进行版本控制时,经常会遇到需要将文件回退到特定版本的情况。可能是因为当前版本出现了错误,或者想要恢复到之前某个稳定的版本。Git提供了多种方式来实现这一需求。...

git如何正确回滚代码(git命令回滚代码)

方法一,删除远程分支再提交①首先两步保证当前工作区是干净的,并且和远程分支代码一致$gitcocurrentBranch$gitpullorigincurrentBranch$gi...

[git]撤销的相关命令:reset、revert、checkout

基本概念如果不清晰上面的四个概念,请查看廖老师的git教程这里我多说几句:最开始我使用git的时候,我并不明白我为什么写完代码要用git的一些列指令把我的修改存起来。后来用多了,也就明白了为什么。gi...

利用shell脚本将Mysql错误日志保存到数据库中

说明:利用shell脚本将MYSQL的错误日志提取并保存到数据库中步骤:1)创建数据库,创建表CreatedatabaseMysqlCenter;UseMysqlCenter;CREATET...

MySQL 9.3 引入增强的JavaScript支持

MySQL,这一广泛采用的开源关系型数据库管理系统(RDBMS),发布了其9.x系列的第三个更新版本——9.3版,带来了多项新功能。...

python 连接 mysql 数据库(python连接MySQL数据库案例)

用PyMySQL包来连接Python和MySQL。在使用前需要先通过pip来安装PyMySQL包:在windows系统中打开cmd,输入pipinstallPyMySQL ...

mysql导入导出命令(mysql 导入命令)

mysql导入导出命令mysqldump命令的输入是在bin目录下.1.导出整个数据库  mysqldump-u用户名-p数据库名>导出的文件名  mysqldump-uw...

MySQL-SQL介绍(mysql sqlyog)

介绍结构化查询语言是高级的非过程化编程语言,允许用户在高层数据结构上工作。它不要求用户指定对数据的存放方法,也不需要用户了解具体的数据存放方式,所以具有完全不同底层结构的不同数据库系统,可以使用相同...

MySQL 误删除数据恢复全攻略:基于 Binlog 的实战指南

在MySQL的世界里,二进制日志(Binlog)就是我们的"时光机"。它默默记录着数据库的每一个重要变更,就像一位忠实的史官,为我们在数据灾难中提供最后的救命稻草。本文将带您深入掌握如...