通用源码阅读指导书:MyBatis源码详解

微信读书

感悟

1
2
3
4
5
6
7
2023-06-02 11:40:35
我以为是个沙雕,随便点开看看,竟然起名“通用源码阅读指导书”,看看能不能学到一些,

看了第3章初探,感觉很不错,知道了Mapper代码 invoke里面回调 execute方法,
看了第4章源码结构,才知道剥洋葱式看源码,(按包看,想到了钟锴,他就是这样,)
看了第5章exceptions包,才知道 异常 Throwable对象的cause,序列化 Serializable、Externalizable(writeExternal/readExternal)
看了第6章reflection包,再看一遍装饰器,反射,Type接口/子接口/Class类,必检InvocationTargetException/非检UndeclaredThrowableException,TypeParameterResolver泛型解析器原理大致讲解(这一块之前,我了解一些,不是很懂,因为Spring Bean这一块也会用到,当时就似懂非懂,现在这个“似”更清楚了一点,)

第2章 MyBatis概述

1
2
3
>> 对象关系映射(Object Relational Mapping,简称 ORM

>> MyBatis则采取了另一种方式,它没有将 Java对象和数据表直接关联起来,而是将 Java方法和 SQL语句关联起来。

第3章 MyBatis运行初探

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MyBatis的操作主要分为两大阶段:

# 第一阶段:MyBatis初始化阶段。
=> 该阶段用来完成 MyBatis运行环境的准备工作,只在 MyBatis启动时运行一次。
· 根据配置文件的位置,获取它的输入流 InputStream。
· 从配置文件的根节点开始,逐层解析配置文件,也包括相关的映射文件。解析过程中不断将解析结果放入 Configuration对象。
· 以配置好的 Configuration对象为参数,获取一个 SqlSessionFactory对象。

# 第二阶段:数据读写阶段。
=> 该阶段由数据读写操作触发,将根据要求完成具体的增、删、改、查等数据库操作。
· 建立连接数据库的 SqlSession。
· 查找当前映射接口中抽象方法对应的数据库操作节点,根据该节点生成接口的实现。
· 接口的实现拦截对映射接口中抽象方法的调用,并将其转化为数据查询操作。
· 对数据库操作节点中的数据库操作语句进行多次处理,最终得到标准的 SQL语句。
· 尝试从缓存中查找操作结果,如果找到则返回;如果找不到则继续从数据库中查询。
· 从数据库中查询结果。
· 处理结果集。
-建立输出对象;
-根据输出结果对输出对象的属性赋值。
· 在缓存中记录查询结果。
· 返回查询结果。

第一阶段、MyBatis初始化阶段
在这里插入图片描述

第二阶段、查询数据
在这里插入图片描述


第4章 MyBatis源码结构概述

在这里插入图片描述

1
2
3
4
5
6
7
8
9
10
11
>> 按照包的功能,我们将所有的包分成三大类:
· 基础功能包:这些包用来为其他包提供一些外围基础功能,如文件读取功能、反射操作功能等。这些包的特点是功能相对独立,与业务逻辑耦合小。
· 配置解析包:这些包用来完成配置解析、存储等工作。这些包中的方法主要在系统初始化阶段运行。
· 核心操作包:这些包用来完成数据库操作。在工作过程中,这些包可能会依赖基础功能包提供的基础功能和配置解析包提供的配置信息。
这些包中的方法主要在数据库操作阶段运行。以上只是功能上的大致划分,各个包中的类、方式实际是互相关联、交织耦合在一起的。

2023-06-02 09:20:18 满怀期待
>> 源码阅读过程中有一个非常重要的技巧,那就是从整个项目的外围源码入手。
· 外围源码很少依赖核心源码,相对独立。先阅读外围源码,受到其他未阅读部分的干扰较小。
· 核心源码大量依赖外围源码。在阅读核心源码时应确保其涉及的外围源码均已阅读完毕,这样可以降低核心源码的阅读难度。
于是整个源码阅读过程会如同剥洋葱一般,由外及内、逐层深入。

第5章 exceptions包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Java的异常

>> Java标准库中内建了一些通用的异常,这些类以Throwable为父类。而 Throwable又派生出 Error类和Exception类两大子类。

>> 对于 Throwable对象,其主要的成员变量有 detailMessage和cause。
· detailMessage 为一个字符串,用来存储异常的详细信息。

· cause 为另一个 Throwable 对象,用来存储引发异常的原因。
这是因为一个异常发生时,通常引发异常的上级程序也发生异常,从而导致一连串的异常产生,叫作异常链。
一个异常的 cause属性可以指向引发它的下级异常,从而将整个异常链保存下来

# 序列化与反序列化

>> 在 Java中,要表明一个类的对象是可序列化的,则必须继承Serializable接口 或其子接口 Externalizable接口。

>> Serializable 接口的使用非常简单,只要一个类实现了该接口,便表明该类的对象是可序列化的,而不需要增加任何方法。

>> · 如果旧 User 类和新 User 类中一方不含 serialVersionUID 字段,或两方都含有serialVersionUID字段但其值不同,则无法反序列化,反序列化过程中会报出序列号版本不一致异常(InvalidClassException)。

>> · 在希望类的版本间实现序列化和反序列化的兼容时,保持serialVersionUID值不变。

第6章 reflection包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 装饰器模式

>> 装饰器模式又称包装模式,是一种结构型模式。这种设计模式是指能够在一个类的基础上增加一个装饰类(也可以叫包装类),并在装饰类中增加一些新的特性和功能。

# 反射

>> 通过 Java反射,能够在类的运行过程中知道这个类有哪些属性和方法,还可以修改属性、调用方法、建立类的实例。

>> Java反射机制主要提供了以下功能。
· 在运行时判断任意一个对象所属的类;
· 在运行时构造任意一个类的对象;
· 在运行时修改任意一个对象的成员变量;
· 在运行时调用任意一个对象的方法。

>> Type接口及其子类

>> 在反射中,我们经常会遇到 Type接口,它代表一个类型,位于“java.lang.reflect”包内。

>> 我们对 Type接口的子类分别进行介绍。
· Class 类:它代表运行的 Java程序中的类和接口,枚举类型(属于类)、注解(属于接口)也都是 Class类的子类。
· WildcardType 接口:它代表通配符表达式。例如,“?”“?extends Number”“?super Integer”都是通配符表达式。
· TypeVariable 接口:它是类型变量的父接口。例如,“Map<K,V>”中的“K”“V”就是类型变量。
· ParameterizedType 接口:它代表参数化的类型。例如,“Collection <String>”就是参数化的类型。
· GenericArrayType 接口:它代表包含 ParameterizedType或者 TypeVariable元素的列表。

>> 遇到不了解的类、方法时,直接跳转到类、方法的定义处查看其原生注释是学习 Java编程、阅读项目源码非常有效的方法。

>> InvocationTargetException为必检异常,UndeclaredThrowableException为免检的运行时异常。它们都不属于 MyBatis,而是来自 java.lang.reflect包。

>> 反射操作中,代理类通过反射调用目标类的方法时,目标类的方法可能抛出异常。
反射可以调用各种目标方法,因此目标方法抛出的异常是多种多样无法确定的。
这意味着反射操作可能抛出一个任意类型的异常。
可以用 Throwable 去接收这个异常,但这无疑太过宽泛。

>> InvocationTargetException就是为解决这个问题而设计的,当反射操作的目标方法中出现异常时,都统一包装成一个必检异常 InvocationTargetException。
InvocationTargetException内部的 target 属性则保存了原始的异常。这样一来,便使得反射操作中的异常更易管理。

2023-06-02 11:09:51 还是没能太理解装拆的意义
>> 有一个简单的例子可以恰好同时涉及InvocationTargetException 和 UndeclaredThrowableException 这两个异常。
就是代理类在进行反射操作时发生异常,于是异常被包装成 InvocationTargetException。
InvocationTargetException显然没有在共同接口或者父类方法中声明过,于是又被包装成了UndeclaredThrowableException。
这样,真正的异常就被包装了两层。这也是为什么在ExceptionUtil的unwrapThrowable方法中存在一个“whiletrue)”死循环,用来持续拆包。
总之,InvocationTargetException 和UndeclaredThrowableException 这两个类都是异常包装类,需要拆包后才能得到真正的异常类。而 ExceptionUtil的unwrapThrowable方法就可以完成该拆包工作。

