Mybatis关键源码深度分析学习

一、Mybatis源码分析之SelectOne和自定义方法区别

学习一下自定义查询方法与Mybatis提供的方法有什么不同

先执行自定义的方法(学习流程的时候学习过,熟悉)

和以前一样这里不做过多叙述,就是通过jdk动态代理返回一个ProductMapper的接口对象。

下面去执行Product product = productMapper.selectById(168903089414213632L);这段代码,看看和上次查询一个list有什么不同,我们进入这个方法org.apache.ibatis.binding.MapperMethod.MethodSignature#MethodSignature

我们看到这个方法上次做查询的list这里有个returnsMany的判断如果是个集合或者isArray则返回true,但是我们是selectOne并且我们的returnType是个对象所以是返回false(这是不一样的地方)

继续debug 进入org.apache.ibatis.binding.MapperMethod#execute

根据判断都是false所以走else去调用selectOne(这里是不是和我们标题联系上了)其实他最终还是原理还是相同的。

进入org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)

selectOne其实去调用selectList方法,但是他有个数量校验,这个错我想大家都见过“selectOne but found n(n>1)”

继续往下走进入:org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds)

这里就是上篇查询list<T>流程就一样了,查询完成后拿到list,进行判断

 

符合条件返回。

下面再看原生的selectOne方法的执行流程

进入selectOnne方法

发现和自定义最后相同还是要去走这个selectOne方法并且通过selectList去查询数据然后进入数量的判断。

但是这里有个不同的,他是怎么把参数与sql语句绑定的,进入:org.apache.ibatis.executor.BaseExecutor#query(org.apache.ibatis.mapping.MappedStatement, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)

进入org.apache.ibatis.mapping.BoundSql#BoundSql的构造方法

将参数赋值给BoundSql的参数对象parameterObject,这样就绑定成功了。下面的执行流程就一样了。

总结

自定义方法: jdk动态代理返回一个ProductMapper的接口对象,就是MapoerProxy的一个实例化对象。通过invoke方法去判断

最后在返回类型去调用selectOne方法。

原生的selectone就少了一个jdk的动态代理,直接调用selectOne方法(最终还是selectList)

================分割线==================

二、Annotation @Select、@Update、@Insert、@Delete

分析Mybatis通过注解的方式去操作数据库

案例:

看看加了注解后他是怎么去构建的,我们把断点打到解析配置的方法中:org.apache.ibatis.builder.xml.XMLConfigBuilder#parseConfiguration

看代码也能大致猜到他的过程:

1、解析mapper文件

2、mapper文件中有dao层接口路径

3、通过路径解析查询方法与注解(先去构建mapper文件的sql语句,再去寻找注解方式的sql语句)

进入mapperElement方法:

org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement

查看parse方法:org.apache.ibatis.builder.xml.XMLMapperBuilder#parse

进入org.apache.ibatis.builder.xml.XMLMapperBuilder#bindMapperForNamespace方法绑定映射器

进入addMapper方法(org.apache.ibatis.session.Configuration#addMapper)

重点来了:

注解生成器,进入:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder

和我们的注解@select建立联系了,下面继续进入:org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parse方法

进入org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#parseStatement方法

通过调用这个org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#getSqlSourceFromAnnotations方法来获取sqlSource对象继续往下走

通过这个org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#buildSqlSourceFromStrings方法获取sql语句并返回

我们看一下返回的sqlSource参数

这就拿到了sql执行语句,后面的创建回话,查询数据和以前源码分析相同

总结:注解执行流程

>org.apache.ibatis.session.SqlSessionFactoryBuilder.build(java.io.InputStream) 
    >org.apache.ibatis.builder.xml.XMLConfigBuilder
       >org.apache.ibatis.builder.xml.XMLConfigBuilder.mapperElement
           >org.apache.ibatis.session.Configuration.addMapper
             >org.apache.ibatis.binding.MapperRegistry.addMapper
               >org.apache.ibatis.binding.MapperRegistry.addMapper
                     
               >org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.parseStatement
                   >org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.getSqlSourceFromAnnotations
                     >org.apache.ibatis.builder.annotation.MapperAnnotationBuilder.buildSqlSourceFromStrings
                       >org.apache.ibatis.builder.SqlSourceBuilder.parse

但是我们这里有个疑问,#{id}是怎么替换成?

我们这里去深入研究下;

通过这个方法(org.apache.ibatis.builder.annotation.MapperAnnotationBuilder#buildSqlSourceFromStrings)获取的sql语句,我们看看他是怎么获取的

进入:org.apache.ibatis.scripting.LanguageDriver#createSqlSource(org.apache.ibatis.session.Configuration, java.lang.String, java.lang.Class<?>)

进入RawSqlSource 

拿到参数类型,然后去获取sql源,进入:org.apache.ibatis.builder.SqlSourceBuilder#parse

看着这里,这个parser对象获得:

猜想他是应该要将#{}替换成? 做准备。我们继续往下走看这个方法org.apache.ibatis.parsing.GenericTokenParser#parse

这段就是核心代码。

代码太多不好截图,看不到debug流程,不过我加了自己理解注释

{
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    //判断该sql语句到指定字符(#{)有多少长度
    int start = text.indexOf(openToken, 0);
    if (start == -1) {
      return text;
    }
    //将sql语句转化成char[]数组
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        //将sql语句从起始位置到#{位置(不包含#{)append到builder中去
        builder.append(src, offset, start - offset);
        //此时将偏移量从0移到一段sql语句(select * from...... id=#{id})中的#{ 的{ 的位置
        offset = start + openToken.length();
        //最后一个{在sql字符串中的偏移量是多少
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue.
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            //将....#{id} 中的参数提取出来
            expression.append(src, offset, end - offset);
            //将偏移量打到末尾
            offset = end + closeToken.length();
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //将?放入新的sql语句中去
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

通过以上流程我们终于将"#{}"替换成“?”.后面的查询流程就和上一篇的执行流程一样了,不在过多叙述。

至此@Select注解源码分析结束,其实update,delect都差不多。我们可以从加载类MapperAnnotationBuilder看出来。

 

===================分割线=========================

Mybatis源码分析之执行器原理

每一个SqlSession都会拥有一个Executor对象,这个对象负责增删改查的具体操作,我们可以简单的将它理解为JDBC中Statement的封装版。

我们进入org.apache.ibatis.executor.Executor接口:

如图所示,位于继承体系最顶层的是Executor执行器,它有两个实现类,分别是BaseExecutor和 CachingExecutor

  BaseExecutor 是一个抽象类,这种通过抽象的实现接口的方式是适配器设计模式之接口适配的体现,是Executor的默认实现,实现了大部分Executor接口定义的功能,降低了接口实现的难度。BaseExecutor的子类有三个,分别是SimpleExecutorReuseExecutorBatchExecutor

     SimpleExecutor: 简单执行器,是MyBatis中默认使用的执行器,每执行一次update或select,就开启一个Statement对象,用完就直接关闭Statement对象(可以是Statement或者是PreparedStatment对象)

     ReuseExecutor: 可重用执行器,这里的重用指的是重复使用Statement,它会在内部使用一个Map把创建的Statement都缓存起来,每次执行SQL命令的时候,都会去判断是否存在基于该SQL的Statement对象,如果存在Statement对象并且对应的connection还没有关闭的情况下就继续使用之前的Statement对象,并将其缓存起来。因为每一个SqlSession都有一个新的Executor对象,所以我们缓存在ReuseExecutor上的Statement作用域是同一个SqlSession。

    BatchExecutor: 批处理执行器,用于将多个SQL一次性输出到数据库。update(没有select,JDBC批处理不支持select),将所有sql都添加到批处理中(addBatch()),等待统一执行(executeBatch()),它缓存了多个Statement对象,每个Statement对象都是addBatch()完毕后,等待逐一执行executeBatch()批处理的;BatchExecutor相当于维护了多个桶,每个桶里都装了很多属于自己的SQL,就像苹果蓝里装了很多苹果,番茄蓝里装了很多番茄,最后,再统一倒进仓库。(可以是Statement或PrepareStatement对象)

 CachingExecutor: 缓存执行器,先从缓存中查询结果,如果存在,就返回;如果不存在,再委托给Executor delegate 去数据库中取,delegate可以是上面任何一个执行器,前面也有说到,先从缓存中去查询,如果查询不到那就从数据库查询。

  Executor创建过程以及源码分析:其实在mybatis的执行流程中就提到了,创建会话时:

SqlSession session = sessionFactory.openSession();

回去执行一个newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)方法

 

通常默认是SImpleExcutor执行器,如果想要更改的话有两种方式:1、在mybatis-config.xml的配置文件中选择好

2、另一种就是创建会话时进行设定

我们看看第二种代码是如何执行(其实也是大同小异,只不过带了一个执行器类型的参数)

这是我们看到执行器的类型是BATCH,继续往下走,看看他如何获取BatchExcutor执行器的,进入org.apache.ibatis.session.Configuration#newExecutor(org.apache.ibatis.transaction.Transaction, org.apache.ibatis.session.ExecutorType)

我们发现不管是Simple执行器还是Batch执行器实例化都是走BaseExecutor方法

我们发现一个当他在执行doquery时,他会去调用Batch执行器的方法

我们看看这个BatchExecutor的方法;

public class BatchExecutor extends BaseExecutor {

  public static final int BATCH_UPDATE_RETURN_VALUE = Integer.MIN_VALUE + 1002;

  /* Statement链表**/
  private final List<Statement> statementList = new ArrayList<Statement>();

  /* batch结果链表**/
  private final List<BatchResult> batchResultList = new ArrayList<BatchResult>();
  private String currentSql;
  private MappedStatement currentStatement;

  public BatchExecutor(Configuration configuration, Transaction transaction) {
    super(configuration, transaction);
  }

  @Override
  public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    //获得配置信息
    final Configuration configuration = ms.getConfiguration();
    //获得StatementHandler
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    //获得Sql语句
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    //如果sql语句等于当前sql MappedStatement 等于当前Map碰到Statement
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
      int last = statementList.size() - 1;
      //获得最后一个
      stmt = statementList.get(last);
      applyTransactionTimeout(stmt);
      handler.parameterize(stmt);//fix Issues 322
      //有相同的MappedStatement和参数
      BatchResult batchResult = batchResultList.get(last);
      batchResult.addParameterObject(parameterObject);
    } else {
      //如果不存在就创建一个批处理操作
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      //添加批量处理操作
      handler.parameterize(stmt);    //fix Issues 322
      currentSql = sql;
      currentStatement = ms;
      statementList.add(stmt);
      batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
  // handler.parameterize(stmt);
    //最终是调用jdbc的批处理操作
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
  }

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql)
      throws SQLException {
    Statement stmt = null;
    try {
      flushStatements();
      //获得配置信息
      Configuration configuration = ms.getConfiguration();
      //获得StatementHandler
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameterObject, rowBounds, resultHandler, boundSql);
      Connection connection = getConnection(ms.getStatementLog());
      stmt = handler.prepare(connection, transaction.getTimeout());
      //获得Statement
      handler.parameterize(stmt);
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

  @Override
  protected <E> Cursor<E> doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) throws SQLException {
    flushStatements();
    Configuration configuration = ms.getConfiguration();
    StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, null, boundSql);
    Connection connection = getConnection(ms.getStatementLog());
    Statement stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return handler.<E>queryCursor(stmt);
  }

  /* 刷新Statement,记录执行次数*/
  @Override
  public List<BatchResult> doFlushStatements(boolean isRollback) throws SQLException {
    try {
      List<BatchResult> results = new ArrayList<BatchResult>();
      if (isRollback) {
        return Collections.emptyList();
      }
      for (int i = 0, n = statementList.size(); i < n; i++) {
        Statement stmt = statementList.get(i);
        applyTransactionTimeout(stmt);
        BatchResult batchResult = batchResultList.get(i);
        try {
          batchResult.setUpdateCounts(stmt.executeBatch());
          MappedStatement ms = batchResult.getMappedStatement();
          List<Object> parameterObjects = batchResult.getParameterObjects();
          KeyGenerator keyGenerator = ms.getKeyGenerator();
          if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) {
            Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator;
            jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects);
          } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141
            for (Object parameter : parameterObjects) {
              keyGenerator.processAfter(this, ms, stmt, parameter);
            }
          }
          // Close statement to close cursor #1109
          closeStatement(stmt);
        } catch (BatchUpdateException e) {
          StringBuilder message = new StringBuilder();
          message.append(batchResult.getMappedStatement().getId())
              .append(" (batch index #")
              .append(i + 1)
              .append(")")
              .append(" failed.");
          if (i > 0) {
            message.append(" ")
                .append(i)
                .append(" prior sub executor(s) completed successfully, but will be rolled back.");
          }
          throw new BatchExecutorException(message.toString(), e, results, batchResult);
        }
        //记录操作
        results.add(batchResult);
      }
      return results;
    } finally {
      for (Statement stmt : statementList) {
        closeStatement(stmt);
      }
      currentSql = null;
      statementList.clear();
      batchResultList.clear();
    }
  }

}

正如概念所说,BatchExecutor执行器就是在update时伪执行,他是等到doQuery方法一起执行。

我们可以看一下对比:

执行mybatis的update语句:

发现BatchExecutor的update方法执行成功后不会返回1,这个需要这注意。在看看数据库有没有变化。

这也验证了他的概念当你使用BatchExecutor方法,一旦执行update类方法,如果没有执行查询方法,那么你执行的sql是不是同步到数据库的。我们再次执行update方法后并执行select方法。看看结果。

更改完成。继续学习,坚持。

=========================结束========================

下一篇:Mybatis的插件与缓存源码解析

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值