1. 程式人生 > 其它 >MyBatis溫故而知新-底層執行原理

MyBatis溫故而知新-底層執行原理

準備工作

public class MainClass {
  public static void main(String[] args) throws Exception {
    String resources = "mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resources);

    SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
    SqlSession sqlSession = sqlSessionFactory.openSession();

    Student student = sqlSession.selectOne("org.apache.ibatis.dao.StudentMapper.getStudent",1);
    System.out.println(student.toString());
    sqlSession.close();
  }
}

MyBatis是如何獲取資料來源的

這是我們mybatis-config.xml中配置資料庫的4個關鍵屬性,也就是看看MyBatis是怎麼來解析這個配置檔案塊的。

<environments default="development">
   <environment id="development">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
      </dataSource>
    </environment>
 </environments>

資料來源獲取,我們從上面的程式碼片段中開始分析,在SqlSessionFactoryBuilder().build(inputStream) 方法中入手,這裡看到例項了XMLConfigBuilder類。

SqlSessionFactoryBuilder

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      //parser.parse()方法返回Configuration物件,然後呼叫build(Configuration config)
      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);
}

XMLConfigBuilder#parse()

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"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      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);
    }
  }

看到parseConfiguration引數為XNode就知道這個方法裡肯定是要解析xml節點了,可以debug檢視下root引數的值,嘗試debug後發現,其內容就是我們的配置檔案mybatis-config.xml的檔案內容。 接著我們在22行這裡則看到了熟悉的environments節點,那就直接看下environmentsElement方法。

<configuration>
    <properties resource="db.properties"/>    
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING"/>        
        <setting name="cacheEnabled" value="true"/>        
        <setting name="lazyLoadingEnabled" value="true"/>        
        <setting name="aggressiveLazyLoading" value="false"/>        
        <setting name="localCacheScope" value="SESSION"/>        
    </settings>
    <typeAliases>
        <typeAlias alias="Student" type="org.apache.ibatis.domain.Student"/>        
    </typeAliases>
    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>            
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>                
                <property name="url" value="${jdbc.url}"/>                
                <property name="username" value="${jdbc.username}"/>               
                <property name="password" value="${jdbc.password}"/>               
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/StudentMapper.xml"/>        
    </mappers>
</configuration>

XMLConfigBuilder#environmentsElement

這個方法引數XNode內容則是配置檔案裡的部分。這裡就看到了解析dataSource節點的地方,這裡使用DataSourceFactory返回一個DataSource, 那麼這裡是怎麼拿到DataSourceFactory的,又是怎麼拿到DataSource的。

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
      if (environment == null) {
        environment = context.getStringAttribute("default");
      }
      for (XNode child : context.getChildren()) {
        String id = child.getStringAttribute("id");
        if (isSpecifiedEnvironment(id)) {
          TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
          DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
          DataSource dataSource = dsFactory.getDataSource();
          Environment.Builder environmentBuilder = new Environment.Builder(id)
              .transactionFactory(txFactory)
              .dataSource(dataSource);
          configuration.setEnvironment(environmentBuilder.build());
          break;
        }
      }
    }
  }

解析DataSourceFactory

這裡看到了getDeclaredConstructor().newInstance()反射例項化一個PoolDataSourceFactory。

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      Properties props = context.getChildrenAsProperties();
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type).getDeclaredConstructor().newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
  }
public class PooledDataSourceFactory extends UnpooledDataSourceFactory {

  public PooledDataSourceFactory() {
    this.dataSource = new PooledDataSource();
  }

}

解析DataSource

拿到PooledDataSourceFactory後則呼叫dsFactory.getDataSource()拿到資料來源, 這裡重要的程式碼是最後一句configuration.setEnvironment,這裡最後會把解析後的Environment物件給Configuration物件賦值, Configuration物件也是MyBatis框架中相當重量級的一個物件。

MyBatis是如何獲取SQL語句的

解析SQL語句Code Chain


文章開頭我們是用sqlSession.selectOne()方法來獲取SQL語句的,那麼這個SQL語句是怎麼解析並獲取呢?上面我們分析獲取資料來源時提到XMLConfigBuilder.parseConfiguration方法時看到會呼叫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);
            try(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);
            try(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.");
          }
        }
      }
    }
  }

該方法會根據配置檔案依次解析package, resource, url, class4種配置mapper的方式。

<mappers>
    <mapper resource="mapper/StudentMapper.xml"/>
</mappers>

這裡我們是用resource方式配置的,所以這個方法程式會在Line 12 的邏輯開始執行。

XMLStatementBuilder