# 泛型解析器

>> TypeParameterResolver是泛型参数解析器。

>> 请问:Student类中的 getInfo方法(继承自父类 User)的输出参数类型是什么?答案很简单,是“List<Number>”。但是得出这个答案的过程却涉及 User 和 Student两个类。
首先通过 User 类确定 getInfo 方法的输出结果是“List<T>”,然后通过 Student类得知“T”被设置为“Number”。
因此,Student 类中的 getInfo 方法的输出参数是“List<Number>”。

>> TypeParameterResolver 类的功能就是完成上述分析过程,帮助 MyBatis 推断出属性、返回值、输入参数中泛型的具体类型。

>> TypeParameterResolver便分析出 User类中的 getInfo方法的输出参数是“List<Object>”,Student类中的 getInfo方法的输出参数是“List<Number>”。

>> 了解了 TypeParameterResolver类的功能后,下面来查看它的源码。它对外提供以下三个方法。
· resolveFieldType:解析属性的泛型;
· resolveReturnType:解析方法返回值的泛型;
· resolveParamTypes:解析方法输入参数的泛型。

>> 上述这三个方法都只是将要解析的变量从属性、方法返回值、方法输入参数中找出来。变量的泛型解析才是最核心的工作。
以代码6-34所示的resolveParamTypes方法为例,该方法将变量从方法输入参数中找出后,对每个变量都调用了 resolveType 方法。
因此,resolveType是最重要的方法。
【日期标记】2023-06-02 11:09:51 以上同步完成

