这样看 mybatis,谁都会分析源码!
![这样看mybatis,谁都会分析源码!](https://static001.geekbang.org/infoq/bf/bff2a738c8d6d3245ab080286702e38e.png)
java程序员访问数据库的方式有很多种,为了简化开发,都会选择使用框架访问数据库,而mybatis是我们常用的一种操作数据库的框架。
本期我们通过展示实际测试结果,带领大家分析mybatis的源码,提升对框架的理解。
本文相关的分析参照资料来源:
mybatis源码包:版本号3.4.1
关注公众号,输入关键字“java-summary”,即可获得源码。
1. JDBC访问数据库
访问数据库的方式有多种,可以使用原生的JDBC,也可以使用spring+mybatis,还可以使用springboot+mybatis。这几种方式一个比一个简单,需要的配置也是一个比一个少,但原理都是通用的。最底层也还是我们熟悉的JDBC。
万变不离其宗,下面我们对照JDBC使用方式,分析mybatis源码。
sql
这里是本文测试需要用到的sql语句
TestJDBC.java
JDBC的使用,主要步骤有四步:加载驱动类、获取连接对象、创建Statement、执行sql。
上面的配置信息可以看成两类:数据库连接配置、sql语句配置。大家记住这两配置,后面不管是使用spring+mybatis,还是使用springboot+mybatis,都离不开这两类配置。
这是最直接的连接方式,通过驱动直接连接数据库,只需要集成mysql-connector-java
这一个jar包即可。
2.直接使用mybatis访问数据库
MybatisTest.java
上面是使用mybatis的简单测试样例,可以直接运行main方法。整个执行过程大概有三步:
通过
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml")
加载配置文件中的信息,然后生成SqlSessionFactory。通过SqlSessionFactory获得SqlSession。这里的SqlSession是关键,后面我们会重点分析源码。
使用SqlSession获得数据库中主键id=1的数据记录。这里使用了两种方式,第一种是使用
session.selectOne()
,第二种是使用UserEntityMapper.selectByPrimaryKey()
。
这种方式我们使用了两个jar包,几个是驱动mysql-connector-java
,一个是mybatis框架mybatis
。使用mybatis框架,将上面JDBC的相关对象进行了封装,这里虽然我们只看到了SqlSession
,其实底层还是JDBC。
mybatis-config.xml
这个配置文件中的信息包括了上面JDBC中说的两类配置:数据库连接配置、sql语句配置。这里只是通过xml文件的形式进行集中配置的。其实本质都是一样的。
UserEntityMapper.java
这是用户对象的接口。在实际开发中直接使用这个类就可以了,参考上面的MybatisTest.java中的sql执行的第二种方式。
UserEntityMapper.xml
这是sql语句的Mapper.xml文件,这个文件用来写sql语句,和UserEntityMapper.java文件对应。
3.源码分析
3.1.mybatis.jar项目结构
首先,打开mybatis.jar包,来看看项目的结构。
![](https://static001.geekbang.org/infoq/6e/6e78f06e33cb0512ca09ab769fb7abe7.png)
其中几个重要的package的简单说明:
session包:这个包是MyBatis接口层的核心类。其中包括SqlSessionFactory、SqlSession这两个接口及其默认的实现类。SqlSession是实现所有数据库操作的API。除此之外,还有一个Configuration,这个类是mybatis的配置类,mybatis-config.xml和UserEntityMapper.xml都会被加载到这个类中。
builder包:这个包中包含了xml文件解析相关的类,其中XMLConfigBuilder是用来解析mybatis-config.xml文件的,XMLMapperBuilder是用来解析UserEntityMapper.xml文件的。
executor包:这个包中包含了数据库操作的相关方法,其中Executor是MyBatis的核心,围绕着它完成了数据库操作的完整过程。Executor主要提供了sql语句查询等数据库操作的相关方法。
cache包:缓存是MyBatis里比较重要的部分,有两种缓存:1.一级缓存。BaseExecutor中根据MappedStatement的Id、SQL、参数值以及rowBound(边界)来构造CacheKey,并使用BaseExccutor中的localCache来维护此缓存。2.全局的二级缓存,通过CacheExecutor来实现,其委托TransactionalCacheManager来保存/获取缓存,这个全局二级缓存比较复杂;通过配置可以开启二级缓存。全局二级缓存是基于Mapper.xml的namespace实现的,连表查询生产的缓存在单表更新时不一定会被更新,可能产生数据的不一致问题;同时二级缓存的失效策略是一个更新操作会导致该namespace下的所有缓存失效;所以这个二级缓存很鸡肋,一般都不用,实际开发中使用redis/memocached代替。
datasource包:这个包是MyBatis自身提供的一个简易的数据源/连接池,主要实现类是PooledDataSource,这个连接池的实现比较简单,实际项目中我们会继承集成其他的连接池,如阿里的druid。
transaction包:这个包是MyBatis自身提供一个简单的事物处理,并不支持内嵌事务这样较复杂的场景,所以在实际开发中会委托Spring来处理事务实现真正的与开发者隔离。
reflection包: 在MyBatis中大量地使用了反射,需要频繁地读取Class元数据,这个包对常见的反射操作进一步封装,以提供更简洁方便的API。
io包:这个包提供读取资源文件的API、封装MyBatis自身所需要的ClassLoader和加载顺序。
3.2.SqlSessionFactory
看源码时,可以通过debug上面的MybatisTest.java一步一步往下看。这里是MybatisTest.java中SqlSessionFactory的生成这一步的代码:
获得SqlSessionFactory的目的是为了下面获得核心的SqlSession,从而访问数据库。获得SqlSessionFactory的过程就是解析mybatis-config.xml和UserEntityMapper.xml,并生成可重复使用的Configuration的过程。
通过debug模式,进入到这个SqlSessionFactoryBuilder().build()方法中,找到核心代码:
![](https://static001.geekbang.org/infoq/28/289aed2ce24107bdfe11973754bf0970.png)
3.3.XMLConfigBuilder
上面出现了XMLConfigBuilder,他是用来解析mybatis-config.xml文件的,具体解析的过程就是调用XMLConfigBuilder.parse()`,返回一个Configuration。下面的parse方法中可以看到mybatis-config.xml文件的节点标签:比如父节点configuration、settings标签,properties标签, typeAliases标签, environments标签,mappers标签等。
![](https://static001.geekbang.org/infoq/38/387b2eea197709040bd2af0205b8b364.png)
上面的environments标签解析,this.environmentsElement(root.evalNode("environments"))
就是获取mybatis-config.xml文件中数据库连接参数的过程。可以看出这里,这里生成了TransactionFactory、DataSourceFactory、DataSource等信息,并将信息存储在Configuration.environment成员变量中:
![](https://static001.geekbang.org/infoq/28/28e7c18219864d35724990fb3ad064e4.png)
3.4.XMLMapperBuilder
上面mappers标签解析,this.mapperElement(root.evalNode("mappers"))
,就是获取mybatis-config.xml文件中mappers节点中配置的Mapper.xml的过程,mppers节点中有两种配置方式,一种是配置package,一种是配置resource,针对这两种配置有不同的分支做解析:
![](https://static001.geekbang.org/infoq/6d/6d9db9c9be0ccd9238a5c0a914bdc5cd.png)
这里我们以mybatis-config.xml文件中mappers节点resource配置为例说明UserEntityMapper.xml的解析过程。整个解析使用的是XMLMapperBuilder.parse()方法,下面的parse方法中可以看到UserEntityMapper.xml文件的节点标签:比如父节点mapper、resultMap标签,sql标签, select标签, insert标签。其中子标签的解析也有具体的方法:
![](https://static001.geekbang.org/infoq/86/86758616a9a1ac544ac274659f8bf9f4.png)
其中this.buildStatementFromContext(context.evalNodes("select|insert|update|delete"))
方法内部最终会调用Configration的this.mappedStatements.put(ms.getId(), ms)
,这里mappedStatements是一个HashMap,他的key就是测试类中 session.selectOne(key,params) 的key,比如key="com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey";这其实就是将UserEntityMapper.java与UserEntityMapper.xml中的方法映射起来的过程。
![](https://static001.geekbang.org/infoq/71/71b27cbc2dec20520c92ae0352610dbe.png)
3.5.Configration
执行完上面的整个过程,我们最终获得初始化完成的Configration,这个Configration已经持有了mybatis-config.xml和UserEntityMapper.xml这两个配置文件中的所有信息,后续的很多操作也是从这个配置类中获取相应的配置参数的,而且Configuration对象是全局唯一的。可以看出,这个Configration应该是很重的,内部包含了太多的信息。
调用DefaultSqlSessionFactory(Configuration configuration)的构造器,即可获得DefaultSqlSessionFactory。Configuration对象与DefaultSqlSessionFactory是1对1的关系,这也就意味着在一个DefaultSqlSessionFactory衍生出来的所有SqlSession作用域里,Configuration对象是全局唯一的。同时SqlSessionFactory提供了getConfiguration()接口来公开Configuration对象。
3.6.SqlSession
通过上面的代码我们获得了DefaultSqlSessionFactory,通过调用sqlSessionFactory.openSession()就可以获得SqlSession对象。
openSession方法通过调用openSessionFromDataSource方法获得DefaultSqlSession:
![](https://static001.geekbang.org/infoq/9b/9b9456dd4590c39cf7273d42fb4899f9.png)
3.7.Executer
在获取SqlSession的同时,也会实例化一个Executer,后面执行sql的时候会用到。这里会根据Executor的类型差别,获取不同的Excutor,默认是SimpleExecutor;这些Executor都继承BaseExecutor或者直接实现Executor接口。 其中BaseExecutor的localCache是一级缓存。而CacheExecutor实现了二级缓存。
MyBatisExecutor,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。
![](https://static001.geekbang.org/infoq/24/2428e4f0116596ae40791e24baccca53.png)
获取到SqlSession后,在测试类中就可以通过调用session.selectOne("com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey", 1)
获得主键id=1的用户信息了。
SqlSession是一套操作数据库的接口,包括数据库的CRUD的各种操作,也是mybatis中最核心的一部分内容之一。DefaultSqlSession.selectList()方法查询数据库的。首先通过key(如:com.wuxiaolong.mybatis.mapper.UserEntityMapper.selectByPrimaryKey)到Configration中找到这个sql语句相关的映射对象,然后调用executor.query()
执行sql:
![](https://static001.geekbang.org/infoq/a9/a956da38a8a7df8c472fcd9e48b2b079.png)
进入上面`executor.query()方法内部,我们最终找到了BaseExecutor.query()方法,在这里可以看出,mybatis的BaseExecutor对象是有一个缓存的,缓存在成员变量localCache中;其实,这个localCache就是我们常说的一级缓存。
![](https://static001.geekbang.org/infoq/b2/b24a45c218a7877b812022fb6e24198c.png)
进入到方法queryFromDatabase内部,最终在SimpleExecutor中找到了doQurey方法。看到这个方法,大家就很熟悉了,这里的Statement对象就是JDBC中使用的对象了,这里就是数据库的相关操作了。
![](https://static001.geekbang.org/infoq/54/54b86dda128a965607bc9f627c5f4bc5.png)
这在了我们同时可以看到StatementHandler,他封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数、将Statement结果集转换成List集合
3.8.其他对象
处理上面介绍的对象外,还有几个数据库相关的对象也需要注意:
ParameterHandler:负责对用户传递的参数转换成JDBC Statement 所需要的参数,
ResultSetHandler:负责将JDBC返回的ResultSet结果集对象转换成List类型的集合;
TypeHandler:负责java数据类型和jdbc数据类型之间的映射和转换
3.9.设计模式
mybatis源码中,涉及了多种涉及模式,简单介绍几种:
Builder模式:例如SqlSessionFactoryBuilder、XMLConfigBuilder、XMLMapperBuilder、XMLStatementBuilder、CacheBuilder;
工厂模式:例如SqlSessionFactory、MapperProxyFactory;
代理模式:Mybatis实现的核心,比如MapperProxy,用的是jdk的动态代理;
模板方法模式:例如BaseExecutor和SimpleExecutor;
装饰器模式,例如CachingExecutor本身就是一个Executor,而其成员变量delegate还是一个从构造器中初始化的Executor。
3.10.总结
相比较spring的源码,其实看mybatis的源码相对还是简单很多的。
看完源码可能发现其实框架没有我们想象中的那么难,只是封装的好一点,考虑的情况多一点,其本质还是一样的。
看mybatis源码,可以首先关注DefaultSqlSession这个对象。这个对象起到承上启下的作用;说到承上,是指解析mybatis-config.xml和UserEntityMapper.xml这两类配置文件文件的过程,最终的结果就是DefaultSqlSession的成员变量 Configuration;说到启下,就是使用Executor对象访问数据库的过程。所以我们说SqlSession是mybatis中非常重要的。
![](https://static001.geekbang.org/infoq/fc/fc2ac4b1568857667eb5743cfa62f2f2.png)
关注公众号,输入“java-summary”即可获得源码。
完成,收工!
![](https://static001.geekbang.org/infoq/77/77e2a53cabfaaac7d98ee0a860144e29.gif)
【传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。
![](https://static001.geekbang.org/infoq/6a/6acafea3f4c9b96373b3f566ec7078e2.png)
版权声明: 本文为 InfoQ 作者【诸葛小猿】的原创文章。
原文链接:【http://xie.infoq.cn/article/2f998541f14289c8d3851fbb9】。文章转载请联系作者。
评论