Mybatis流程源码

目录

本文用的是3.5.10版本

源码地址:https://github.com/mybatis/mybatis-3/releasesopen in new window

文档地址:https://mybatis.org/mybatis-3/zh/sqlmap-xml.htmlopen in new window

环境的搭建本文不做阐述了,文档里面有,本文适用已经对Mybatis使用有一定了解的人阅读!

一、JDBC与Mybatis对比

我们先看看Mybatis调用Mysql和JDBC调用Mysql有什么区别?这样就知道Mybatis帮我们做了哪些事了

JDBC调用

  1. 加载驱动后,先获取连接(数据源)
  2. 获取操作对象(预编译)
  3. 传参处理
  4. 执行
  5. 返回值处理
Connection connection =null;
    try {
      // 加载驱动
      Class.forName("com.mysql.cj.jdbc.Driver");
      //1.获取连接 数据源
      connection = DriverManager.getConnection(url, user, password);
      //2.获取操作对象
      PreparedStatement preparedStatement = connection.prepareStatement("select * from test_user where id=?");
      //3.传参处理
      preparedStatement.setInt(1,1);
      //4.执行
      ResultSet resultSet = preparedStatement.executeQuery();
      //5.返回值处理
      while (resultSet.next()){
        TestVO testVO = new TestVO(
          resultSet.getInt("id"),
          resultSet.getString("name"),
          resultSet.getString("team"),
          resultSet.getInt("grade")
        );
        System.out.println(JSONObject.toJSON(testVO));
      }
    } catch (Exception e) {
      e.printStackTrace();
    }finally {
      if(connection!=null){
        connection.close();
      }
    }

Mybatis调用

加载驱动就不说了,总的分几步:

  1. 加载配置文件把资源放入工厂
  2. 从工厂获取一个会话(会话就可以理解成是个连接)
  3. 从会话里面获取动态代理的Mapper对象执行对应方法就可以了
    // 获取数据源  获取SQL语句  获取Mapper  传参处理   执行   返回值处理
    try {
      String resource = "resources/mybatis-config.xml";
      // 通过classLoader获取到配置文件
      InputStream inputStream = Resources.getResourceAsStream(resource);
      SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
      // 获取会话工厂
      SqlSessionFactory sqlSessionFactory = builder.build(inputStream);
      // 获取一个会话
      SqlSession sqlSession = sqlSessionFactory.openSession();

      // 动态代理方式
      TestMapper mapper = sqlSession.getMapper(TestMapper.class);
      List<TestVO> dataList = mapper.findDataList(1,"张三");
      System.out.println("通过动态代理返回结果" + JSONObject.toJSON(dataList));
      sqlSession.close();
    } catch (Exception e) {
      e.printStackTrace();
    }

两者对比

首先要达成一个共识,Mybatis最终调用Mysql的方式和JDBC是不是一样的?

是一样的,所以Mybatis只是对JDBC做了一层封装,帮我们简化了很多操作,比如:

  1. SQL传参的自动适配处理(JDBC传参还要传参与SQL参数索引对应)
  2. 返回值处理自动映射处理(JDBC返回值还要自己与实体类映射吧)

最主要就这两个吧,SQL还是需要我们自己写,连接、预编译、执行器这些JDBC都有吧!

所以为什么Mybatis被称为半ORM框架这些懂了吧?就是SQL还需要自己写,传参和返回值处理它帮你搞定了

所以我们总结一下Mybatis做了什么?怎么帮我们做的封装,接下来分步骤一步一步看:

  • 一、资源的获取
    1. 数据源获取
    2. SQL语句获取
    3. Mapper代理对象获取
  • 二、执行
    1. 传参处理
    2. 语句执行
    3. 返回值处理

二、Mybatis资源加载

资源的获取就是XML文件的解析,我们再回顾一下Mybatis的代码,是不是加载了一份资源文件

数据源获取

数据源就是XML配置文件里面environments标签部分,我们需要解析并加载,如下图所示:

SqlSessionFactoryBuilder.build

调用XMLConfigBuilder.parse 解析

XMLConfigBuilder.parse

找到我们需要解析的environments标签,就是数据源的配置解析

XMLConfigBuilder.environmentsElement

