MyBatis 源码分析篇 6:Mapper 方法执行的“前因”

在前面的探索中,我们已经知道了 MyBatis 是如何 getMapper 并执行 Mapper 接口中的方法来进行数据库操作的。那么今天我们就来看看 Mapper 方法执行的“前因”:获取语句+参数映射。

我们还是按照之前的方式,使用 debug 在入口代码上打断点,步入源码。入口代码为:

List<Author> list = mapper.selectByName("Sylvia");

对应的 SQL:

    <select id="selectByName" resultMap="AuthorMap" >
        select
          id, name, sex, phone
        from author
        where name = #{name}
    </select>

1 从 Mapper XML 读取 SQL

首先,我们要思考一下,因为 SQL 语句是定义在 Mapper XML 中的,那么毫无疑问它会去读取该 Mapper XML 中的内容。可是它会在什么时候读取呢?答案是在获取 SqlSessionFactory 的时候,即执行:

SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);  

为了验证,我们可以在这行打个断点来一探究竟。

我们一直往下走,直到 org.apache.ibatis.builder.xml.XMLConfigBuilder 的 parseConfiguration 方法,我们在之前已经见过这个方法了,它的作用是解析 mybatis-config.xml 文件中的配置,将相应的元素内容转换到 Configuration 等类中:

  private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

我们要看的是 Mapper XML 中 SQL 是不是在这个地方读取的,理所应当进入最后一行解析代码:mapperElement(root.evalNode("mappers"));。这个时候我们会进入到 org.apache.ibatis.builder.xml.XMLConfigBuilder 的 mapperElement 方法:

  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {
          String resource = child.getStringAttribute("resource");
          String url = child.getStringAttribute("url");
          String mapperClass = child.getStringAttribute("class");
          if (resource != null && url == null && mapperClass == null) {
            ErrorContext.instance().resource(resource);
            InputStream inputStream = Resources.getResourceAsStream(resource);
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse();
          }//略...
        }
      }
    }
  }

在这个方法中我们可以看到它在解析 mybatis-config.xml 中的 mappers 元素内容。注意最后一行代码:mapperParser.parse(); 看名字它似乎就是想要去解析 Mapper XML 的内容,进入 org.apache.ibatis.builder.xml.XMLMapperBuilder 的 parse() 方法:

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

继续进入第 3 行代码,进入到 configurationElement() 方法:

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.equals("")) {
        throw new BuilderException("Mapper's namespace cannot be empty");
      }
      builderAssistant.setCurrentNamespace(namespace);
      cacheRefElement(context.evalNode("cache-ref"));
      cacheElement(context.evalNode("cache"));
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      sqlElement(context.evalNodes("/mapper/sql"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }

果然,这个方法就是用来解析 Mapper XML 的,我们可以看到平时在 Mapper XML 中常用的 resultMap、sql 等。很明显倒数第 5 行就是要读取 SQL 语句的,所以我们直接进入第 5 行:buildStatementFromContext(context.evalNodes("select|insert|update|delete"));,进入到 org.apache.ibatis.builder.xml.XMLStatementBuilder 的 parseStatementNode 方法()。这时,我们就能看到它对每个元素进行了解析(由于篇幅过长,我们省略掉部分源代码,感兴趣的同学可以动手查看):

  public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    //太长省略啦...
    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    //太长省略啦...
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

我们先进入第 6 行,直到 org.apache.ibatis.scripting.xmltags.XMLScriptBuilder 的 parseScriptNode() 方法:

  public SqlSource parseScriptNode() {
    MixedSqlNode rootSqlNode = parseDynamicTags(context);
    SqlSource sqlSource = null;
    if (isDynamic) {
      sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
    } else {
      sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
    }
    return sqlSource;
  }

通过第 7 行代码,我们会发现它将 sql 语句封装在了 sqlSource 中(感兴趣的话可以跟进去看看)。此时的 sqlSource 是这个样子哒:

包含 SQL 语句的 sqlSource

好了这个时候我们已经拿到包含了 sql 语句的 sqlSource 了,那么我们来继续看一下它要拿这个 sqlSource 来做什么。回到 parseStatementNode() 方法,继续进入 builderAssistant.addMappedStatement(...) 代码:

public MappedStatement addMappedStatement(
      String id,
      SqlSource sqlSource,
      //参数太长省略啦...) {
    //...
    MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
        .resource(resource)
        .fetchSize(fetchSize)
        .timeout(timeout)
        .statementType(statementType)
        .keyGenerator(keyGenerator)
        .keyProperty(keyProperty)
        .keyColumn(keyColumn)
        .databaseId(databaseId)
        .lang(lang)
        .resultOrdered(resultOrdered)
        .resultSets(resultSets)
        .resultMaps(getStatementResultMaps(resultMap, resultType, id))
        .resultSetType(resultSetType)
        .flushCacheRequired(valueOrDefault(flushCache, !isSelect))
        .useCache(valueOrDefault(useCache, isSelect))
        .cache(currentCache);
    //...
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
  }

首先我们要进入第 6 行看一下 org.apache.ibatis.mapping.MappedStatement 类的 Builder 构造方法,我们可以看出此时 sqlSource 赋值给了 mappedStatement 这个成员变量的 sqlSource,也就是说此时该 Builder 就持有了包含 SQL 的 sqlSource:

  public static class Builder {
    private MappedStatement mappedStatement = new MappedStatement();

    public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
      mappedStatement.configuration = configuration;
      mappedStatement.id = id;
      mappedStatement.sqlSource = sqlSource;
      //略...
    }

那么接着我们从这个构造方法出来继续往下看,从倒数第 3 行代码
configuration.addMappedStatement(statement); 就可以看出,包含 SQL 的 statement 最终存进了 configuration。Configuration 中有个 Map 类型的成员变量 mappedStatements,如下:

protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>("Mapped Statements collection");

那么点进去 configuration.addMappedStatement(statement);,不出意外,它一定是在给 mappedStatements 变量 put 内容,而 key 就是语句的 id:

  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);
  }

