MyBatis原理(一)—— 初始化

82 阅读14分钟

我正在参加「掘金·启航计划」

前言

近期在阅读MyBatis源码,网上各种文章也参差不齐,不太符合我的口味,于是就有了该篇。

约定:版本为3.5.11, 核心方法指代真正做事情的方法。环境搭建网上皆是,便不再赘述了。本篇主要分析以下两行代码

init

1. 资源加载

第一步要处理的肯定是加载其配置文件,MyBatis针对资源加载封装了一套易用工具,可将资源加载为Properties,FileStream等。核心功臣便是Resources类,但是Resources也不是真正做事情的,都委托给了ClassLoadWrapper

见名知意,我们此处使用的是以流的方式获取配置信息。在Resources类中,使用的是ClassLoaderWrapper进行解析,该类包装了多个类加载器。看图,各位应该都清晰了,注:以下截图来自ClassLoadWrapper

ClassLoaderWrapper

后续便是循环classLoader#getResourceAsStream查找资源,针对不同的类加载器,可能需要/前缀,在加载失败时,便会加上该前缀重试。

2. 资源解析

MyBatis交互以SqlSession为核心,而该类的获取需通过SqlSessionFactory获取。当然也可以自己new,只不过用人家的更省事!先看下对应类图

SqlSessionFactory & SqlSession

定睛一看,工厂方法,那这SqlSessionManager是什么鬼?自身是Factory又是SqlSession,鉴于内容完整性在这里简述一下:内部也是使用的DefaultSqlSessionFactory获取Session,但是使用ThreadLocal进行了session隔离,同时增加了自动提交和回滚机制。由于也没其他地方使用了SqlSessionManager这里不再展开,可自行了解。

为了便捷创建SqlSessionFactoryMyBatis又提供了SqlSessionFactoryBuilder供我们使用。核心方法如下

SqlSessionFactoryBuilder

不难看出使用的是XmlConfigBuilder#parse()解析配置文件生成Configuration对象

XmlConfigBuilder#parse() XmlConfigBuilder继承至BaseBuilder,与此类似的还有各种XmlXXXBuilder用于解析不同内容。可以看到有趣的一点是,对于父类构造参数是直接new Configuration(),也就是说我们也可以直接创建DefaultSqlSessionFactory对象,但是这样的话Mapper的映射关系就得由我们自己维护(详情可见(Xml方式构建)。

2.1 解析节点

MyBatis使用XParser解析Xml,并封装了一个XPathParser,在其内部使用XMLMapperEntityResolver将外部DTD文件转为内部资源(路径为org/apache/ibatis/builder/xml/),避免下载所带来的资源消耗。我们只需知道XPathParserXml解析工具类即可。

2.1.1 propertiesElement()

解析<properties>标签内容,先上该方法代码 propertiesElement 方法不难,直接给出结论:针对<properties>标签,reosurceurl属性只能存在一个。Configuriton对象内的属性设置优先级最高,其次通过url或者resource配置的,最后是标签内的属性。

在使用SqlSessionFactory#build()时可传递Properties对象,该对象最终会设置到Configuration中。

2.1.2 settingsAsProperties()

解析<settings>标签,但是该方法只是将标签内容以Properties返回

settingsAsProperties

这个方法别看他内容少,但涉及到的东西还是挺多的。首先,如何限制设置的属性是可用的,而不是乱填的?针对这个问题,我们可以从代码中找到答案。

核心代码就是MetaClass.forClass,该类返回MetaClass对象,该对象就是类定义信息工具类,代码如下

MetaClass

内部聚合了ReflectorFactoryReflector,先说说Reflector吧,这个类表示一个类的类定义信息。比如类的getset方法信息,构造函数定义等。贴个参数图

Reflector

说完Reflector,那ReflectorFactory作用显而易见了,又是一个工厂。默认的实现使用了一个ConcurrentHashMap缓存已生成的ReflectorkeyClass<?>,也可以设置为非缓存,每次都new一个,应该没人这样干吧!

至于如何获取类中getset方法之类的过程(核心方法为new Reflector),节省篇幅就不详细介绍了,这里说下大概:都是通过反射进行的

  1. 获取没有参数的构造方法,也就默认构造方法。
  2. 解析get,set方法。
  3. 解析所有属性。

这其中有几个个细节问题,它怎么知道我们哪些方法是get哪些是set呢?这就需要我们的getset方法需要符合命名规范,也就是以getXXX,setXXX开头,get方法还可以is开头。多么朴实无华啊~

其二是如果有多个get或者set方法该如何选择?这也得分几种情况,此处以get为例:

  1. String getName()String getname()对于这种返回值类型相同,在添加时不会出现异常,但是在使用Reflector执行该方法时,会抛出异常,因为该get方法时含糊不清的,MyBatis无法判断哪个是有效的。
  2. String getName()boolean getname() 同上。
  3. boolean getName()boolean getname()取第一个定义的get方法,但如果最后get方法以is开头,则以最后一个is方法为准。
  4. 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修饰的),在这里会进行兜底处理,将解析getset方法没有处理到的属性再添加一遍,同时也会添加父类的属性。