第7章 annotations包与lang包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
2023-06-03 05:25:16 周六 继续看

# 7.1 Java注解详解

>> Java注解是一种标注。Java中的类、方法、变量、参数、包等均可以被注解标注从而添加额外的信息。相比于直接修改代码的硬编码方式,基于注解的这种松耦合的信息添加方式更受欢迎。

>> @Target 注解用来声明注解可以用在什么地方,它的值需要从枚举类 ElementType 中选取。ElementType的枚举值及其含义如下。
· TYPE:类、接口、注解、枚举;
· FIELD:字段;
· METHOD:方法;
· PARAMETER:参数;
· CONSTRUCTOR:构造方法;
· LOCAL_VARIABLE:本地变量;
· ANNOTATION_TYPE:注解;
· PACKAGE:包;
· TYPE_PARAMETER:类型参数;
· TYPE_USE:类型使用。

>> @Retention注解用来声明注解的生命周期,即表明注解会被保留到哪一阶段。它的值需要从枚举类 RetentionPolicy中选取。RetentionPolicy的枚举值如下。
· SOURCE:保留到源代码阶段。这一类注解一般留给编译器使用,在编译时会被擦除。
· CLASS:保留到类文件阶段。这是默认的生命周期,注解会保留到类文件阶段,但是 JVM运行时不包含这些信息。
· RUNTIME:保留到 JVM运行阶段。如果想在程序运行时获得注解,则需要保留在这一阶段。

>> @Documented 不需要设置具体的值。如果一个注解被@Documented 标注,则该注解会在 javadoc中生成。
@Inherited 不需要设置具体的值。如果一个注解被@Inherited 标注,表明允许子类继承父类的该注解(可以从父类继承该注解,但是不能从接口继承该注解)。
@Repeatable是 JDK 8中新加入的。如果一个注解被@Repeatable标注,则该注解可以在同一个地方被重复使用多次。用@Repeatable 来修饰注解时需要指明一个接受重复注解的容器。

