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

MapStruct架构设计(mapstruct的坑)

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

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

相关推荐

MySQL进阶五之自动读写分离mysql-proxy

自动读写分离目前,大量现网用户的业务场景中存在读多写少、业务负载无法预测等情况,在有大量读请求的应用场景下,单个实例可能无法承受读取压力,甚至会对业务产生影响。为了实现读取能力的弹性扩展,分担数据库压...

Postgres vs MySQL_vs2022连接mysql数据库

...

3分钟短文 | Laravel SQL筛选两个日期之间的记录,怎么写?

引言今天说一个细分的需求,在模型中,或者使用laravel提供的EloquentORM功能,构造查询语句时,返回位于两个指定的日期之间的条目。应该怎么写?本文通过几个例子,为大家梳理一下。学习时...

一文由浅入深带你完全掌握MySQL的锁机制原理与应用

本文将跟大家聊聊InnoDB的锁。本文比较长,包括一条SQL是如何加锁的,一些加锁规则、如何分析和解决死锁问题等内容,建议耐心读完,肯定对大家有帮助的。为什么需要加锁呢?...

验证Mysql中联合索引的最左匹配原则

后端面试中一定是必问mysql的,在以往的面试中好几个面试官都反馈我Mysql基础不行,今天来着重复习一下自己的弱点知识。在Mysql调优中索引优化又是非常重要的方法,不管公司的大小只要后端项目中用到...

MySQL索引解析(联合索引/最左前缀/覆盖索引/索引下推)

目录1.索引基础...

你会看 MySQL 的执行计划(EXPLAIN)吗?

SQL执行太慢怎么办?我们通常会使用EXPLAIN命令来查看SQL的执行计划,然后根据执行计划找出问题所在并进行优化。用法简介...

MySQL 从入门到精通(四)之索引结构

索引概述索引(index),是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护者满足特定查询算法的数据结构,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构...

mysql总结——面试中最常问到的知识点

mysql作为开源数据库中的榜一大哥,一直是面试官们考察的重中之重。今天,我们来总结一下mysql的知识点,供大家复习参照,看完这些知识点,再加上一些边角细节,基本上能够应付大多mysql相关面试了(...

mysql总结——面试中最常问到的知识点(2)

首先我们回顾一下上篇内容,主要复习了索引,事务,锁,以及SQL优化的工具。本篇文章接着写后面的内容。性能优化索引优化,SQL中索引的相关优化主要有以下几个方面:最好是全匹配。如果是联合索引的话,遵循最...

MySQL基础全知全解!超详细无废话!轻松上手~

本期内容提醒:全篇2300+字,篇幅较长,可搭配饭菜一同“食”用,全篇无废话(除了这句),干货满满,可收藏供后期反复观看。注:MySQL中语法不区分大小写,本篇中...

深入剖析 MySQL 中的锁机制原理_mysql 锁详解

在互联网软件开发领域,MySQL作为一款广泛应用的关系型数据库管理系统,其锁机制在保障数据一致性和实现并发控制方面扮演着举足轻重的角色。对于互联网软件开发人员而言,深入理解MySQL的锁机制原理...

Java 与 MySQL 性能优化:MySQL分区表设计与性能优化全解析

引言在数据库管理领域,随着数据量的不断增长,如何高效地管理和操作数据成为了一个关键问题。MySQL分区表作为一种有效的数据管理技术,能够将大型表划分为多个更小、更易管理的分区,从而提升数据库的性能和可...

MySQL基础篇:DQL数据查询操作_mysql 查

一、基础查询DQL基础查询语法SELECT字段列表FROM表名列表WHERE条件列表GROUPBY分组字段列表HAVING分组后条件列表ORDERBY排序字段列表LIMIT...

MySql:索引的基本使用_mysql索引的使用和原理

一、索引基础概念1.什么是索引?索引是数据库表的特殊数据结构(通常是B+树),用于...