到这里Reflector了解清楚了,MetaClass.forClass也就理解了,后面的就好办了。说好的不详细介绍呢~

接下来便是metaConfig.hasSetter(String.valueOf(key))这一句啦,见名知意是否存在对应名称的set方法。开头问题的答案也就跃然纸上~

2.1.3 loadCustomVfs()

VFS将底层磁盘文件系统屏蔽,提供统一操作方式。

loadCustomVfs 源码比较简单,从settings标签中读取vfsImpl值。一般我们也不会更改它,需要更改的话在settings标签中设置vfsImpl即可,可设置多个,用,隔开。也就是说我们可以有多个VFS实现,优先以用户指定顺序使用,其次是JBoss6VFS, DefaultVFS.class

2.1.4 loadCustomLogImpl()

配置MyBatis日志的实现,可以使用全限定名或别名。可使用的别名在Configuration创建时就已经添加好了,如下:

new Configuration() 默认加载日志顺序在官网已经说明了,此处不再赘述了。MyBatis会自动按照官网的顺序加载。一般情况下引入合适包就行了,不用配置logImpl属性,避免重复加载浪费资源。但配置该属性也有个好处,在存在多个日志实现的时候,使用自动查找会覆盖前面已配置的,例如:同时存在SLF4JLog4j2时,会使用SLF4J实现,而不是Log4j2,此时就可以用logImpl属性限定使用的日志实现。

loadCustomLogImpl 代码还是很简单的。但是此处涉及到资源损耗的问题,多一次反射创建:若指定logImpl的别名存在自动加载中,则会出现该情况。核心方法LogFactory.useCustomLogging()

2.1.5 typeAliasesElement()

类型别名注册,可用简短缩写代替长长的类限定名。默认已经添加多个缩写对应关系,详情可见TypeAliasRegistry构造方法。

该方法代码说明如下 typeAliasesElement 别名注册可使用批量或单个注册方式。批量注册的方式最终应该和单个注册的方式一样,区别在于哪些需要注册,这就涉及到类查找问题了。核心代码如下:

TypeAliasRegistry#registerAliases() 关键点在于ResolverUtil这个类。根据文档注释得知ResolverUtil用于定位在/a类路径中可用的、符合任意条件的类。而这个条件该怎么定义呢?在ResolverUtil中定义了一个内部接口Test,可以看作这是一个条件接口,使用该接口的matches()确认是否匹配。默认有两个实现IsAAnnotatedWithIsA指定是否是的关系,另一个实现则代表是否包含指定注解。这两个实现都很简单,大家可以自行阅读

回到正题,了解清楚ResolverUtil后直接看看find方法的实现:

ResolverUtil#find() 首先getPackagePath()会将.替换为/,所以在我们指定包时,采用/分割包,可以略微提升性能。第二步就是通过VFS获取指定包和其子包下的所有资源路径,循环判断.class资源是否符合ResolverUtil.Test,使用HashSet存储了所有符合的类对象。

这整个查找过程都没有抛出异常,只是简单的使用日志记录,可见对于包扫描来说,将异常延后到了别名使用处。这样做的好处在于处理多个资源加载时,不影响前面已加载的,同时让流程还可以继续。

回到最初调用的地方,接下来就是针对每个获取到的资源进行别名映射了,单个注册也是用的该方法:

TypeAliasRegistry#registerAlias() 方法挺简单的,优先使用配置文件中的alias属性,其次@Alias注解,最后使用小写类名。再次添加同类型的别名会进行替换,别名一样但是类型不同则会抛出异常。

针对loadCustomLogImpl()typeAliasesElement()有个点需要注意,在配置logImpl为自定义实现时是不能使用别名的。logImpl()会从别名列表中获取该类,无则加载,这时你传别名是会加载失败的。那为啥别名注册要放到日志配置后面呢?在我认为可能是容易产生两份日志的问题。如果放到前面,注册别名的日志可能由日志自动导致生成在别的文件,后续改变日志实现又导致生成的在另外一个文件中。

2.1.6 pluginElement()

该方法负责插件注册。虽说叫插件,但我认为叫拦截器更为合适。插件的使用可以参考官网文档

pluginElement() 在图中也标明代码注释,整体也是比较简单。有些细节还是需要注意的,实现Interceptor接口的类需要有无参构造方法。Configuration中保存了对过滤器链InterceptorChain对象,内部使用一个ArrayList保存所有的过滤器。具体过滤器的使用我们到使用到的地方在详细说明。

2.1.7 objectFactoryElement()

注册对象工厂 每次MyBatis创建结果对象的新实例时,它都会使用一个对象工厂(ObjectFactory)实例来完成实例化工作。默认的实现是DefaultObjectFactory。核心代码如下:

