日暮途远

日暮途远,涸辙难行;东隅已逝,桑榆非晚

mybatis源代码阅读笔记

1.前言

从听说过mybatis到开始接触,最后在公司项目中真正使用它已经快4年了,都没有真正去了解它的源代码,了解它的实现,实在是很对不起开发出这个框架的作者。最近,有在解读一些开源框架的源码,乘此机会了解一下mybatis。之前通过jdbc直接操作数据库,在java源代码中充斥着各种sql,作为轻微强迫症的我,完全不能忍;而mybatis的一个最大优点就是把开发者从拼接sql语句中解放出来,当然也有人会说Hibenate也有这样的功能啊,但想想Hibenate的hql,就感觉蛋疼,没有mybatis直接写sql来的方便明了。ok,闲话不多说,进入正题吧。下面会从mybatis的几方面来剖析这个框架:1)加载配置文件&初始化 2)一次sql执行过程分析 3)参数映射 4)sql解析&执行 5)结果映射 6)里面包含的设计思想

2.加载配置&初始化

2.1 首先抛弃spring,我们来看下原生的mybatis如何加载配置文件:

对应的sql mapper如下:

这个xml文件包含了mybatis的核心配置,包括数据源、事务管理器等配置,具体使用方法详见:http://mybatis.github.io/mybatis-3/zh/getting-started.html

当然,也可以不通过xml直接来构建SqlSessionFactory,不过我们在实际开发中一般不这么做。

2.2 Resources类

这个类提供了几个通过classLoader访问资源的方法,返回InputStream,URL或者Reader,这个类分装了一个ClassLoaderWrapper类,内部包含了多个classLoader,分别为外部传入的classLoader,默认classLoader,当前线程的上下文classLoader,ClassLoaderWrapper的类classLoader和系统classLoader:

遍历这些classLoader来获取资源输入流:

2.3 SqlSessionFactory

到目前为止,我们获得了配置文件的输入流,下一步SqlSessionFactoryBuilder负责创建SqlSessionFactory工厂类:

2.4

具体解析xml文件的逻辑这里就不分析了,反正原理就是根据W3C的DOM API生成Document对象然后进行解析。下面来分析一下Configuration对象的生成过程,首先在解析器创建的时候,Configuration对象已经初始化了:

2.5

Configuration对象也是复杂的一逼啊,它直接继承Object类,也不实现任何接口,现在先暂时不介绍类里面的所有字段,只介绍过程中用到的那些:

很显然,构造函数里面做的是注册别名和Class的映射关系,放到一个HashMap中;那这个映射关系是干嘛用的?别问我,我也不知道,继续分析下去吧,总有它存在的意义!

2.6 Configuration初始化完成之后,在解析阶段就开始具体的配置了:

想了解每个配置项作用的同学可以点这里:http://mybatis.github.io/mybatis-3/zh/configuration.html

2.7 现在让我们单独来分析每一步干了甚,首先看下properties属性的配置方法:

有意思的来了,因为Properties是一个HashMap的子类,所以每次调用putAll就会覆盖已经读取的同名属性,那么Properties的putAll方法被调用的顺序就有讲究了,那么问题来了,这个覆盖的优先级是怎么定的?从源代码中我们可以清晰的发现,Mybatis首先读取properties元素内的属性,然后会读取类路径或URL属性中加载的属性,最后读取sqlSessionFactoryBuilder.build调用时传入的属性,而属性覆盖的优先级相反,因此方法传入的属性优先级最高,资源文件及url配置的属性次之,properties元素指定的属性优先级最低,和mybatis官网中解释的一模一样,好吧,我承认我差不多是直接抄过来的。

2.8

然后看下类别名的配置方法,它把别名注册到了Configuration对象的TypeAliasRegistry字段中,而TypeAliasRegistry本质上也就是封装了一个String->Class的HashMap:

2.9 plugin

然后是插件plugin的配置方法,主要做的事情就是构造这些拦截类,配置到configuration对象中,具体源码就不分析了,不过plugin的功能倒是可以介绍一下,mybatis plugin的功能是允许在已映射语句执行过程中的某一点进行拦截调用。允许拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

ok,还是来看个栗子吧:

插件使用起来非常方便,只需要实现Interceptor接口,并且在注解中加上要拦截的方法调用的方法签名即可;当然,还需要在配置文件中加入下面这段配置:

这样,上面这个插件就会拦截所有Executor实例中的query方法调用。mybatis的plugin基于jdk动态代理,而Plugin这个类就是我们所要找的InvocationHandler接口实现类。Plugin类提供一个wrap的静态方法,该方法可以决定返回目标类还是代理类:

可以看到代理类中封装了一个插件类,当拦截生效时,Plugin类的invoke方法被调用:

进而,插件类的intercept方法被调用,而我们写在插件类中的真正的代码才开始生效,还是挺简单的吧,只要写个插件类就能在例如执行sql增删改查的时候织入自己的代码。有点意思,有了这个功能,就能衍生出很多五花八门的用法了,有啥功能?别问我,自己去想。ok,配置configuration类的第三步,把插件类添加到了Configuration对象中的InterceptorChain拦截链中了。

第四步和第五步配置了ObjectFactory和ObjectWrapperFactory,ObjectFactory接口的作用是每次mybatis创建结果对象实例的时候加入自己个性化的行为,而ObjectWrapperFactory的作用目前还未发现,这两步配置这里就不再分析了。

第六步是配置settings,其实也很简单,就是配置了mybatis的一些设置项;第七步,设置了环境配置environments,主要还是构造了TransactionFactory和DataSourceFactory两个工厂类;后面第八步、第九步不太重要,忽略不表;

2.10