还通过了遍历获取,说明数据源可以有多个,然后数据源被放到了Configuration里面,到这数据源就解析完成了

(这个Configuration记住哈,资源的解析完了后都是放到这里面的,很重要)

SQL语句获取

1.入口

XMLConfigBuilder.parse

和上面一样同样在解析XML配置文件里面

2.两种方式

我们看看配置文件里面mappers标签是怎么配置的,官方给出了四种配置方式:

  1. Mapper.xml文件的相对路径引用(解析XML获取SQL)
  2. Mapper.xml文件的绝对路径引用(解析XML获取SQL)
  3. Mapper.java类文件的完全限定类名(解析注解获取SQL)
  4. Mapper.java类文件所在的包路径(解析注解获取SQL)

既然配置有4种配置方法,那对应的解析的时候也有4种解析方法:

XMLConfigBuilder.mapperElement

所以我们要知道SQL语句有两个地方可以获取(应该都知道霍,SQL本身就可以写两个地方)

  1. 加载XML资源文件获取
  2. 加载Mapper接口文件通过注解的形式获取

所以接下来我们要分两种方式来解析!

3.XML方式获取SQL

从入口处看,XML方式都会调用

3.1 XMLMapperBuilder.parse()

3.2 XMLMapperBuilder.configurationElement

有很多标签就不一一看了,直接看增删改查的标签

3.4 XMLMapperBuilder.buildStatementFromContext

遍历XML节点一个一个获取

3.5 XMLStatementBuilder.parseStatementNode

这个方法有点长我们分三个截图,分别代表三个关键的点,一个一个来

第一段: 这个是干嘛的,我们后面说(判断重复加载)

第二段:这就是获取SQL资源的地方,我们一样后面说,因为通过注解的方式解析的时候也会调用这个方法,后面一起说,先记着是LanguageDriver.createSqlSource方法

第三段:这个可以是最后的一步了,我们的数据源被加载放到了Configuration里面,所以SQL相关的也不例外要放入Configuration

3.6 MapperBuilderAssistant.addMappedStatement

最终被封装成了MappedStatement放入了Configuration

4.注解的方式获取SQL

我们一样从入口开始看

4.1 MapperRegistry.addMapper

两个入口一直往下都会到这个方法,中间的就不看了,没啥好说的

4.2 MapperAnnotationBuilder.parse

中间要做什么处理,搞什么鬼的一律不理,我们直奔主题

4.3 MapperAnnotationBuilder.parseStatement

这个方法有点长我们同样分几段

第一段:第一步获取SQL资源,这个buildSource一样会到我们上面说的LanguageDriver.createSqlSource方法,后面一起说

第二段:是不是有点熟悉又是MapperBuilderAssistant.addMappedStatement方法,和上面一样的最后被封装成MappedStatement放入了Configuration中(和上面一样就不说了)

5.最终的SQL处理方法

我们解析XML的时候或者解析注解的时候难道拿不到SQL吗?还需要单独去解析获取SQL?

这里的SQL资源获取并不代表是获取XML或者注解里面的SQL,恰恰是对里面的SQL处理一次,我们最终需要的SQL是像JDBC里面那样参数是 "?"的SQL格式,所以这里的处理是将XML或者注解里面SQL中的占位符处理掉,并建立占位符下标索引和传参参数的映射

如下图所示:

处理后SQL已经变成了最后执行所需的样子,同时产生了占位符的下标索引和传参的映射关系的ParameterMapping对象

所以需要处理两种情况:

  • 带${}占位符:处理方式是直接用传参直接替换
  • 带#{}占位符:处理方式是需要将占位符变成 "?",然后再用传参顺序替换 "?"

上面我们说到createSqlSource方法,实际是在XMLLanguageDriver中,存在两个重载方法

XMLLanguageDriver.createSqlSource

不管是注解形式的SQL还是XML形式的SQL最终都会到这,两者解析完最终都会判断SQL是静态SQL还是动态SQL,有不同的处理方法

  • 静态SQL:SQL中不存在${}占位符,采用RawSqlSource类处理
  • 动态SQL:SQL中存在${}占位符,采用DynamicSqlSource类处理

动/静SQL判断

TextSqlNode.isDynamic()