objectFactoryElement() 不难看出,和过滤器的代码基本同理。默认实现DefaultObjectFactory比较简单,针对一些集合做了默认实现,例如需要创建的对象是List的话,默认使用ArrayList。创建过程就是通过是反射构造函数创建。如果想实现自己的ObjectFactory可以在默认实现上进行扩展。

2.1.8 objectWrapperFactoryElement()

对象包装器工厂,DefaultObjectWrapperFactory为默认实现。不过默认的啥也没做,还反手给你抛个异常。解析的代码如下,和ObjectFactory如出一辙。

objectWrapperFactoryElement() 对于ObjectWrapper具体是个什么东东之类的,为节省篇幅这里不做过多讲解,等用到的时候再唠唠。

2.1.9 reflectorFactoryElement()

反射对象工厂。这我们就很熟悉了,在上文我们已经介绍过了,这里不过多描述了。配置代码如下 reflectorFactoryElement()

看来和上个方法也是一样的。

2.1.10 settingsElement()

这方法就是通过settings标签给出的属性填充到Configuration中。代码如下,省略部分:

settingsElement()

2.1.11 environmentsElement()

环境配置。该标签使得我们可以配置多套环境,例如生产,测试或者开发等等。 environmentsElement() 关键代码已经注释,从代码可以看出Environment代表当前激活的环境。内部还整合了事务工厂和数据源配置。MyBatis还使用了Transaction来包装数据库连接,还支持DataSource数据源配置,这部分内容可以查看文档。源码还是挺简单的,感兴趣的可自行了解,不懂的也可以评论留言。

2.1.12 databaseIdProviderElement()

数据库厂商标识,针对不同数据执行不同的Sql语句。可以指定不同的实现或属性映射列。默认实现是VendorDatabaseIdProvider

databaseIdProviderElement() 可见DataBaseIdProvider只是一个工具类,用来填充ConfigurationdataBaseId

原理比较简单,根据JDBC获取databaseProductName

  1. 若配置了peoperties则进行key值匹配,匹配成功返回value,没有匹配项返回null
  2. 直接返回获取到的productName

这些前提都是在你配置了databaseIdProvider标签的情况下。不然ConfigurationdataBaseId就是空的。

2.1.13 typeHandlerElement()

MyBatis在设置预处理语句(PreparedStatement)中的参数或从结果集中取出一个值时, 都会用类型处理器将获取到的值以合适的方式转换成Java类型。

对于TypeHandler的使用说明可参照文档。对于默认类型处理器在TypeHandlerRegistry的构造函数中进行了注册。 TypeHandlerRegistry() 处理代码如上图,这里我们还是以包扫描为路线进行分析。register()方法处理逻辑和typeAlasesElement()包扫描时一致,使用ResolverUtil进行查找匹配。

在阅读前,得先了解TypeHandlerRegistry中的几个属性,不需要完全记住,大概了解一下,用到的时候回头再看就行。

TypeHandlerRegistry.property

了解以上属性后,我们进入包扫描的核心方法:

TypeHandlerRegistry#register(Class) 以上通过是否添加@MappedTypes注解进行了流程划分。我们先看在添加了该注解的情况下是如何处理。核心方如下:

TypeHandlerRegistry#register 需注意注解的value是一个数组,而不是只是单个,也就是我们可以指定多个java类型。每个java类型都会创建一个TypeHandler实例,如上参数typeHandler代表的就是当前处理的java类型的实例。

在之前只是是通过@MappedTypes获取java类型创建TypeHandler实例的过程。而之后就是解析@MappedJdbcTypes注解获取到数据库所对应类型。includeNullJdbcType默认false此处就忽略了(是否多添加一个TypeHandler)。

java对应类型有了,jdbc对应类型也有了,那最后就是保存咯

TypeHandlerRegistry#register 朴实无华。这里总结下这个保存流程(后续还会再提到):

  1. 能够解析到java类型,将会添加至typeHandlerMap(需要注意TypeHandlerMap替换问题)。
  2. 其他情况只保存到allTypeHandlersMap

以上就是TypeHandler包扫描的过程了。不难看出,包扫描主要逻辑就是针对注解的处理,这也是官方说的自动发现功能。其实除了@MappedTypes注解发现java类型外,还支持通过TypeReference<T>泛型指定,同时指定的话,注解优先级更高。

包扫描到这就结束了,单个注册的方式其实和包扫描一致,区别在于可能xml已经给出了对应的类型,从而不需要扫描注解。同时单个注册时,xml配置优先级更高。

保存流程图

以上可以说是配置TypeHandler的整个逻辑了。

2.1.14 mapperElement()

解析mapper文件。节省篇幅,我打算单独写一篇解析mapper的文章。此处就不多介绍了。

3. 总结

本篇主要是对配置文件解析,填充Configuration流程做了解析。MyBatis虽然代码注释少(刚开始看还是有点难受),好在官方文档挺全。

解析配置的整体流程还是挺清晰的。针对一些功能的处理方式,以及运用到的设计模式(工厂,模板)是值得学习的。

写作不易,如有错误,欢迎指正~