Bingo!

好了,现在就让我们先牢记这个结论:Mapper XML 中的 SQL 语句(们)在构建 SqlSessionFactory 的时候存入了 Configuration 实例的 mappedStatements (Map 类型)中。

2 参数映射

现在就让我们用文章开头的入口代码来 debug 进入,看看 MyBatis 在执行 SQL 之前是如何处理语句和参数的。其实上一节我们已经跟踪过 selectList 方法了,只是上次我们的关注点在数据库方法执行上,现在我们就把关注点放在 SQL 处理和参数映射上。

我们跳过前面的代码直到 org.apache.ibatis.executor.SimpleExecutor 的 doQuery(...) 方法:

  @Override
  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.<E>query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

进入第 7 行代码,走到 org.apache.ibatis.executor.SimpleExecutor 的 prepareStatement(...) 方法:

  private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
    Statement stmt;
    Connection connection = getConnection(statementLog);
    stmt = handler.prepare(connection, transaction.getTimeout());
    handler.parameterize(stmt);
    return stmt;
  }

该方法即为处理 SQL 和参数的核心方法。其中,第 3 行代码为获取数据库连接,第 4 行代码为获取 PreparedStatement,第 5 行代码为参数映射。我们依次来看一下这三行代码具体的实现。

2.1 获取连接 Connection connection = getConnection(statementLog);

  protected Connection getConnection(Log statementLog) throws SQLException {
    Connection connection = transaction.getConnection();
    if (statementLog.isDebugEnabled()) {
      return ConnectionLogger.newInstance(connection, statementLog, queryStack);
    } else {
      return connection;
    }
  }

进入方法,我们看到它先是通过 Transaction 获取了一个连接,然后判断日志级别是不是 debug ,如果是,就会执行 ConnectionLogger.newInstance(...) 方法,不是则直接返回 connection。其中,MyBatis 的 Transaction 接口主要负责获取和关闭连接、提交和回滚事务,它有两个实现类:

Transaction 的实现类
  • JdbcTransaction 直接使用了 JDBC 的提交和回滚设置。
  • ManagedTransaction 几乎什么也不做,翻开源码你会看到提交和回滚的方法里只有一行注释代码 “Does nothing”,它让容器来管理事务的全生命周期(例如 JEE 应用服务器的上下文)。
  • 当然我们也可以自定义自己的实现类,有兴趣的同学可以自己玩儿一下(可参考文末附的项目实践)。

另外,关于 ConnectionLogger.newInstance(...) 方法,根据 statementLog.isDebugEnabled() 的判断条件我们很容易能想到它是要处理连接时的日志输出的。基于查看语句处理的目的,我们还是要跟进去看一下是否进行了其他操作:

  public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
    InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
    ClassLoader cl = Connection.class.getClassLoader();
    return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
  }

还是熟悉的代码,还是熟悉的配方:动态代理!通过 ConnectionLogger 的代理动态生成 Connection 类。

2.2 获取 PreparedStatement 对象 :stmt = handler.prepare(connection, transaction.getTimeout());

这行代码用来获取执行语句的 PreparedStatement,我们跟入代码直到 org.apache.ibatis.executor.statement.PreparedStatementHandler 的 instantiateStatement(...) 方法:

  @Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    String sql = boundSql.getSql();
     //省略啦...
    } else {
      return connection.prepareStatement(sql);
    }
  }