解析SQL語句的關鍵邏輯都在這個類的parseStatementNode方法裡,方法最後會呼叫MapperBuilderAssistant#.addMappedStatement(), 這個方法會很到MyBatis裡很重要的一個類MappedStatement物件,這個方法最關鍵的一行程式碼是先生成MappedStatement物件,最後會把生成的MappedStatement物件放入Configuration物件的Map字典中。
看到這個方法裡的程式碼應該很直觀,就是在解析XML中select標籤的一些屬性,比如useCache, flushCache等,這些標籤可以檢視MyBatis文件,真的很詳細。

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

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

MapperBuilderAssistant

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

    ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
    if (statementParameterMap != null) {
      statementBuilder.parameterMap(statementParameterMap);
    }

    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

MyBatis是如何操作資料庫的

執行器(Executor)

執行器在MyBatis中是用來封裝Statement執行JDBC操作,MyBatis共包含3種類型的執行器:SimpleExecutor, ReuseExecutor, BatchExecutor,預設值使用SimpleExecutor。

 @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.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
 }

MySQL查詢的元資料和實體建立對映關係

那麼我們的select語句查詢出來的一條mysql元資料和java實體到底是怎樣建立對映關係的? 我們先從PreparedStatementHandler#query方法著手分析。

 @Override
 public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    //這兩行程式碼是不是很親切了,在JDBC中我們就是這麼幹的。
    //1.執行SQL
    PreparedStatement ps = (PreparedStatement) statement;
    //2.處理結果集
    ps.execute();
    return resultSetHandler.handleResultSets(ps);
 }

結果集對映

DefaultResultSetHandler#handleResultSets

@Override
 public List<Object> handleResultSets(Statement stmt) throws SQLException {
    ErrorContext.instance().activity("handling results").object(mappedStatement.getId());
    //1.<select>標籤的resultMap屬性,用來裝對映後的實體物件資料
    final List<Object> multipleResults = new ArrayList<>();

    int resultSetCount = 0;
    //2.獲取第一個結果集資料
    ResultSetWrapper rsw = getFirstResultSet(stmt);
    //3.這裡就是取出需要對映的ResultMap
    List<ResultMap> resultMaps = mappedStatement.getResultMaps();
    //4.這裡就是需要對映的ResultMap的數量
    int resultMapCount = resultMaps.size();
    validateResultMapsCount(rsw, resultMapCount);
    //5.迴圈處理每個ResultMap
    while (rsw != null && resultMapCount > resultSetCount) {
      //6.迴圈取出需要對映的ResultMap(id,type)實體名稱
      ResultMap resultMap = resultMaps.get(resultSetCount);
      //7.從rsw結果集引數中獲取查詢結果,再根據resultMap對映資訊,將查詢結果裝到到multipleResults中
      handleResultSet(rsw, resultMap, multipleResults, null);
      rsw = getNextResultSet(stmt);
      cleanUpAfterHandlingResultSet();
      resultSetCount++;
    }
    
    //這裡會獲取select標籤的resultSet屬性
    String[] resultSets = mappedStatement.getResultSets();
    if (resultSets != null) {
      while (rsw != null && resultSetCount < resultSets.length) {
        ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]);
        if (parentMapping != null) {
          String nestedResultMapId = parentMapping.getNestedResultMapId();
          ResultMap resultMap = configuration.getResultMap(nestedResultMapId);
          handleResultSet(rsw, resultMap, null, parentMapping);
        }
        rsw = getNextResultSet(stmt);
        cleanUpAfterHandlingResultSet();
        resultSetCount++;
      }
    }

    return collapseSingleResultList(multipleResults);
}

DefaultResultSetHandler#handleResultSet

private void handleResultSet(ResultSetWrapper rsw, ResultMap resultMap, List<Object> multipleResults, ResultMapping parentMapping) throws SQLException {
    try {
      if (parentMapping != null) {
        handleRowValues(rsw, resultMap, null, RowBounds.DEFAULT, parentMapping);
      } else {
        if (resultHandler == null) {
          //1.建立DefaultResultHandler 來處理結果集
          DefaultResultHandler defaultResultHandler = new DefaultResultHandler(objectFactory);
          //2.這裡就是處理對映結果集的最終方法
          handleRowValues(rsw, resultMap, defaultResultHandler, rowBounds, null);
          multipleResults.add(defaultResultHandler.getResultList());
        } else {
          handleRowValues(rsw, resultMap, resultHandler, rowBounds, null);
        }
      }
    } finally {
      closeResultSet(rsw.getResultSet());
    }
  }

DefaultResultSetHandler#handleRowValues

public void handleRowValues(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping) throws SQLException {
    if (resultMap.hasNestedResultMaps()) {
      ensureNoRowBounds();
      checkResultHandler();
      //巢狀結果對映
      handleRowValuesForNestedResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    } else {
      //簡單結果對映
      handleRowValuesForSimpleResultMap(rsw, resultMap, resultHandler, rowBounds, parentMapping);
    }
  }

