zoukankan      html  css  js  c++  java
  • Mybatis工作原理

    近来想写一个mybatis的分页插件,但是在写插件之前肯定要了解一下mybatis具体的工作原理吧,于是边参考别人的博客,边看源码就开干了。

    核心部件:

    SqlSession、Executor、StatementHandler、ParameterHandler、ResultSetHandler、TypeHandler、MappedStatement、Configuration

    在分析工作原理之前,首先看一下我的mybatis全局配置文件

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
    PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
    <configuration>
        <!-- 和spring整合后 environments配置将废除 -->
        <environments default="development">
            <environment id="development">
                <!-- 使用jdbc事务管理 -->
                <transactionManager type="JDBC" />
                <!-- 数据库连接池 -->
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.jdbc.Driver" />
                    <property name="url"
                        value="jdbc:mysql://localhost:3306/test?characterEncoding=utf-8" />
                    <property name="username" value="root" />
                    <property name="password" value="123456" />
                </dataSource>
            </environment>
        </environments>
        <mappers>
           <mapper  resource="sqlMapper/userMapper.xml"/>
        </mappers>
    </configuration>

    第一步:创建一个sqlSessionFactory

    在了解如何创建sqlSessionFactory之前,先看一下mybatis是如何加载全局配置文件,解析xml文件生成Configuration

    public Configuration parse() {
        if (parsed) {
          throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
      }
    private void parseConfiguration(XNode root) {
        try {
          propertiesElement(root.evalNode("properties")); //issue #117 read properties first
          typeAliasesElement(root.evalNode("typeAliases"));
          pluginElement(root.evalNode("plugins"));
          objectFactoryElement(root.evalNode("objectFactory"));
          objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
          settingsElement(root.evalNode("settings"));
          environmentsElement(root.evalNode("environments")); // read it after objectFactory and objectWrapperFactory issue #631
          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);
        }
      }

     在上面的第二段代码中有一句

    mapperElement(root.evalNode("mappers"));

    刚好我们的全局配置文件中有一个mapper的配置,由此可见,mapperElemet()方法是解析mapper映射文件的,具体代码如下

      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();
              } else if (resource == null && url != null && mapperClass == null) {
                ErrorContext.instance().resource(url);
                InputStream inputStream = Resources.getUrlAsStream(url);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                mapperParser.parse();
              } else if (resource == null && url == null && mapperClass != null) {
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
              } else {
                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
              }
            }
          }
        }
      }

    根据以上代码可以分析,在写mapper映射文件的地址时不仅可以写成resource,还可以写成url和mapperClass的形式,由于我们用的是resource,所以直接进入第一个判断,最后解析mapper映射文件的方法是

     private void configurationElement(XNode context) {
        try {
          String namespace = context.getStringAttribute("namespace");
          if (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. Cause: " + e, e);
        }
      }

    其中具体解析每一个sql语句节点的是

    buildStatementFromContext(context.evalNodes("select|insert|update|delete"));

    进入这个方法一层层深究,最后到这里可以知道MappedStatement是由builderAssistant(即MapperBuildAssistant)创建的

      public void parseStatementNode() {
        ...
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
            fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
            resultSetTypeEnum, flushCache, useCache, resultOrdered, 
            keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
      }

    最后进入方法addMappedStatement()mappedStatement最后以id为键保存在了Configuration中的一个map变量mappedStatements中。

      public MappedStatement addMappedStatement(
          String id,
          SqlSource sqlSource,
          StatementType statementType,
          SqlCommandType sqlCommandType,
          Integer fetchSize,
          Integer timeout,
          String parameterMap,
          Class<?> parameterType,
          String resultMap,
          Class<?> resultType,
          ResultSetType resultSetType,
          boolean flushCache,
          boolean useCache,
          boolean resultOrdered,
          KeyGenerator keyGenerator,
          String keyProperty,
          String keyColumn,
          String databaseId,
          LanguageDriver lang,
          String resultSets) {
        
        if (unresolvedCacheRef) throw new IncompleteElementException("Cache-ref not yet resolved");
        
        id = applyCurrentNamespace(id, false);
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    
        MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType);
        statementBuilder.resource(resource);
        statementBuilder.fetchSize(fetchSize);
        statementBuilder.statementType(statementType);
        statementBuilder.keyGenerator(keyGenerator);
        statementBuilder.keyProperty(keyProperty);
        statementBuilder.keyColumn(keyColumn);
        statementBuilder.databaseId(databaseId);
        statementBuilder.lang(lang);
        statementBuilder.resultOrdered(resultOrdered);
        statementBuilder.resulSets(resultSets);
        setStatementTimeout(timeout, statementBuilder);
    
        setStatementParameterMap(parameterMap, parameterType, statementBuilder);
        setStatementResultMap(resultMap, resultType, resultSetType, statementBuilder);
        setStatementCache(isSelect, flushCache, useCache, currentCache, statementBuilder);
    
        MappedStatement statement = statementBuilder.build();
        configuration.addMappedStatement(statement);
        return statement;
      }

     最后回到我们的创建sqlSessionFactory上,之前的一切都是为了生成一个sqlSessionFactory服务的

      public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
        try {
          XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
          return build(parser.parse());
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error building SqlSession.", e);
        } finally {
          ErrorContext.instance().reset();
          try {
            inputStream.close();
          } catch (IOException e) {
            // Intentionally ignore. Prefer previous error.
          }
        }
      }
        
      public SqlSessionFactory build(Configuration config) {
        return new DefaultSqlSessionFactory(config);
      }

    从上面的代码可以看出最后是通过以Configuration为参数build()方法生成DefautSqlSessionFactory

    第二步:创建sqlSession

      public SqlSession openSession() {
        return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
      }
      private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;
        try {
          final Environment environment = configuration.getEnvironment();
          final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
          tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
          final Executor executor = configuration.newExecutor(tx, execType);
          return new DefaultSqlSession(configuration, executor, autoCommit);
        } catch (Exception e) {
          closeTransaction(tx); // may have fetched a connection so lets call close()
          throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }
    //返回一个SqlSession,默认使用DefaultSqlSession 
     public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
        this.configuration = configuration;
        this.executor = executor;
        this.dirty = false;
        this.autoCommit = autoCommit;
      }

    executor在这一步得到创建,具体的使用在下一步。

    第三步:执行具体的sql请求

    在我的代码里执行的是

    User user = sqlSession.selectOne("test.findUserById", 1);

    具体到里面的方法就是

     public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        try {
         //1.根据Statement Id,在mybatis 配置对象Configuration中查找和配置文件相对应的MappedStatement
          MappedStatement ms = configuration.getMappedStatement(statement);
          //2. 将查询任务委托给MyBatis 的执行器 Executor
          List<E> result = executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
          return result;
        } catch (Exception e) {
          throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
        } finally {
          ErrorContext.instance().reset();
        }
      }

    在这里通过statementId拿到了我们在第一步存在map里面的MappedStatement。在这里引用参考博客的一句话:

    SqlSession根据Statement ID, 在mybatis配置对象Configuration中获取到对应的MappedStatement对象,然后调用mybatis执行器来执行具体的操作。

    再继续看query()queryFromDatabase()这两个方法

    @SuppressWarnings("unchecked")
      public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
        if (closed) throw new ExecutorException("Executor was closed.");
        if (queryStack == 0 && ms.isFlushCacheRequired()) {
          clearLocalCache();
        }
        List<E> list;
        try {
          queryStack++;
          list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
          if (list != null) {
            handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
          } else {
            list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
          }
        } finally {
          queryStack--;
        }
        if (queryStack == 0) {
          for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
          }
          deferredLoads.clear(); // issue #601
          if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache(); // issue #482
          }
        }
        return list;
      }
      private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
        List<E> list;
        localCache.putObject(key, EXECUTION_PLACEHOLDER);
        try {
          list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
        } finally {
          localCache.removeObject(key);
        }
        localCache.putObject(key, list);
        if (ms.getStatementType() == StatementType.CALLABLE) {
          localOutputParameterCache.putObject(key, parameter);
        }
        return list;
      }

    在这两个方法里面会为当前的查询创建一个缓存key,如果缓存中没有值,直接从数据库中读取,执行查询后将得到的list结果放入缓存之中。

    紧接着看doQuery()SimpleExecutor类中重写的方法

    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);
        }
      }

    Statement连接对象就是在这里创建的,因此Executor的作用之一就是创建Statement了,创建完后又把Statement丢给StatementHandler返回List查询结果。

    接下来再看一下这里的两个方法prepareStatement()query()的具体实现

      private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
        Statement stmt;
        Connection connection = getConnection(statementLog);
        stmt = handler.prepare(connection);
        handler.parameterize(stmt);
        return stmt;
      }
      public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
        PreparedStatement ps = (PreparedStatement) statement;
        ps.execute();
        return resultSetHandler.<E> handleResultSets(ps);
      }

    prepareStatement()是创建Statement的具体实现方法,调用parameterize()对创建的Statement对象设置参数,即为我们设为占位符的地方赋上指定的参数parameterize()方法再深入进去就是调用ParameterHandlersetParameters()方法具体赋值了

    这里的query()是调用了ResultSetHandlerhandleResultSets(Statement) 方法。作用就是把ResultSet结果集对象转换成List类型的集合。

    总结以上步骤就是:

    1.根据具体传入的参数,动态地生成需要执行的SQL语句,用BoundSql对象表示

    2.为当前的查询创建一个缓存Key

    3.缓存中没有值,直接从数据库中读取数据

    4.执行查询,返回List 结果,然后 将查询的结果放入缓存之中

    5.根据既有的参数,创建StatementHandler对象来执行查询操作

    6.将创建Statement传递给StatementHandler对象,调用parameterize()方法赋值

    7.调用StatementHandler.query()方法,返回List结果集

    总结

    以上三个步骤所有流程大体可以用一张图来总结

    这是本人第一次分析框架源码,感受颇深,另外肯定也有不足之处,望批评指正。

    最后本博客参考了以下博客:

    《深入理解mybatis原理》 MyBatis的架构设计以及实例分析.https://blog.csdn.net/luanlouis/article/details/40422941

    debug源码之mybatis.https://blog.csdn.net/a412451848/article/details/82723754

  • 相关阅读:
    Spring框架构造注入的属性问题type属性
    Spring框架AOP添加日志记录功能
    Spring框架构造注入
    Spring框架AOP原理
    Spring框架构造注入的顺序问题index属性
    Spring框架使用P命名空间进行注入
    工作中的SQL脚本
    spring框架ioc设置注入小demo
    [笔试] C和C++动态内存分配和释放的区别
    [算法] 当今世界最为经典的十大算法投票进行时
  • 原文地址:https://www.cnblogs.com/scuury/p/10371246.html
Copyright © 2011-2022 走看看