我正在参加「掘金·启航计划」
前言
近期在阅读MyBatis
源码,网上各种文章也参差不齐,不太符合我的口味,于是就有了该篇。
约定:版本为3.5.11
, 核心方法指代真正做事情的方法。环境搭建网上皆是,便不再赘述了。本篇主要分析以下两行代码
1. 资源加载
第一步要处理的肯定是加载其配置文件,MyBatis
针对资源加载封装了一套易用工具,可将资源加载为Properties
,FileStream
等。核心功臣便是Resources
类,但是Resources
也不是真正做事情的,都委托给了ClassLoadWrapper
。
见名知意,我们此处使用的是以流的方式获取配置信息。在Resources
类中,使用的是ClassLoaderWrapper
进行解析,该类包装了多个类加载器。看图,各位应该都清晰了,注:以下截图来自ClassLoadWrapper
后续便是循环classLoader#getResourceAsStream
查找资源,针对不同的类加载器,可能需要/
前缀,在加载失败时,便会加上该前缀重试。
2. 资源解析
MyBatis
交互以SqlSession
为核心,而该类的获取需通过SqlSessionFactory
获取。当然也可以自己new
,只不过用人家的更省事!先看下对应类图
定睛一看,工厂方法,那这SqlSessionManager
是什么鬼?自身是Factory
又是SqlSession
,鉴于内容完整性在这里简述一下:内部也是使用的DefaultSqlSessionFactory
获取Session
,但是使用ThreadLocal
进行了session
隔离,同时增加了自动提交和回滚机制。由于也没其他地方使用了SqlSessionManager
这里不再展开,可自行了解。
为了便捷创建SqlSessionFactory
,MyBatis
又提供了SqlSessionFactoryBuilder
供我们使用。核心方法如下
不难看出使用的是XmlConfigBuilder#parse()
解析配置文件生成Configuration
对象
XmlConfigBuilder
继承至BaseBuilder
,与此类似的还有各种XmlXXXBuilder
用于解析不同内容。可以看到有趣的一点是,对于父类构造参数是直接new Configuration()
,也就是说我们也可以直接创建DefaultSqlSessionFactory
对象,但是这样的话Mapper
的映射关系就得由我们自己维护(详情可见(Xml方式构建)。
2.1 解析节点
MyBatis
使用XParser
解析Xml
,并封装了一个XPathParser
,在其内部使用XMLMapperEntityResolver
将外部DTD
文件转为内部资源(路径为org/apache/ibatis/builder/xml/
),避免下载所带来的资源消耗。我们只需知道XPathParser
是Xml
解析工具类即可。
2.1.1 propertiesElement()
解析<properties>
标签内容,先上该方法代码
方法不难,直接给出结论:针对
<properties>
标签,reosurce
和url
属性只能存在一个。Configuriton
对象内的属性设置优先级最高,其次通过url
或者resource
配置的,最后是标签内的属性。
在使用SqlSessionFactory#build()
时可传递Properties
对象,该对象最终会设置到Configuration
中。
2.1.2 settingsAsProperties()
解析<settings>
标签,但是该方法只是将标签内容以Properties
返回
这个方法别看他内容少,但涉及到的东西还是挺多的。首先,如何限制设置的属性是可用的,而不是乱填的?针对这个问题,我们可以从代码中找到答案。
核心代码就是MetaClass.forClass
,该类返回MetaClass
对象,该对象就是类定义信息工具类,代码如下
内部聚合了ReflectorFactory
和Reflector
,先说说Reflector
吧,这个类表示一个类的类定义信息。比如类的get
和set
方法信息,构造函数定义等。贴个参数图
说完Reflector
,那ReflectorFactory
作用显而易见了,又是一个工厂。默认的实现使用了一个ConcurrentHashMap
缓存已生成的Reflector
,key
为Class<?>
,也可以设置为非缓存,每次都new
一个,应该没人这样干吧!
至于如何获取类中get
,set
方法之类的过程(核心方法为new Reflector
),节省篇幅就不详细介绍了,这里说下大概:都是通过反射进行的
- 获取没有参数的构造方法,也就默认构造方法。
- 解析
get
,set
方法。 - 解析所有属性。
这其中有几个个细节问题,它怎么知道我们哪些方法是get
哪些是set
呢?这就需要我们的get
,set
方法需要符合命名规范,也就是以getXXX
,setXXX
开头,get
方法还可以is
开头。多么朴实无华啊~
其二是如果有多个get
或者set
方法该如何选择?这也得分几种情况,此处以get
为例:
String getName()
和String getname()
对于这种返回值类型相同,在添加时不会出现异常,但是在使用Reflector
执行该方法时,会抛出异常,因为该get
方法时含糊不清的,MyBatis
无法判断哪个是有效的。String getName()
和boolean getname()
同上。boolean getName()
和boolean getname()
取第一个定义的get
方法,但如果最后get
方法以is
开头,则以最后一个is
方法为准。ParentClass getName()
和SubClass getname()
子父类关系,以返回子类对象的方法为准。
以上只是用了两个get
方法举例子,可能有更多,以上四种情况中能取出正确取得get
方法(3,4)的将会和后面同属性(getXXX()
中的首个X
忽略大小写相同,则为同一属性)get
方法继续循环上面的比较逻辑。
为啥两个返回boolean
类型的就可以正常运行呢?在我认为是命名规范的问题,对于返回String
的一般取名getXXX
,而boolean
的可能以is
或者get
开头。也看得出MyBatis
更倾向于使用is
开头定义boolean
类型的get
方法。
set
方法的处理会根据第一个参数的类型和上述过程选取的正确get
方法(运行不报错)的返回值类型相匹配,这被认为是最合适的。其次是set
方法的第一个参数类型以子类为准。其余情况都会在运行指定set
方法时报错。
最后会解析该类的属性(排除 final
,static
修饰的),在这里会进行兜底处理,将解析get
,set
方法没有处理到的属性再添加一遍,同时也会添加父类的属性。
到这里Reflector
了解清楚了,MetaClass.forClass
也就理解了,后面的就好办了。说好的不详细介绍呢~
接下来便是metaConfig.hasSetter(String.valueOf(key))
这一句啦,见名知意是否存在对应名称的set
方法。开头问题的答案也就跃然纸上~
2.1.3 loadCustomVfs()
VFS
将底层磁盘文件系统屏蔽,提供统一操作方式。
源码比较简单,从
settings
标签中读取vfsImpl
值。一般我们也不会更改它,需要更改的话在settings
标签中设置vfsImpl
即可,可设置多个,用,
隔开。也就是说我们可以有多个VFS
实现,优先以用户指定顺序使用,其次是JBoss6VFS
, DefaultVFS.class
。
2.1.4 loadCustomLogImpl()
配置MyBatis
日志的实现,可以使用全限定名或别名。可使用的别名在Configuration
创建时就已经添加好了,如下:
默认加载日志顺序在官网已经说明了,此处不再赘述了。
MyBatis
会自动按照官网的顺序加载。一般情况下引入合适包就行了,不用配置logImpl
属性,避免重复加载浪费资源。但配置该属性也有个好处,在存在多个日志实现的时候,使用自动查找会覆盖前面已配置的,例如:同时存在SLF4J
和Log4j2
时,会使用SLF4J
实现,而不是Log4j2
,此时就可以用logImpl
属性限定使用的日志实现。
代码还是很简单的。但是此处涉及到资源损耗的问题,多一次反射创建:若指定
logImpl
的别名存在自动加载中,则会出现该情况。核心方法LogFactory.useCustomLogging()
2.1.5 typeAliasesElement()
类型别名注册,可用简短缩写代替长长的类限定名。默认已经添加多个缩写对应关系,详情可见TypeAliasRegistry
构造方法。
该方法代码说明如下
别名注册可使用批量或单个注册方式。批量注册的方式最终应该和单个注册的方式一样,区别在于哪些需要注册,这就涉及到类查找问题了。核心代码如下:
关键点在于
ResolverUtil
这个类。根据文档注释得知ResolverUtil
用于定位在/a
类路径中可用的、符合任意条件的类。而这个条件该怎么定义呢?在ResolverUtil
中定义了一个内部接口Test
,可以看作这是一个条件接口,使用该接口的matches()
确认是否匹配。默认有两个实现IsA
和AnnotatedWith
,IsA
指定是否是的关系,另一个实现则代表是否包含指定注解。这两个实现都很简单,大家可以自行阅读
回到正题,了解清楚ResolverUtil
后直接看看find
方法的实现:
首先
getPackagePath()
会将.
替换为/
,所以在我们指定包时,采用/
分割包,可以略微提升性能。第二步就是通过VFS
获取指定包和其子包下的所有资源路径,循环判断.class
资源是否符合ResolverUtil.Test
,使用HashSet
存储了所有符合的类对象。
这整个查找过程都没有抛出异常,只是简单的使用日志记录,可见对于包扫描来说,将异常延后到了别名使用处。这样做的好处在于处理多个资源加载时,不影响前面已加载的,同时让流程还可以继续。
回到最初调用的地方,接下来就是针对每个获取到的资源进行别名映射了,单个注册也是用的该方法:
方法挺简单的,优先使用配置文件中的
alias
属性,其次@Alias
注解,最后使用小写类名。再次添加同类型的别名会进行替换,别名一样但是类型不同则会抛出异常。
针对loadCustomLogImpl()
和typeAliasesElement()
有个点需要注意,在配置logImpl
为自定义实现时是不能使用别名的。logImpl()
会从别名列表中获取该类,无则加载,这时你传别名是会加载失败的。那为啥别名注册要放到日志配置后面呢?在我认为可能是容易产生两份日志的问题。如果放到前面,注册别名的日志可能由日志自动导致生成在别的文件,后续改变日志实现又导致生成的在另外一个文件中。
2.1.6 pluginElement()
该方法负责插件注册。虽说叫插件,但我认为叫拦截器更为合适。插件的使用可以参考官网文档
在图中也标明代码注释,整体也是比较简单。有些细节还是需要注意的,实现
Interceptor
接口的类需要有无参构造方法。Configuration
中保存了对过滤器链InterceptorChain
对象,内部使用一个ArrayList
保存所有的过滤器。具体过滤器的使用我们到使用到的地方在详细说明。
2.1.7 objectFactoryElement()
注册对象工厂 每次MyBatis
创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory
)实例来完成实例化工作。默认的实现是DefaultObjectFactory
。核心代码如下:
不难看出,和过滤器的代码基本同理。默认实现
DefaultObjectFactory
比较简单,针对一些集合做了默认实现,例如需要创建的对象是List
的话,默认使用ArrayList
。创建过程就是通过是反射构造函数创建。如果想实现自己的ObjectFactory
可以在默认实现上进行扩展。
2.1.8 objectWrapperFactoryElement()
对象包装器工厂,DefaultObjectWrapperFactory
为默认实现。不过默认的啥也没做,还反手给你抛个异常。解析的代码如下,和ObjectFactory
如出一辙。
对于
ObjectWrapper
具体是个什么东东之类的,为节省篇幅这里不做过多讲解,等用到的时候再唠唠。
2.1.9 reflectorFactoryElement()
反射对象工厂。这我们就很熟悉了,在上文我们已经介绍过了,这里不过多描述了。配置代码如下
看来和上个方法也是一样的。
2.1.10 settingsElement()
这方法就是通过settings
标签给出的属性填充到Configuration
中。代码如下,省略部分:
2.1.11 environmentsElement()
环境配置。该标签使得我们可以配置多套环境,例如生产,测试或者开发等等。
关键代码已经注释,从代码可以看出
Environment
代表当前激活的环境。内部还整合了事务工厂和数据源配置。MyBatis
还使用了Transaction
来包装数据库连接,还支持DataSource
数据源配置,这部分内容可以查看文档。源码还是挺简单的,感兴趣的可自行了解,不懂的也可以评论留言。
2.1.12 databaseIdProviderElement()
数据库厂商标识,针对不同数据执行不同的Sql
语句。可以指定不同的实现或属性映射列。默认实现是VendorDatabaseIdProvider
。
可见
DataBaseIdProvider
只是一个工具类,用来填充Configuration
的dataBaseId
。
原理比较简单,根据JDBC
获取databaseProductName
:
- 若配置了
peoperties
则进行key
值匹配,匹配成功返回value
,没有匹配项返回null
。 - 直接返回获取到的
productName
。
这些前提都是在你配置了databaseIdProvider
标签的情况下。不然Configuration
的dataBaseId
就是空的。
2.1.13 typeHandlerElement()
MyBatis
在设置预处理语句(PreparedStatement
)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成Java
类型。
对于TypeHandler的使用说明可参照文档。对于默认类型处理器在TypeHandlerRegistry
的构造函数中进行了注册。
处理代码如上图,这里我们还是以包扫描为路线进行分析。
register()
方法处理逻辑和typeAlasesElement()
包扫描时一致,使用ResolverUtil
进行查找匹配。
在阅读前,得先了解TypeHandlerRegistry
中的几个属性,不需要完全记住,大概了解一下,用到的时候回头再看就行。
了解以上属性后,我们进入包扫描的核心方法:
以上通过是否添加
@MappedTypes
注解进行了流程划分。我们先看在添加了该注解的情况下是如何处理。核心方如下:
需注意注解的
value
是一个数组,而不是只是单个,也就是我们可以指定多个java
类型。每个java
类型都会创建一个TypeHandler
实例,如上参数typeHandler
代表的就是当前处理的java
类型的实例。
在之前只是是通过@MappedTypes
获取java
类型创建TypeHandler
实例的过程。而之后就是解析@MappedJdbcTypes
注解获取到数据库所对应类型。includeNullJdbcType
默认false
此处就忽略了(是否多添加一个TypeHandler
)。
java
对应类型有了,jdbc
对应类型也有了,那最后就是保存咯
朴实无华。这里总结下这个保存流程(后续还会再提到):
- 能够解析到
java
类型,将会添加至typeHandlerMap
(需要注意TypeHandlerMap
替换问题)。 - 其他情况只保存到
allTypeHandlersMap
。
以上就是TypeHandler
包扫描的过程了。不难看出,包扫描主要逻辑就是针对注解的处理,这也是官方说的自动发现
功能。其实除了@MappedTypes
注解发现java
类型外,还支持通过TypeReference<T>
泛型指定,同时指定的话,注解优先级更高。
包扫描到这就结束了,单个注册的方式其实和包扫描一致,区别在于可能xml已经给出了对应的类型,从而不需要扫描注解。同时单个注册时,xml
配置优先级更高。
以上可以说是配置TypeHandler
的整个逻辑了。
2.1.14 mapperElement()
解析mapper
文件。节省篇幅,我打算单独写一篇解析mapper
的文章。此处就不多介绍了。
3. 总结
本篇主要是对配置文件解析,填充Configuration
流程做了解析。MyBatis
虽然代码注释少(刚开始看还是有点难受),好在官方文档挺全。
解析配置的整体流程还是挺清晰的。针对一些功能的处理方式,以及运用到的设计模式(工厂,模板)是值得学习的。
写作不易,如有错误,欢迎指正~