该方法中会调用GenericTokenParser类进行对SQL中"${}"占位符的处理,而处理方法正是DynamicCheckerTokenParser.handleToken方法,该方法中将标志位设置为了True,而这种动态的SQL会在最终执行的时候才去处理(用传参替换)

动态SQL一共有两种,一是${}占位符,一是XML中存在IF标签等判断语句(大家可以从XML入口去看)

静态SQL处理

RawSqlSource

静态的SQL会在该类中的构造方法中直接处理了

SqlSourceBuilder.parse()

相比于动态SQL,处理的Handler换了,看替换的本文变成了"?",同时还加上了下标的映射关系

GenericTokenParser这个是占位符的处理类,就等于是一个工具类,这里就不贴了,有兴趣的可以去看看,实际开发说不定也能用得上哦

6.如果两种配置方式我都配了会怎样?

先走Xml解析,后走注解的方式

结果会报错,因为注解的方式后解析,而在注解的方式解析过程中并没有判断MappeStatement是否已经存在,此时会继续往Configuration中添加MappeStatement,而Configuration里面用的Map是自定义的StrictMap,put相同的key会报错

先走注解的方式,后走Xml的解析

无事发生,因为在xml解析过程中有判断MappeStatement是否已经存在

(这就是我上面说截图第一段后面说的,至此坑已经填完)

Mapper代理对象获取

应该都知道Mapper的执行原理是动态代理,所以Mapper也需要加载变成代理对象,结合SQL的获取方式,Mapper加载也有两种

  • XML方式加载:通过XML里面配置的命名空间获取Mapper对象
  • 注解方式加载:配置的是包名或者全类名所以直接直接获取Mapper对象

命名空间加载入口

就在XML解析的下面

XMLMapperBuilder.bindMapperForNamespace()

会调用addMapper()这个方法

包/类名加载入口

就在加载的时候,也会调用addMapper()方法,我们直接看这个方法

MapperRegistry.addMapper()

先判断是否存在,不存在就直接加入到了knownMappers这个Map对象中,加入的是一个代理工厂

MapperProxyFactory,然后就没了,等着执行时调用

总结

  1. 在配置文件加载过程中,加载了数据源、SQL资源、Mapper代理工厂
  2. SQL分为动态SQL和静态SQL,静态SQL是在加载过程中就处理好了,动态SQL需要在执行时获取参数才能处理(带${}占位符或者IF等判断标签)
  3. Mapper会生成其代理工厂保存在一个Map对象中
  4. MappedStatement保存着所有SQL相关资源(SQL语句、参数映射关系等)
  5. 除了Mapper代理对象,其他资源都会放入Configuration

三、Mybatis语句执行

应该都知道,前面也说过获取的Mapper是一个代理对象,最后的执行是代理对象的执行方法,所以我们直接点进去看看代理对象的执行方法是什么,先看看代理对象如何生成的

1.代理对象的生成

MapperRegistry.getMapper

我们随着getMapper方法一路进来会找到该方法,一眼过去非常的眼熟是不是,先从Mapper集合取出代理工厂,然后用代理工厂去生成代理对象,这里的SqlSession是什么?就是DefaultSqlSession,里面就包含之前装载资源的Configuration

采用的就是JDK的动态代理,所以这个MapperProxy很明显里面就有最终代理对象的处理方法了

2.代理对象的执行方法

MapperProxy.invoke

很容易就找到了方法执行处,忽略掉一系列判断,我们直接找到最终的处理方法

又采用了一个静态代理的方式调用了执行

3.方法的执行

MapperMethod.execute

到了这里是不是就顺眼多了,熟悉的增删改查,我们以查为例,查还分无返回值、查多条、查Map、查一条这些情况,我们就简单一点以差一条为例子走

4.查询一条为例

DefaultSqlSession.selectOne

进来这里你看看这个类下的方法,虽然是以查询一条为例,但是增删改查操作最终都会到这,而且你看看查询一条的时候实际也是查询的是list,最后只是返回了一条结果而已,下面那个报错异常相信大家也很熟悉吧

DefaultSqlSession.selectList

顺着上面下来,这里会先获取一个MappedStatement,这个是什么东西?还记得上面说的SQL资源获取吗?这里面就是SQL相关的所有资源,比如SQL语句、返回值、传参映射等等,所以这里的获取指的就是去获取现在要执行的方法对应的SQL资源(MappedStatement),通过这个方法的全路径类名+方法名称获取,然后再调用执行器执行查询!