>> 在定义注解的属性时,是使用方法的形式来定义的,即属性名就是方法名。每个属性都可以定义默认值。如果不为属性指定默认值,则在使用时必须赋值。

>> 如果一个注解只有一个名为 value 的属性,则在使用过程中为该属性赋值时可以省略属性名。

>> @Param("id")后,“id”便成了实参的名称,因此能够使用“id”索引到对应的实参。

第8章 type包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# type包

>> type包中的类有 55个之多。在遇到这种繁杂的情况时,一定要注意归类总结。

>> 归类总结是源码阅读中非常好的办法。往往越是大量的类,越是大量的方法,越有规律进行分类。

2023-06-03 05:40:09 看到这个,迫不及待要看这个讲解了,
>> TypeReference:类型参考器;

>> TypeReference 类。它能够判断出一个TypeHandler用来处理的目标类型。而它判断的方法也很简单:取出 TypeHandler实现类中的泛型参数 T的类型,这个值的类型也便是该 TypeHandler能处理的目标类型

>> TypeReference 类是 BaseTypeHandler 的父类,因此所有的类型处理器都继承了TypeReference 的功能。这意味着对任何一个类型处理器调用 getSuperclassTypeParameter方法,都可以得到该处理器用来处理的目标类型。

>> Java数据类型和 JDBC数据类型并不是一对一的关系,而是一对多的关系。

第9章 io包

1
2
3
4
5
6
7
8
9
>> 除了读取磁盘文件的功能外,io包还提供对内存中类文件(class文件)的操作。

>> 代理模式(Proxy Pattern)是指建立某一个对象的代理对象,并且由代理对象控制对原对象的引用。

>> 例如,我们不能直接访问对象 A,则可以建立对象 A的代理对象 A Proxy。这样,就可以通过访问 A Proxy来间接地使用对象 A的功能。A Proxy就像 A的对外联络人一般。

>> 但是静态代理也有一些局限性,最明显的就是代理对象和被代理对象是在程序中写死的,显然不够灵活。动态代理则没有此弊端,

>> VFS(Virtual File System)作为一个虚拟的文件系统将各个磁盘文件系统的差异屏蔽了起来,提供了统一的操作接口。

第10章 logging包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 适配器模式

>> 搭建起了调用方和核心方法之间的桥梁。

>> 图10-1 类适配器类图

>> 借助 isTraceEnabled方法就避免了资源的浪费。

>> 在阅读源码的过程中,读懂源码只是完成了浅层知识的学习。在读懂源码的同时思考源码为何这么设计将会使我们有更大的收获,也会使我们更容易读懂源码。

>> 当 statementLog的 Debug功能开启时,getConnection 方法返回的不是一个原始的 Connection 对象,而是由“ConnectionLogger.newInstance”方法生成的一个代理对象。

2023-06-03 06:44:57 原来如此,是这样实现的,动态代理啊动态代理,开启 debug,增强打日志,

>> 所有“java.sql.Connection”对象的方法调用都会进入 ConnectionLogger 中的invoke方法中。

>> 在 prepareStatement、prepareCall这两个方法执行之前增加了日志打印操作。

MyBatis源码   日志打印

第11章 parsing包

1
2
3
4
5
6
7
8
>> XML文档实际上表述了一棵树。

>> 在一个类中封装自己的解析器,这是一种非常常见的做法,如此一来这个类不需要外界的帮助便可以解析自身,即获得了自解析能力。

>> 假设“openToken=#{”“closeToken=}”,向 GenericTokenParser中的 parse方法传入的参数为“jdbc:mysql://127.0.0.1:3306/${dbname}?serverTimezone=UTC”,
则 parse方法会将被 “#{” 和 “}” 包围的 dbname 字符串解析出来,作为输入参数传入 handler 中的handleToken方法,然后用 handleToken方法的返回值替换“${dbname}”字符串。

>> 将指定模式的属性值定位出来,然后将其替换为 TokenHandler接口中 handleToken方法的返回值。