最后介绍Configuration配置最重要的一个步骤,mapper配置;众所周知,mybatis的sql映射有两种:注解映射和xml映射,而一般我们用的最多的也就是xml映射,下面来看下mapper的配置过程:

这里分别处理了三种情况:1.根据类路径指定的mapper xml映射文件 2.根据url指定的mapper xml映射文件 3.根据Mapperclass指定的 mapper配置。前两者处理方式几乎一致,只是区别在文件读取的步骤;后者没有解析过程,直接把mapperclass注册到Configuration对象中。

2.11

下面来看一下mapper xml文件是怎么被解析的

分别从配置文件中读取配置信息并存在configure对象的缓存中

3.创建SqlSession

在第二步中我们获得了SqlSessionFactory工厂类,通过openSession就能得到SqlSession,下面我们看下这个openSession具体干了哪些事:

这个方法有三个参数,分别表示本次sql会话选择的执行器(SIMPLE,REUSE,BATCH)、事务隔离级别(NONE,READ_COMMITTED,READ_UNCOMMITTED,REPEATABLE_READ,SERIALIZABLE)和是否自动提交,这里默认的分别是SIMPLE,null和false。

4.执行select语句

talk is cheap, show me the code

下面来看一下executor的query方法:

4.1 prepareStatement

4.1.1 DefaultParameterHandler

4.2 mybatis中的参数类型处理

这里有个地方值得细细琢磨,mybatis如何设置入参和获取结果集呢?原来有个TYPE_HANDLER_MAP的map存放着映射关系,key是Type,value是Map<JdbcType,TypeHandler<?>>,这个TypeHandler又是何方神圣?

再来看下IntegerTypeHandler的实现:

关键在于ps.setInt和rs.getInt,这些都是JDBC里面的一些基本方法。那么现在,也可以解释mybatis是怎么处理参数类型的转换了:

1.首先通过TypeHandlerRegistry类注册了一系列java基本类型的TypeHandler

2.再根据参数类型获取TypeHandler

3.调用特定类型的TypeHandler做ps.setXXX或rs.getXXX

最后剩下的就是执行ps.execute(),收集结果。

4.3 Bean类型参数

上面分析了Integer类型参数的传取值过程,如果对象是一个JavaBean类型,那么mybatis如何来做到传取值呢?

主要通过反射的原理,下面来看下BeanWrapper类:

 

配置文件#{xxx}中指定的变量名,通过反射获取getXxx()方法来取得对象的属性值,然后再同4.2步类型把属性值当做基本类型,使用TypeHandler来处理;同样,取值转换也是类似,通过反射获得Bean对象的构造方法,构造出初始对象,然后根据配置文件中resultMap元素定义的对象column名称,获取对应的属性名称,通过反射setXxx()方法设置对象属性值。

4.4 动态sql

4.4.1 if标签

动态sql是mybatis的其中一个强大功能,例如:

实际上在4.0步骤之前,mybatis就执行了动态sql的生成。

 

上述动态sql对应的rootSqlNode类型是MixedSqlNode,包含一组子sqlNode,这里分别是StaticTextSqlNode,IfSqlNode和StaticTextSqlNode。其中IfSqlNode的apply方法实现如下:

4.4.2 foreach标签

当然foreach标签的实现原理也是类似的,只是比if标签要稍微复杂一点

再往回看4.4.1步:

在后续的参数设置步骤4.1中,会放入?对应的值

4.5

mybatis还有一个有意思的地方是,没有实现类的DAO接口居然可以正常使用,给出如下

mapper:

 

接口声明:

应用场景:

原理:

1) 首先上面这段mapper xml会被mybatis在2.6步解析,MappedStatement会被注册到configuration对象中;

MappedStatement statement = statementBuilder.build();

configuration.addMappedStatement(statement);

同时,对应的接口SqlIntf.class及其对应的Mapper代理工厂类也会被注册到configuration中:

2)调用session.getMapper时,会从configuration对象中获取Mapper的代理工厂类

3)代理工厂类MapperProxyFactory生成代理类的方法:

 

4) 代理类MapperProxy:

这样,通过jdk动态代理,调用SqlIntf.getA(1)实际上调用的是MapperProxy代理类的invoke方法,具体执行sql的逻辑在mapperMethod.execute中实现:

最后同4步骤中的例子类似,执行了sqlSession方法蔟中的方法。

这里mybatis有个默认的约定:

1) mapper xml配置文件中的mapper元素的namespace属性值要跟接口的全路径名一直,否则就无法mybatis就无法代理这个接口 2)sql的id属性要跟接口的方法名一致,否则执行接口方法时会找不到方法。如果使用注解实现的原理类似,在解析mapper xml配置时选择解析注解里面的内容。

恭喜你,终于快要结束了,下面还有两张流程图~

5.补两张流程图 

5.1 mybatis读取配置文件流程图

5.2 mybatis执行一次查询sql流程图

 

点赞
  1. 疯狂的上帝说道:

    博主啥时候分析一下 hibernate 源码呢,同样都是 ORM 框架呢,我们公司用 hibernate ,很多小公司都是用 hibernate 吧

    1. 岁月无痕说道:

      确实,我们公司也是用 hibernate ,简单方便啊

      1. 大石头和小巨象说道:

        那是小公司小团队,真正的大公司都是 batis 系列,对 SQL 有更好的掌控力,DBA 可以更高的优化 SQL 的效率。当然,小团队没有 DBA 的用 hibernate 确实要省事许多,业务较小的时候也很难碰到性能优化的时候。 :smile:

  2. 匿名说道:

    好长,要找时间才能慢慢看,不过我喜欢这类源码分析类的文章,点个赞 :mrgreen:

发表评论

电子邮件地址不会被公开。

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">