一文彻底掌握MyBatis的底层工作原理

314 阅读10分钟

hello,我是周瑜(公众号:IT周瑜),这篇文章会分析MyBatis底层很多核心对象和核心机制的底层实现原理,文章较长,内容较多,建议收藏。

我们先看MyBatis是如何使用的:

// 1、解析配置文件
String resource = "config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 2、生成SqlSession对象
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
    
    // 3、生成UserMapper代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 4、执行SQL
    UserInfo userInfo = userMapper.getUserInfo(1);
    System.out.println(userInfo);
}

主要分为四个步骤:

  1. 利用SqlSessionFactoryBuilder解析mybatis-config.xml配置文件,得到SqlSessionFactory对象
  2. 利用SqlSessionFactory对象得到SqlSession对象
  3. 利用SqlSession对象得到UserMapper代理对象
  4. 利用UserMapper代理对象执行方法,从而执行SQL

本文也会按这个顺序来分析每个步骤涉及到的核心对象和核心机制的底层实现原理。

配置文件解析

我们先来分析MyBatis是如何解析mybatis-config.xml配置文件的。

我们通过mybatis-config.xml文件可以对MyBatis进行配置,它有以下子节点可以配置:

<configuration>
    <properties/>
    <settings/>
    <typeAliases/>
    <typeHandlers/>
    <objectFactory/>
    <objectWrapperFactory/>
    <plugins>
        <plugin/>
    </plugins>
    <environments>
        <environment/>
    </environments>
    <databaseIdProvider/>
    <mappers/>
</configuration>

所谓解析配置文件,实际上就是解析以上各个节点,在MyBatis源码中,一个XML节点对应一个XNode对象,而XNode中提供了一个getChildren()方法,用来返回子节点,也就是返回一个List<XNode>

MyBatis会先解析XML节点得到XNode对象,进而解析XNode对象得到其他对象,比如:

  • 解析<typeHandlers>节点,将得到TypeHandler对象
  • 解析<environment>节点,将得到Environment对象
  • 解析<mappers>节点,将得到MappedStatement对象(后文会分析这个对象)、ResultMap对象(后文会分析这个对象)、ParameterMap对象等等
  • ...

这些解析出来的对象,都会保存在另外一个“大”对象中,这个对象就是Configuration对象,我们可以理解为,mybatis-config.xml配置文件对应的就是Configuration对象,mybatis-config.xml配置文件中的各个节点对应的就是Configuration对象中的各个属性,比如:

  • <environment/>节点,对应的是Environment environment属性
  • <properties/>节点,对应的是Properties variables属性
  • <objectFactory/>节点,对应的是ObjectFactory objectFactory属性
  • <databaseIdProvider/>节点,对应的是String databaseId属性
  • <plugins/>节点,对应的是InterceptorChain interceptorChain属性
  • <typeAliases/>节点,对应的是TypeAliasRegistry typeAliasRegistry属性
  • <typeHandlers/>节点,对应的是TypeHandlerRegistry typeHandlerRegistry属性
  • <mappers/>节点,对应的是Map<String, MappedStatement> mappedStatementsMap<String, ResultMap> resultMapsMap<String, ParameterMap> parameterMaps等属性(<mappers>节点解析出来的东西比较多)
  • ...

以上属性都可以在org.apache.ibatis.session.Configuration类中找到。

因此简单理解MyBatis的配置文件解析就是:通过解析mybatis-config.xml文件得到一个Configuration对象,后面在执行SQL时就可以直接从Configuration对象中取出相关配置来使用了。

这里着重分析一下MappedStatement对象和ResultMap对象,其他节点和对象本文就不花篇幅来分析了,感兴趣的同学可以动动小手点点赞或关注我,点赞比较多的话我就熬夜肝出来,哈哈。

MappedStatement对象

MyBatis中的一等公民自然就是我们定义的SQL语句了,也就是:

<select id="getUserInfo" resultType="UserInfo">
  select * from user_info where id = #{id}
</select>

和解析mybatis-config.xml中的节点类似,MyBatis在解析UserMapper.xml文件时,也是一个一个XML节点进行解析,对于<select><insert><update><delete>这四个节点是类似的,都是生成的MappedStatement对象,在MappedStatement对象中有一个SqlCommandType属性用来区分SQL的类型,SqlCommandType是一个枚举:

public enum SqlCommandType {

    UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH

}

在MappedStatement对象中还有以下核心属性:

  • String id,表示当前MappedStatement对象的id,由namespace+getUserInfo组成,namespace就是在UserMapper.xml文件中配置的接口名
  • StatementType statementType,默认为PREPARED,表示利用PreparedStatement来执行SQL
  • SqlSource sqlSource,表示对应的SQL语句(后文详细介绍)
  • List<ResultMap> resultMaps,表示对应的ResultMap(后文会详细介绍),一般只有一个,执行存储过程时可以设置多个
  • KeyGenerator keyGenerator,对于insert语句,如果设置了useGeneratedKeys=true,那么该属性的值为Jdbc3KeyGenerator,表示会在insert执行完后来获取新纪录对应的自增id
  • String databaseId,表示对应的databaseId,关于databaseId我还发现了源码中的一个bug并进行了修改,后续也给大家分享一下
  • ...