ResultSetWrapper

這裡還有一個關鍵的類在上述方法呼叫鏈裡沒有提到就是ResultSetWrapper, 它的構造器裡有3個集合分別是columnNames,jdbcTypes, classNames, 當你看到這3個數組時腦海裡是不是浮現出MyBatis框架的意圖呢,代替你管理JDBC,並從結果集建立到java物件的對映。

public ResultSetWrapper(ResultSet rs, Configuration configuration) throws SQLException {
    super();
    this.typeHandlerRegistry = configuration.getTypeHandlerRegistry();
    this.resultSet = rs;
    final ResultSetMetaData metaData = rs.getMetaData();
    final int columnCount = metaData.getColumnCount();
    for (int i = 1; i <= columnCount; i++) {
      columnNames.add(configuration.isUseColumnLabel() ? metaData.getColumnLabel(i) : metaData.getColumnName(i));
      jdbcTypes.add(JdbcType.forCode(metaData.getColumnType(i)));
      classNames.add(metaData.getColumnClassName(i));
    }
  }

DefaultResultSetHandler#handleRowValuesForSimpleResultMap

private void handleRowValuesForSimpleResultMap(ResultSetWrapper rsw, ResultMap resultMap, ResultHandler<?> resultHandler, RowBounds rowBounds, ResultMapping parentMapping)
      throws SQLException {
    DefaultResultContext<Object> resultContext = new DefaultResultContext<>();
    //1.因為在建立ResultSetWrapper的時候 我們將結果集封裝進去了 現在將結果集取出來
    ResultSet resultSet = rsw.getResultSet();
    //2.分頁資訊
    skipRows(resultSet, rowBounds);
    while (shouldProcessMoreRows(resultContext, rowBounds) && !resultSet.isClosed() && resultSet.next()) {
      ResultMap discriminatedResultMap = resolveDiscriminatedResultMap(resultSet, resultMap, null);
      ///3.將查詢結果封裝到POJO中(這一行程式碼重點關注)
      Object rowValue = getRowValue(rsw, discriminatedResultMap, null);
      storeObject(resultHandler, resultContext, rowValue, parentMapping, resultSet);
    }
  }

DefaultResultSetHandler#getRowValue

private Object getRowValue(ResultSetWrapper rsw, ResultMap resultMap, String columnPrefix) throws SQLException {
    final ResultLoaderMap lazyLoader = new ResultLoaderMap();
    Object rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix);
    if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
      final MetaObject metaObject = configuration.newMetaObject(rowValue);
      boolean foundValues = this.useConstructorMappings;
      if (shouldApplyAutomaticMappings(resultMap, false)) {
        //封裝結果集 將sql的結果與實體類封裝對應起來
        foundValues = applyAutomaticMappings(rsw, resultMap, metaObject, columnPrefix) || foundValues;
      }
      foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix) || foundValues;
      foundValues = lazyLoader.size() > 0 || foundValues;
      rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
    }
    return rowValue;
  }

DefaultResultSetHandler#applyAutomaticMappings

這個方法裡會迴圈處理行列欄位並對映實體屬性。

private boolean applyAutomaticMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String columnPrefix) throws SQLException {
    //1.封裝你的返回結果集物件
    List<UnMappedColumnAutoMapping> autoMapping = createAutomaticMappings(rsw, resultMap, metaObject, columnPrefix);
    boolean foundValues = false;
    if (!autoMapping.isEmpty()) {
      //2.遍歷 給實體類物件賦值
      for (UnMappedColumnAutoMapping mapping : autoMapping) {
        //3.根據實體屬性 去sql中取值,拿到SQL的結果值和實體類的屬性值,並封裝在metaObject物件中。
        final Object value = mapping.typeHandler.getResult(rsw.getResultSet(), mapping.column);
        if (value != null) {
          foundValues = true;
        }
        if (value != null || (configuration.isCallSettersOnNulls() && !mapping.primitive)) {
          metaObject.setValue(mapping.property, value);
        }
      }
    }
    //返回值為boolean值
    return foundValues;
  }

這裡autoMapping集合裡能看到就是存放三個欄位的元資料。OK,執行到這一步,外層的getRowValue的返回值就拿到了返回的實體物件。

部落格地址:http://www.cnblogs.com/sword-successful/
部落格版權:本文以學習、研究和分享為主,歡迎轉載,但必須在文章頁面明顯位置給出原文連線。
如果文中有不妥或者錯誤的地方還望高手的你指出,以免誤人子弟。如果覺得本文對你有所幫助不如【推薦】一下!如果你有更好的建議,不如留言一起討論,共同進步!
再次感謝您耐心的讀完本篇文章。