美文网首页
MyBatis 源码分析篇 6:Mapper 方法执行的“前因”

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

作者: 兆雪儿 | 来源:发表于2019-04-29 15:35 被阅读0次

    在前面的探索中,我们已经知道了 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 简易实现

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/fpjzgqtx.html