MappedStatement对象中的属性也很多,本文只介绍一下SqlSource对象和ResultMap对象。

SqlSource对象

一个SqlSource对象对应的就是我们定义的一个SQL语句,但是我们定义的SQL语句是多种多样的,大概分为以下几类:

select name from user_info
select name from user_info where id = #{id}
select name from user_info where ${col} = #{id}
select name from user_info <if test="id == 26"> where id = #{id}</if>
select name from user_info where id <![CDATA[=]]> #{id}
  1. 最简单的SQL语句
  2. 用了#{}
  3. 用了${}
  4. 用了<if>标签
  5. 用了<![CDATA[=]]>标签

而对于MyBatis而言,底层只分为了两类:动态SQL和静态SQL,分别对应DynamicSqlSource和RawSqlSource,RawSqlSource里面包含了StaticSqlSource,所以本质上就是DynamicSqlSourceStaticSqlSource

用了${}<if>等标签的就是动态SQL,这类SQL最终生成出来的是DynamicSqlSource对象,其他的(就算用了#{}cdata)都是静态SQL,这类SQL最终生成出来的是StaticSqlSource对象。

MyBatis中,所谓动态SQL,就是要等到执行SQL时才知道最终的SQL语句到底长啥样,比如select name from user_info where ${col} = #{id},不等到执行SQL时,就不知道${col}的值到底是什么,从而不知道最终的SQL是什么,对于<if>标签也是一样。

那用了#{}不也是一样吗?不也要等到执行SQL时才知道#{}具体的值吗?注意,MyBatis是对JDBC的封装,对于以上SQL中的#{}都需要替换为"?"才能利用PreparedStatement来进行预编译执行。

对于:

select name from user_info where id = #{id}

可以在生成SqlSource对象时,也就是解析XML节点时,直接将#{id}替换为“?”号。

而对于用了<if>标签或${}的SQL,则不行,因为没办法在解析XML节点时确定最终的SQL语句到底张啥样,比如:

select name from user_info <if test="id == 26"> where id = #{id}</if>

如果最终执行SQL时,发现if条件不成立,那么就没有必要把#{id}替换为"?"号了,对于${}也一样,我给的demo中,只传了一个字段名,但也可以给${}传一个更复杂的"子SQL",这样也只能在执行时,才知道这个“子SQL”中是不是有#{}需要替换为“?”号。

总结一下,MyBatis中的静态SQL,表示在解析XML节点时,就会将#{}替换为"?"号,而动态SQL需要等到执行SQL时,才能确定最终的SQL,才会将#{}替换为"?"号,然后走JDBC的流程给"?"赋值,最后执行SQL。

另外,SqlSource对象是由LanguageDriver对象生成出来的,相信有同学对LanguageDriver有印象,这里顺便提一下。

ResultMap对象

再来聊聊ResultMap对象,一个MappedStatement对象会对应一个或多个ResultMap对象,顾名思义,ResultMap是用来处理SQL返回结果的,先看一个demo:

<resultMap id="userMap" type="UserInfo">
  <id property="id" column="c_id"/>
  <result property="name" column="c_name" />
</resultMap>

<select id="getUserInfo" resultMap="userMap">
  select * from user_info where id = #{id}
</select>

当我们执行一条select语句时,有可能返回多行记录,通常,我们需要将这多行记录转成多个Java对象,比如List<UserInfo>,而这个过程就需要用ResultMap来做转换了。

一行记录对应一个对象,那么一行记录中的某个字段应该对应对象中的哪个属性呢,我们可以通过ResultMap来配置映射关系,当然,默认MyBatis是有自动映射机制的,比如id字段能自动映射到id属性,user_id字段能自动映射到userId属性,对于不能自动映射的字段和属性,我们通过ResultMap来进行手动映射就可以了。

注意,一个ResultMap是需要有一个type属性的,表示通过这个ResultMap可以将记录转换成哪个类型的对象。

大家再看看下面这个SQL:

<select id="getUserInfo" resultType="UserInfo">
  select * from user_info where id = #{id}
</select>

这个SQL用的是resultType,在MyBatis的底层,对于这种情况,会自动生成一个ResultMap对象,该对象的type属性就是resultType的值,特殊的是,该ResultMap对象没有手动映射关系,只会进行自动映射。

其实ResultMap可以很复杂,我们可以在<resultMap>标签中使用其他子标签,比如<constructor><collection><discriminator><association>等标签,还可能存在多ResultMap、多ResultSet的情况,本文就不展开分析了,同样,点赞如果比较多,我就会更新,哈哈。

好啦,关于MyBatis的配置文件解析本文就分享这么多了,接下来分析SqlSession对象的生成。

SqlSession对象

解析完配置文件就会得到一个SqlSessionFactory对象,默认为DefaultSqlSessionFactory对象,该对象中就包含了Configuration对象。

通过SqlSessionFactory可以得到一个SqlSession对象,而一个SqlSession对象中又包含了Executor对象,Executor对象中又包含了Transaction对象,Transaction对象中又包含了Connection对象,只不过当一个SqlSession对象创建出来后,Connection对象为null而已,那什么时候才有值呢?会在真正执行SQL的时候才去创建数据库连接,从而得到一个Connection对象并赋值。

其中Executor对象,可以用来实现:

  • 缓存功能,对应CachingExecutor
  • 批量功能,对应BatchExecutor

其中Transaction对象,可以用来实现:

  1. 从DataSource中获取数据库连接,对应JdbcTransaction
  2. 从Spring容器中获取数据库连接,比如拿到Spring事务创建的数据库连接,对应SpringManagedTransaction

因此,我们可以简单理解SqlSession对象:是用来获取数据库连接并执行SQL的。

比如,我们可以直接利用SqlSession对象来执行SQL:

try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
    
    Object result = sqlSession.selectOne("com.zhouyu.mapper.UserMapper.getUserInfo", 26);
    System.out.println(result);
    
}