我们看到在这里调用了 Connection 对象的 prepareStatement(...) 方法,从(1)的分析中我们知道,如果我们开启了 debug 日志级别,那么此时的 Connection 为动态代理生成的类,调用其方法一定会进入代理类的 invoke(...)。那么现在,就让我们跟进代理类 ConnectionLogger 来一探究竟吧:

  @Override
  public Object invoke(Object proxy, Method method, Object[] params)
      throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, params);
      }    
      if ("prepareStatement".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("prepareCall".equals(method.getName())) {
        if (isDebugEnabled()) {
          debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
        }        
        PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params);
        stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else if ("createStatement".equals(method.getName())) {
        Statement stmt = (Statement) method.invoke(connection, params);
        stmt = StatementLogger.newInstance(stmt, statementLog, queryStack);
        return stmt;
      } else {
        return method.invoke(connection, params);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
  }

我们来解读一下这个方法:首先它会在调用 connection 方法的时候根据方法名进行判断,我们以第 8 行处的
if 为例。如果是 connection.prepareStatement(...),则首先会打印相应的连接日志。然后,重点来了,它会获取一个 PreparedStatement,之后再调用 PreparedStatementLogger.newInstance(...) 方法并覆盖前一行获取的 PreparedStatement 对象。 PreparedStatementLogger 和 ConnectionLogger 的实现非常像:

  public static PreparedStatement newInstance(PreparedStatement stmt, Log statementLog, int queryStack) {
    InvocationHandler handler = new PreparedStatementLogger(stmt, statementLog, queryStack);
    ClassLoader cl = PreparedStatement.class.getClassLoader();
    return (PreparedStatement) Proxy.newProxyInstance(cl, new Class[]{PreparedStatement.class, CallableStatement.class}, handler);
  }

和获取连接时打印日志和动态生成 PreparedStatement 一样,这里也通过动态代理的方式打印日志并动态生成下一步要用到的结果集 ResultSet 。这里不再深入探讨了,等到我们看结果映射的时候再来讨论。大家可以看出来,MyBatis 作为一个优秀的 ORM 框架,有很多值得我们学习的地方,光是动态代理的使用就很有意思了。

好了,我们已经拿到 PreparedStatement 了,下一步就是要处理参数了。

2.3 参数映射:handler.parameterize(stmt);

我们在文档篇就知道了 MyBatis 通过 TypeHandler 来进行参数和结果映射,这里既然要分析参数映射,那么我们猜测它应该会通过 TypeHandler 去实现。不急,我们来慢慢验证一下:

handler 是 RoutingStatementHandler 类型的,但是它通过装饰器模式委托给了 PreparedStatementHandler 来执行(它们都是 StatementHandler 接口的实现类),并将上面我们得到的 PreparedStatement 作为参数传递了进去。而跟入到 PreparedStatementHandler 我们又发现它通过 parameterHandler (DefaultParameterHandler 类型)来执行,我们依次跟入会看到:

org.apache.ibatis.executor.statement.RoutingStatementHandler 类:

  @Override
  public void parameterize(Statement statement) throws SQLException {
    delegate.parameterize(statement);
  }

org.apache.ibatis.executor.statement.PreparedStatementHandler 类:

  @Override
  public void parameterize(Statement statement) throws SQLException {
    parameterHandler.setParameters((PreparedStatement) statement);
  }

org.apache.ibatis.scripting.defaults.DefaultParameterHandler 类:

  @Override
  public void setParameters(PreparedStatement ps) {
    //...
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          //这里是对 value 的赋值,略...

          TypeHandler typeHandler = parameterMapping.getTypeHandler();
          JdbcType jdbcType = parameterMapping.getJdbcType();
          if (value == null && jdbcType == null) {
            jdbcType = configuration.getJdbcTypeForNull();
          }
          try {
            typeHandler.setParameter(ps, i + 1, value, jdbcType);//主要是这行!!!
          } //...
        }
      }
    }
  }

注意 try 中的代码:typeHandler!这里就验证了我们开始的猜测。它将参数值 value,参数索引位置(i+1)和 jdbcType 作为参数传递给 typeHandler(该测试代码中为 ObjectTypeHandler 类型) 的 setParameter 方法。ObjectTypeHandler 继承自 BaseTypeHandler 类,同时继承了其 setParameter 方法,于是下一步会进入 BaseTypeHandler 类:

  @Override
  public void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException {
    if (parameter == null) {
      //...
    } else {
      try {
        setNonNullParameter(ps, i, parameter, jdbcType);
      } catch (Exception e) {
        //...
      }
    }
  }

它又调用了自己(子类 ObjectTypeHandler)的 setNonNullParameter(...) 方法,进入:

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType)
      throws SQLException {
    TypeHandler handler = resolveTypeHandler(parameter, jdbcType);
    handler.setParameter(ps, i, parameter, jdbcType);
  }

setNonNullParameter(...) 方法的第一行代码是通过参数值实际类型(parameter.getClass())和 jdbcType 自动推算其 TypeHandler,这也印证了我们在文档篇中多次提到的 TypeHandler 不需要显式地定义,MyBatis 会自动推算出来。我们继续深入方法的第二行代码,直到 StringTypeHandler 的 setNonNullParameter(...):

  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
      throws SQLException {
    ps.setString(i, parameter);
  }

终于看到我们熟悉的 JDBC 代码了,那么参数在这里就被填充进 PreparedStatement 中了。

好了,到此为止 PreparedStatement 就完全准备好了,这时它就可以执行了。

附:

当前版本:mybatis-3.5.0
官网文档:MyBatis
项目实践:MyBatis Learn
手写源码:MyBatis 简易实现

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容