第13章 binding包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
>> MapperProxy类的帮助,它基于动态代理将针对映射接口的方法调用转接成了对 MapperMethod对象 execute方法的调用,进而实现了数据库操作。

>> MapperProxy 继承了 InvocationHandler 接口,是一个动态代理类。

>> MapperProxyFactory则是 MapperProxy的生产工厂,newInstance核心方法会生成一个 MapperProxy对象。

>> 在MapperRegistry类的 knownMappers

>> knownMappers 是一个 HashMap,其键为映射接口,值为对应的 MapperProxyFactory对象。

>> MapperProxyFactory 的构造方法如代码13-10 所示,只有一个参数便是映射接口。

>> MapperProxy类就是映射接口的一个代理类。代理关系建立完成后,只要调用映射接口中的方法,都会被对应的 MapperProxy 截获,而 MapperProxy会创建或选取合适的 MapperMethod对象,并触发其 execute方法。

2023-06-04 08:11:35 这是我见过最好的源码讲解了。
>> 为了让大家对这个功能的实现有一个概括性的了解,我们将对这一功能进行一次总结。

>> 13.3.1 初始化阶段

>> 13.3.2 数据读写阶段

>> 对于扫描到的映射接口,mybatis-spring 会将其当作MapperFactoryBean对象注册到 Spring的 Bean列表中。而 MapperFactoryBean可以给出映射接口的代理类。

>> 这样,我们可以在代码中直接使用@Autowired 注解来注入映射接口。然后在调用该接口时,MapperFactoryBean给出的代理类会将操作转接给 MyBatis。

第14章 builder包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
>> 14.1 建造者模式

>> 使用建造者模式,对象的建造细节均交给建造者来完成,调用者只需掌控总体流程即可,而不需要了解被建造对象的细节。

>> 基于建造者创建对象时,有以下几个优点。
· 使用建造者时十分灵活,可以一次也可以分多次设置被建造对象的属性;
· 调用者只需调用建造者的主要流程而不需要关系建造对象的细节;
· 可以很方便地修改建造者的行为,从而建造出不同的对象。

>> 建造者类一般包含两类方法:
· 一类是属性设置方法。这类方法一般有多个,可以接受不同类型的参数来设置建造者的属性。
· 一类是目标对象生成方法。该类方法一般只有一个,即根据目前建造者中的属性创建出一个目标对象。

>> SqlSourceBuilder 类能够将 DynamicSqlSource 和 RawSqlSource 中的“#{}”符号替换掉,从而将它们转化为 StaticSqlSource

>> StaticSqlSource是 SqlSource的四个子类之一,它内部包含的 SQL语句中已经不存在“${}”和“#{}”这两种符号,而只有“?”

>> 对于这种以字符串处理为主的类,最合适的源码阅读方法是断点调试法。

>> MyBatis可能会运行在无网络的环境中,无法通过互联网下载 DTD文件。这时该怎么办?XMLMapperEntityResolver就是用来解决这个问题的。

>> include节点的解析过程示意图


>> @SelectProvider注解,该注解中的 type字段指向一个类,method 指向了该类中的一个方法。

2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 第15章 mapping包
>> 为了能够兼容不同数据库的 SQL规范,MyBatis支持多种数据库。在使用多种数据库前,需要先在配置文件中列举要使用的数据库类型

>> 在 SQL语句上标识其对应的数据库类型。


# 第16章 scripting包
>> 可见,如果要多次运行一个表达式,则先将其编译后再运行的执行效率更高。

>> SqlSource接口的四种实现类及它们的区别。
· DynamicSqlSource:动态 SQL语句。所谓动态 SQL语句是指含有动态 SQL节点(如if节点)或者含有“${}”占位符的语句。
· RawSqlSource:原生 SQL语句。指非动态语句,语句中可能含有“#{}”占位符,但不含有动态 SQL节点,也不含有“${}”占位符。
· StaticSqlSource:静态语句。语句中可能含有“?”,可以直接提交给数据库执行。
· ProviderSqlSource:上面的几种都是通过 XML 文件获取的 SQL 语句,而ProviderSqlSource是通过注解映射的形式获取的 SQL语句。