5.执行器执行

执行这块涉及到两个知识点,一是Mybatis的二级缓存,二是Mybatis的三大执行器

这里我们先跳过这两个点,直接来看默认的执行器(SimpleExecutor)执行过程

SimpleExecutor.doQuery

这里分三步

  1. 获取具体的操作类,这里的操作类就类似于JDBC中的PreparedStatement
  2. 获取最后要执行的SQL(处理传参)、获取连接并设置相关参数(超时时间等)
  3. 执行并处理返回结果

5.1 获取StatementHandler

Configuration.newStatementHandler

这里主要就是选择采用哪个StatementHandler

RoutingStatementHandler

StatementHandler的选择是根据参数来的,这个参数在XML里面是可以配置的,默认是PreparedStatementHandler

5.2 获取连接并获取SQL

SimpleExecutor.prepareStatement

  1. 先获取连接
  2. 预处理一下Statement(设置超时时间、获取SQL)
  3. SQL传参处理

5.2.1 预处理Statement

BaseStatementHandler.prepare

获取连接就不看了,没啥看的,直接看看预处理,主要关注这个初始化

PreparedStatementHandler.instantiateStatement

看这是不是跟JDBC里面第二步一模一样?现在就只差传参处理、执行、返回值处理了是不是?

5.2.2 SQL传参处理

PreparedStatementHandler.parameterize

DefaultParameterHandler.setParameters

这个就是处理传参的,根据之前处理后的映射关系,找到对应类型的处理类处理参数,然后与JDBC类型映射,这整个过程都是自动完成的,所以这里的参数处理还需要所有类型的对应处理方式是不是,一旦处理方式没匹配上就会有问题对吧

TypeHandler

所以Mybatis里面列举了很多类型的处理方式,而且看方法,不仅处理参数,还处理返回值

5.3 执行并处理返回结果

PreparedStatementHandler.query

DefaultResultSetHandler.handleResultSets

获取了JAVA中与JDBC参数类型的映射关系、获取XML中的配置、然后遍历处理结果集

ResultSetWrapper

JAVA中与JDBC参数类型的映射关系如下

6.总结

以查询一条、SimpleExecutor执行器为例:

  1. 首先会获取Mapper代理对象
  2. 然后判断要执行的是增、删、改、查其中的哪种
  3. 然后选择执行器执行(默认SimpleExecutor
  4. 然后选择操作处理类StatementHandler
  5. 获取连接、获取SQL
  6. 传参处理,然后执行,然后返回值处理

这里网上找了图,结构上更明了:

四、总结

核心部件

从MyBatis代码实现的角度来看,MyBatis的主要的核心部件有以下几个:

  • Configuration:初始化基础配置,比如MyBatis的别名等,一些重要的类型对象,如插件,映射器,ObjectFactory和typeHandler对象,MyBatis所有的配置信息都维持在Configuration对象之中。 SqlSessionFactory:SqlSession工厂。
  • SqlSession:作为MyBatis工作的主要顶层API,表示和数据库交互的会话,完成必要的数据库增删改查功能。
  • Executor: MyBatis的内部执行器,它负责调用StatementHandler操作数据库,并把结果集通过ResultSetHandler进行自动映射,另外,它还处理二级缓存的操作。
  • StatementHandler: MyBatis直接在数据库执行SQL脚本的对象。
  • ParameterHandler: 负责将用户传递的参数转换成JDBC Statement所需要的参数。是MyBatis实现SQL入参设置的对象。
  • ResultSetHandler: 负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。是MyBatis把ResultSet集合映射成POJO的接口对象。
  • TypeHandler:负责Java数据类型和JDBC数据类型之间的映射和转换。
  • MappedStatement: MappedStatement维护了SQL相关资源
  • SqlSource:负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。
  • BoundSql:表示动态生成的SQL语句以及相应的参数信息SqlSource :负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回。

遗漏的知识点

这三个可能有些人平时用的少,但都算是Mybatis提供的一些功能,后续单独拿出来说

  1. Mybatis二级缓存
  2. 三大执行器
  3. 扩展功能之Mybatis拦截器
Last Updated: