下面以官网给出的入门实例来简单分析MyBatis的源码
一、从 XML 中构建 SqlSessionFactory
1、使用
每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为中心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。
从 XML 文件中构建 SqlSessionFactory 的实例非常简单,建议使用类路径下的资源文件进行配置。但是也可以使用任意的输入流(InputStream)实例,包括字符串形式的文件路径或者 file:// 的 URL 形式的文件路径来配置。MyBatis 包含一个名叫 Resources 的工具类,它包含一些实用方法,可使从 classpath 或其他位置加载资源文件更加容易。
1 | String resource = "org/mybatis/example/mybatis-config.xml"; |
下面看一下源码中发生了什么
2、源码
2.1、SqlSessionFactoryBuilder
1 | package org.apache.ibatis.session; |
可以看出:
- 不同类型的构造方法最后都调用到build(InputStream inputStream, String environment, Properties properties)
- 首先定义一个XMLConfigBuilder,这个类主要完成XML文件的解析,通过调用parse()方法,生成一个Configuration对象。前面提到过,Configuration对象中包含着全局的配置参数。
- 最后调用build()方法,将解析生成的Configuration对象作为参数,来构造一个SqlSessionFactory,SqlSessionFactory是一个接口,这里生成的是它的默认实现类:DefaultSqlSessionFactory
下面看一下DefaultSqlSessionFactory类:
2.2、DefaultSqlSessionFactory
1 | package org.apache.ibatis.session.defaults; |
DefaultSqlSessionFactory中有一个final的Configuration变量,构造方法就是将传入的Configuration绑定到改常量,并且不可修改。
至此为止,就生成了SqlSessionFactory对象,可以看出该过程主要就是对配置文件XML文件的解析,然后生成Configuration对象并绑定到SqlSessionFactory上。
下面继续看
二、从 SqlSessionFactory 中获取 SqlSession
1、使用
既然有了 SqlSessionFactory ,顾名思义,我们就可以从中获得 SqlSession 的实例了。SqlSession 完全包含了面向数据库执行 SQL 命令所需的所有方法。你可以通过 SqlSession 实例来直接执行已映射的 SQL 语句。例如:
1 | SqlSession session = sqlSessionFactory.openSession(); |
诚然这种方式能够正常工作,并且对于使用旧版本 MyBatis 的用户来说也比较熟悉,不过现在有了一种更直白的方式。使用对于给定语句能够合理描述参数和返回值的接口(比如说BlogMapper.class),你现在不但可以执行更清晰和类型安全的代码,而且还不用担心易错的字符串字面值以及强制类型转换。
例如:
1 | SqlSession session = sqlSessionFactory.openSession(); |
这里其实分两个部分,首先研究一下第一句话,也就是SqlSession的获取。
2、源码
2.1、DefaultSqlSessionFactory
再来看一下DefaultSqlSessionFactory中的openSession方法。
1 | package org.apache.ibatis.session.defaults; |
很多重载方法,调用的都是下面这个openSessionFromDataSource方法,有三个参数:
- ExecutorType
- TransactionIsolationLevel
- autoCommit
这里使用的都是默认值。
而这个openSessionFromDataSource方法的逻辑也很简单:
- 从Configuration中获取Environment
- 通过Environment获取TransactionFactory
- 通过TransactionFactory的newTransaction方法构造一个Transaction对象
- 通过configuration的newExecutor方法构造一个Executor对象
- 由于SqlSession也是一个接口,所以也返回它的默认实现类DefaultSqlSession。并且将configuration、autoCommit和将刚才生成的executor作为参数。
这个过程中主要就是生成了一个Executor对象。该对象非常重要,事实上sqlsession的所有操作都是通过它完成的。下面来看一下这个对象
2.2、Executor
1 | package org.apache.ibatis.session; |
由于生成Executor的方法在Configuration中,而Configuration对象包含了配置中所有的属性,所以这个类的代码特别多,虽然省略了很大一部分,但可以看出还是很长,上面特意保留了Configuration的所有属性。
生成Executor的方法newExecutor有两个参数:
- Transaction:主要用于构造Executor。
- ExecutorType:Executor的类型,根据它进行判断生成那种Executor。
该方法的逻辑:
检查传入的ExecutorType,如果为null,则将defaultExecutorType赋值给它,如果还为null,则将ExecutorType.SIMPLE赋值给它
根据ExecutorType的类型,创建对应类型的Executor。
- BatchExecutor:专门用于执行批量sql操作
- ReuseExecutor:会重用statement执行sql操作
- SimpleExecutor:只是简单执行sql没有什么特别的
判断是否开启cache
开启cache的话,就会创建CachingExecutor,它以前面创建的Executor作为唯一参数。CachingExecutor在查询数据库前先查找缓存,若没找到的话调用delegate(就是构造时传入的Executor对象)从数据库查询,并将查询结果存入缓存中。
最后加入插件
Executor对象是可以被插件拦截的,如果定义了针对Executor类型的插件,最终生成的Executor对象是被各个插件插入后的代理对象
至此,Executor就生成了。回到上面,生成的Executor将作为参数,用于构造SqlSession的默认实现类DefaultSqlSession。
2.3、DefaultSqlSession
1 | package org.apache.ibatis.session.defaults; |
DefaultSqlSession类中也有几个成员变量:
- Configuration
- Executor
- autoCommit
- dirty
- List cursorList
到此,就获取到了一个SqlSession,这个过程中主要就是生成了Executor。
三、从SqlSession中获取Mapper
1、使用
还是第二段代码
1 | SqlSession session = sqlSessionFactory.openSession(); |
上面只介绍了第一句话SqlSession的获取,下面看第二句,分析一个Mapper的获取。
首先先来看一下这个BlogMapper,它是我们定义的一个映射器类。
1 | package org.mybatis.example; |
它是一个接口,接口中所有方法都映射到对应的sql语句,在使用时,只需要调用接口中的方法,就可以执行对应的sql语句。而这个映射器类的作用就是把我们定义的接口方法和对应的sql语句映射到一起。
当然MyBatis 提供的全部特性可以利用基于 XML 的映射语言来实现
1 | <?xml version="1.0" encoding="UTF-8" ?> |
好的,下面再来看一下这句话,它获得了一个我们定义的映射器类的接口的实现类
BlogMapper mapper = session.getMapper(BlogMapper.class);
在源码中,它是怎样实现的
2、源码
在DefaultSqlSession中,这个方法调用了Configuration的getMapper方法。
1 | @Override |
在Configuration中,又调用了MapperRegistry的getMapper方法
1 | public <T> T getMapper(Class<T> type, SqlSession sqlSession) { |
2.1、MapperRegistry
1 | package org.apache.ibatis.binding; |
这个类中有一个重要属性就是上面的Map,它持有着所有的MapperProxyFactory,而map的key就是我们传入的映射器的类型,比如上面定义的BlogMapper.class。
- 通过传入的类型,从map中获取对应的MapperProxyFactory,如果map中没有,那么说明没有对应的映射类,直接抛出异常。这里是一个非常典型的工厂模式。
- 调用MapperProxyFactory类的newInstance方法,并且传入当前的sqlSession,生成一个映射器类的实例,即上面的BlogMapper的实例。
2.2、MapperProxyFactory
看一下MapperProxyFactory类
1 | package org.apache.ibatis.binding; |
这个类比较短,所以贴出了所有代码,从import中可以看出,这个类用到了Java中的动态代理。
这个类中包含了MyBatis的关键,利用动态代理技术来生成定义接口的代理对象,从而在调用对应方法的时候执行相应的SQL语句。
动态代理
再来回忆一下动态代理的相关知识,涉及的两个关键的类就是Proxy与InvocationHandler
通过Proxy.newProxyInstance()方法可以创建动态代理,这个方法需要三个参数:
类加载器:通过可以从已经被加载的对象中获取其类加载器并传递给它
上面使用的是mapperInterface.getClassLoader()来获取类加载器
希望该代理实现的接口列表(只能是接口,不能是类或抽象类)
上面使用的是new Class[] { mapperInterface },即MapperProxyFactory要代理的接口
InvocationHandler接口的一个实现
上面用的是一个MapperProxy类的对象,该类实现了InvocationHandler接口。下面来看一下MapperProxy这个类。
2.3、MapperProxy
1 | package org.apache.ibatis.binding; |
MapperProxy实现了InvocationHandler接口,所以肯定是用于动态代理的。
对于接口的所有方法调用,都会跳到它的invoke(Object proxy, Method method, Object[] args)
方法之中。它有三个参数:
- Object proxy:最终生成的代理实例。
- Method method:是被代理目标实例的某个具体方法,通过它可以发起目标实例方法的反射调用;
- Object[] args:是通过被代理实例某一个方法的入参,在方法反射调用时使用。
在这个方法的具体实现中我们可以看到:
首先判断调用被调用方法的getDeclaringClass()方法,该方法的作用为:
返回表示声明由此 Method 对象表示的方法的类或接口的 Class 对象。如果它与传入的Object对象相同,那么则直接调用,这块不是很理解。
再判断是不是isDefaultMethod,这个和上面第一次判断作用应该是差不多的,虽然不完全明白,但大概意思应该是一些没有映射的方法。
如果上面的判断都不满足,则需要代理执行映射的方法:
- 首先获取缓存:MapperMethod mapperMethod = cachedMapperMethod(method);
- 在缓存中查找该方法,如果缓存中没有,就新建一个然后存入缓存中。
- 最终获得一个MapperMethod对象,最后的SQl语句就是通过这个类来执行的,下面来看一下这个类。
2.4、MapperMethod
1 | package org.apache.ibatis.binding; |
关键属性
这个类从最上面的两个常量中就可以看出,保存着我们定义的方法和它对应的SQl语句,也就是说我们在Mapper类中定义的每个接口的每个方法最后都会以MapperMethod的形式存在于内存之中。
1 | package org.mybatis.example; |
构造函数
一个接口中有着多个方法,而MapperMethod却是以方法为单位的,从构造函数中可以看出,我们定义的方法是以MethodSignature形式存在的,而它则由接口与方法名来共同标识
1 | this.method = new MethodSignature(config, mapperInterface, method); |
而且,一旦构造出来,就会存放在缓存中,下次再获取的时候先从缓存中获取,也就是说每个方法的MapperMethod都只会有一个。
excute方法
在上面MapperProxy中,以调用的方法为key,找到了对应的MapperMethod,然后就返回了MapperMethod的excute方法,也就是说调用的方法被动态代理后,通过这个excute方法来执行对应的SQl语句。
在调用的时候传入对应的SqlSession与参数,然后判断对应的SQL语句类型,将传入参数合成到最后的SQL语句中,并返回最终的执行结果。
至此,一个最简单的查询就分析完成了。
四、总结
1、几个关键的类
通过上面的分析,已经明白了我们的各种配置文件是怎么在MyBatis中存在的。
1.1、Configuration类
首先我们通过XML文件对全局的所有配置,都是以Configuration类的形式存在的,并且通过DefaultSqlSessionFactory来持有它,通过源码可以发现,在每个生成的DefaultSqlSession中,也会持有这个Configuration。
1.2、MapperProxy类
这个类有两点:
- 它对应着我们定义的接口,比如上面的BlogMapper,保存着这个接口中定义的所有方法和方法对应的SQL语句
- 另一方面它实现了InvocationHandler接口,所以对接口的没法方法的调用都会在这个类的invoke方法中得到真正的执行
1.3、MapperMethod类
上面说MapperProxy类定义了一个接口中的所有方法和方法对应的SQL语句,而这个方法和方法对应的SQL语句就是通过MapperMethod来定义的,而且还负责这个方法具体的执行。
2、查询过程
再来看一下整个过程,
1 | String resource = "org/mybatis/example/mybatis-config.xml"; |
大致分为以下几个步骤:
2.1、获取SqlSessionFactoryBuilder
该类没有属性,主要就是完成文件的定位
2.2、获取SqlSessionFactory
- 通过SqlSessionFactoryBuilder的build方法来实现XML文件的解析,并将配置信息生成一个对应的Configuration对象
- 生成一个SqlSessionFactory,保存对应的Configuration。
以上两个步骤都是在最开始完成的,两个对象通常都是只要一个实例就足够了,而下面则需要每次查询都去生成。
2.3、获取SqlSession
SqlSession对应着一次数据库的会话。所以想要对数据库进行操作,首先要获取一个SqlSession。
- 先生成一个Excuter,它是最后的执行者,包含着执行此次会话的各种参数以及事务管理的各种方法
- 再将原来的Configuration的交给它持有。
2.4、获取Mapper
从我们使用的角度,每个操作都是对应的接口的,而我们通过对接口中方法的调用完成对数据库的操作,首先就要获得这个接口的一个实现类,从源码中我们可以知道,实际生成的是这个接口的动态代理对象,也就是我们获取的这个Mapper。
- 根据传入的接口类型,获取对应的MapperProxyFactory工厂。每个接口类型都有一个自己对应的工厂。
- 在工厂的newInstance方法,生成一个MapperProxy对象,这个对象对应着一个接口,并且通过一个map保存着该接口所有的方法和方法对应的SQL语句。
- 通过上面的MapperProxy来生成一个代理类的实例,因为它是InvocationHandler接口的实现,所以对代理类每个方法的调用都会在它的invoke方法中。
2.5、方法调用
获取到代理对象后,就可以通过调用方法来执行对应的SQL语句了。
- 通过动态代理,在MapperProxy的invoke方法中,找到对应的方法。
- 上面说了MapperProxy保存着这个接口中所有方法与对应的SQL语句,其实是通过一个MapperMethod的类来定义的。
- 找到方法对应的MapperMethod,并调用它的excute方法,执行对应的语句。
3、总结
- 世界上几乎所有的问题都可以用跳脱的想象力配合超凡的行动力来解决,以前一直想学习一下MyBatis,但就是下不了手,这次不知道是怎么了一鼓作气,用了一天时间就把源码看完了,另外一天写这篇文章
- 这是第一次完全自己看着源码分析的,因为网上相关的资料并不多,而且源码实现上也没有那么复杂。
- 里面涉及了两种设计模式:
- 工厂模式:每个MapperProxy都对应自己的工厂
- 动态代理模式:以前不知道这个也是设计模式的一种,上次面试问到了竟然没有答上来,它最经典的例子应该就是AOP了。