# 第17章 datasource包
>> 在出借之前,他会在东西上签上自己的名字;在东西归还时,他会检查东西上是不是自己的签名。这样就避免了别人的东西错还到自己这里。

# 第18章 jdbc包
>> 那 AbstractSQL为什么要留存一个抽象方法,然后再创建一个 SQL类来实现呢?这一切的意义是什么呢?
将AbstractSQL作为抽象方法独立出来,使得我们可以继承AbstractSQL实现其他的子类,保证了AbstractSQL类更容易被扩展。

# 第19章 cache包

>> MyBatis 还准备了一个 NullCacheKey,该类用来充当一个空键使用。
在缓存查询中,如果发现某个CacheKey信息不全,则会返回 NullCacheKey对象,类似于返回一个 null值。
但是 NullCacheKey毕竟是 CacheKey的子类,在接下来的处理中不会引发空指针异常。这种设计方式也非常值得我们借鉴。

# 第22章 executor包

>> 本节将介绍另一种实现动态代理的方式:基于 cglib(Code Generation Library,代码生成库)的动态代理。
在 3.1.1节中我们介绍一个类必须通过类加载过程将类文件加载到 JVM后才能使用。
那么是否能够直接修改 JVM中的字节码信息来修改和创建类呢?答案是可以的,cglib就是基于这个原理工作的。
cglib使用字节码处理框架 ASM来转换字节码并生成被代理类的子类,然后这个子类就可以作为代理类展开工作。
ASM是一个底层的框架,除非你对JVM内部结构包括 class文件的格式和指令集都很熟悉,否则不要直接使用 ASM。

>> cglib基于底层的 ASM框架来实现 Java字节码的修改。
而 javassist和 ASM类似,它也是一个开源的用来创建、修改 Java字节码的类库,能实现类的创建、方法的修改、继承关系的设置等一系列的操作。
相比于 ASM,javassist的优势是学习成本低,可以根据 Java代码生成字节码,而不需要直接操作字节码。

>> 我们可以用图22-6将继承 Externalizable接口的类的序列化和反序列化流程展示出来。

>> 继承 Serializable接口的类的序列化和反序列化流程相对简单一些,如图22-7所示。

>> 其中的 ErrorContext 类是一个错误上下文,它能够提前将一些背景信息保存下来。这样在真正发生错误时,便能将这些背景信息提供出来,进而给我们的错误排查带来便利。

>> 当线程进入一个与之前操作无关的新环境时,调用 reset方法清除 ErrorContext对象的所有信息。

# 第24章 plugin包

>> 责任链模式将多个处理器组装成一个链条,被处理对象被放置到链条的起始端后,会自动在整个链条上传递和处理。
这样被处理对象不需要和每个处理器打交道,也不需要了解整个链条的传递过程,于是便实现了被处理对象和单个处理器的解耦。

>> 要想了解一个功能模块的源码,一种简单的办法是先学会使用这个模块。

# 第25章 源码阅读总结

>> 记住一点:通过功能猜测源码要比通过源码猜测功能简单得多。

>> 为了便于项目的源码阅读,可以在使用项目时做以下两方面的工作。
· 分清项目的核心功能、次要功能,了解各功能之间的依赖关系。找出核心功能会让我们在后续的源码阅读过程中有的放矢。
· 猜测核心功能的实现原理。可以试想如果是自己开发该功能会怎么实现。想不出来也没关系,这会成为疑问埋在我们的脑海中,然后会在我们读懂相关源码时给我们带来豁然开朗的感觉。

>> 桩模块中不涉及驱动模块的信息,而驱动模块中会涉及桩模块的信息。
先阅读桩模块再阅读驱动模块,能保证我们在阅读代码时较少被未知代码干扰,这就是自底向上阅读源码的原因。