SqlSession中提供了很多用来执行SQL的方法,比如selectOne()、selectList()、insert()、update()等,而在执行这些方法的过程中,就会判断当前SqlSession对应的数据库连接是否创建,如果没有就会先创建数据库连接,再执行SQL,创建出来的数据库连接Connection对象,会赋值给上图中的属性,该SqlSession对象下次执行SQL时就不需要再创建数据库连接了。

SqlSession生成代理对象

直接利用SqlSession对象执行SQL,不是很方便,所以SqlSession还提供了一个功能来生成UserMapper接口的代理对象,可以通过这个代理对象来间接的利用SqlSession对象来执行SQL,也就是文章开头给的demo:

// 1、解析配置文件
String resource = "config/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

// 2、生成SqlSession对象
try (SqlSession sqlSession = sqlSessionFactory.openSession(true)) {
    
    // 3、生成UserMapper代理对象
    UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

    // 4、执行SQL
    UserInfo userInfo = userMapper.getUserInfo(1);
    System.out.println(userInfo);
}

这样,我们只需要执行UserMapper代理对象的某个方法,就能间接的利用SqlSession对象来执行SQL了,而这个代理对象是通过JDK动态代理机制生成的:

其中mapperInterface就是UserMapper接口,sqlSession就是我们说的SqlSession对象,而MapperProxy对象就是JDK动态代理机制中的InvocationHandler,一旦执行代理对象中的某个方法,就会经过MapperProxy对象,利用SqlSession对象来执行对应SQL。

SQL执行

最后,我们来详细看看UserMapper代理对象执行某个方法时,底层会发生什么。

首先,需要根据当前接口名+方法名,找到对应的MappedStatement对象,这一步容易,MyBatis在解析生成MappedStatement对象时就会存到Configuration对象的Map<String, MappedStatement> mappedStatements属性中,key就是接口名+方法名,因此执行时,可以很方便的找到对应的MappedStatement对象。

然后根据MappedStatement对象中的SqlCommandType,也就是当前方法对应的SQL的SQL类型,来判断到底应该调用SqlSession的select方法,还是update方法,还是delete方法,还是insert方法。

然后拿出MappedStatement对象中的SqlSource对象,判断是静态的还是动态的,如果是动态SQL,就需要根据当前方法参数进行SQL解析,得到最终SQL,最终SQL对象叫做BoundSql。

然后利用BoundSql、方法参数等生成CacheKey,用来判断当前执行的SQL是否能命中一级缓存、二级缓存。

如果不能命中缓存,就需要真正执行SQL了,MyBatis是对JDBC的封装,所以底层就是利用JDBC的机制来执行SQL,只不过MyBatis会封装出一些Handler,比如会:

  1. 利用PreparedStatementHandler来生成PreparedStatement对象
  2. 利用ParameterHandler来给PreparedStatement对象的参数赋值
  3. 利用PreparedStatementHandler来执行PreparedStatement对象,也就是真正执行SQL
  4. 利用ResultSetHandler来处理SQL执行结果,也就是将结果记录按照ResultMap转换成相应的Java对象
  5. 利用TypeHandler来实现JdbcType和JavaType之间的转换

最后利用SqlSession的commit()、rollback()方法来提交或回滚事务,从而完整SQL的执行。

至此,本文介绍了MyBatis中配置文件的解析、SQL的执行等核心对象和机制的底层原理,以上就是本文的全部内容了,本来就是一篇总结性的文章,我就不再总结了,温故而知新,大家可以收藏起来以后多看看,能看到这的同学非常棒了,希望有收获,码字不易,能否给我一个小小的赞呢,谢谢!

我是大都督周瑜,我的公众号是:IT周瑜,文章会优先发布在公众号,欢迎大家关注,谢谢!