mybatis執行過程源碼分析

1 總結

代碼有些多,怕有的大兄弟耐不下心, 就先寫一個總結。

mybatis的的大概流程是這樣的:

  • 通過解析配置文件分析mapper文件和接口,生成代理對象。
  • 根據配置文件,創建會話
  • 通過會話拿到代理對象
  • 通過代理對象,執行具體方法,將接口和sql關聯,並執行。

Note: mybatis version --3.4.6

2 代碼示例

public void howToUseMybatis() throws Exception {

    String confLocation = "mybatis-conf.xml";

    Reader reader = Resources.getResourceAsReader(confLocation);

    // step1. 通過配置文件構建 SqlSessionFactory
    SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(reader);

    // step2. openSession
    SqlSession sqlSession = sessionFactory.openSession();

    // step3. 給Mapper接口生成對應的的代理類。
    PetMapper petMapper = sqlSession.getMapper(PetMapper.class);

    // step4. 調用方法
    Pet pet = petMapper.selectByName("zhangsan");

    System.out.println(pet);
}

2.1 Step1

解析配置文件,然後構建出SqlSessionFactory。build中有很多細節,需要一一分析出來。

org.apache.ibatis.session.SqlSessionFactoryBuilder#build()方法一直點下, 進入終極buiild。在終極build中,有parse方法,顧名思義,那就是解析,那就要去看去解析什麼。

  public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    	// 進入parse方法。
      return build(parser.parse());
    } 
  ..
  }

進入org.apache.ibatis.builder.xml.XMLConfigBuilder#parse方法, 可以看出來,是在解析的配置文件,根節點是configuration。

  public Configuration parse() {
    ...
    // 具體解析
    parseConfiguration(parser.evalNode("/configuration"));
    // 返回的是configration對象。
    return configuration;
  }

接下來,進入parseConfiguration,繼續追蹤細節。

  private void parseConfiguration(XNode root) {
    try {
      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);
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析mapper文件,我們需要具體關注。因爲關乎後面如何通過接口代理來調用方法。
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

我們通過節點名稱可以看出來,在解析configuration文件中的每個節點。但裏面有個我們需要特別關注,那就是mapperElement(root.evalNode("mappers"));,這個關乎後面如何和java代理關聯起來。通過接口來調用方法。我們進入這個方法,查看具體細節。

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {
    			...
        } 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());
            // 因爲我配置是resource,所以進入的是此方法。
            mapperParser.parse();
          } else if (resource == null && url != null && mapperClass == null) {
            mapperParser.parse();
          } else if (resource == null && url == null && mapperClass != null) {
            configuration.addMapper(mapperInterface);
          } else {
          }
        }
      }
    }

這個方法裏面是根據配置文件,採用什麼邏輯來解析,因爲我配置的mapper-resources, 所以進入的是上面方法。沒啥好逼逼的,直接點進去看看。

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析mapper配置文件
      configurationElement(parser.evalNode("/mapper"));
      // 防止重複解析
      configuration.addLoadedResource(resource);
      // 關聯mapper.xml對應的的mapper.java文件
      bindMapperForNamespace();
    }
    ...
  }
2.1.1 解析mapper.xml

來了,就是這, 就是這!!讓我開始裸看代碼出現迷糊的地方,沒有找到解析配置文件的地方。大家注意,我們一步步來。先從configurationElement(parser.evalNode("/mapper"));看起來。

  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"));
      // 需要關注這個,這個就是解析sql的方法入口
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
    }
  }

我們通過解析的節點可以看出來,是在解析配置中的節點。例如:resultMap,sql等。 我們需要關注buildStatementFromContext(context.evalNodes("select|insert|update|delete"));,因爲這個是具體解析sql的方法。並且透露一下,這個裏面也是生成MappedStatement的地方。

  private void buildStatementFromContext(List<XNode> list) {
    if (configuration.getDatabaseId() != null) {
      buildStatementFromContext(list, configuration.getDatabaseId());
    }
    buildStatementFromContext(list, null);
  }

  private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
    for (XNode context : list) {
      final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
      try {
        // look here.  具體執行的地方。
        statementParser.parseStatementNode();
      } catch (IncompleteElementException e) {
        configuration.addIncompleteStatement(statementParser);
      }
    }
  }

我把兩個代碼一起貼出來,這兩段代碼沒什麼好解釋的,我們直接看statementParser.parseStatementNode();

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultType = context.getStringAttribute("resultType");
    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);
		...
    // MappedStatement
    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered, 
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

代碼比較多,刪了一部分。不過我們還是可以看出來,就是在解析每一個sql片段,生成一個MappedStatement. 不過我們還是要點進addMappedStatement方法, 因爲裏面還有一段生成id的邏輯。

public MappedStatement addMappedStatement(...){
     if (unresolvedCacheRef) {
      throw new IncompleteElementException("Cache-ref not yet resolved");
    }
		// 處理ID, 把id轉換成使用namespace+方法名。 
    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;
}

我們在這就可以清楚的看到,mapper.xml文件最後被解析成了MappedStatement文件,並且id是以namespace+方法名組成的。並且把這些MappedStatement放入到了全局的configuration類中。自此,我們看到MappedStatement細節全部處理完成。

2.1.2 解析Mapper.java

回過頭,我們接着看bindMapperForNamespace();方法,這個方法也比較重要。也解釋了爲什麼明明是接口(Interface), 但是可以返回對象。Let’s go .

private void bindMapperForNamespace() {
  	// 獲取MappedStatement 的Namespace
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        // 通過namespace獲取接口類型。這裏也說明了,爲什麼我們在配置mapper.xml文件的時候,
        // namespace 要和接口的包名相同
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        //ignore, bound type is not required
      }
      if (boundType != null) {
        if (!configuration.hasMapper(boundType)) {
          // Spring may not know the real resource name so we set a flag
          // to prevent loading again this resource from the mapper interface
          // look at MapperAnnotationBuilder#loadXmlResource
          configuration.addLoadedResource("namespace:" + namespace);
          // 關鍵點,把mapper.java類型保存起來
          configuration.addMapper(boundType);
        }
      }
    }

核心點了,解析到Mapper.java後,調用了configuration.addMapper(boundType);,裏面具體是什麼樣的呢?我們一路點下去,最後addMapper的具體代碼如下.

  public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {
      if (hasMapper(type)) {
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
        // 核心關鍵點,把類型和,和類型生成的MapperProxyFactory放入了map中保存了起來
        knownMappers.put(type, new MapperProxyFactory<T>(type));
        // It's important that the type is added before the parser is run
        // otherwise the binding may automatically be attempted by the
        // mapper parser. If the type is already known, it won't try.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 通過接口,解析,因爲有的sql直接寫在接口上面。這樣的也會生成MappedStatement對象。
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

我們看到,核心是把類型,和代理工廠放入到了Map中保存了起來。接着解析接口上面的sql。至此。build代碼解析完了,核心關鍵點都點了出來。但是還有一個小細節,我們看到解析完,返回的configuration對象。然後通過buid()方法,build出來的是DefaultSqlSessionFactory。

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);
  }

2.2 Step2

從openSession()方法點進去,直接看看是如何開啓一個會話的。(上面說到了,生成的是DefaultSqlSessionFactory, 別進錯了)

  private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
      // 獲取數據庫信息,已經解析完。
      final Environment environment = configuration.getEnvironment();
      // 生成TransactionFactory
      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
      // 創建事務
      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
      // 創建執行器
      final Executor executor = configuration.newExecutor(tx, execType);
      // 生成SqlSession
      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();
    }
  }
2.2.1 Executor

這個裏面有生成SqlSession的具體不走,前面沒啥好看的,就是從解析出來的configuration中獲取信息。 我們直接從創建執行器開始。

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    // 獲取需要創建的執行器的類型,
    executorType = executorType == null ? defaultExecutorType : executorType;
    // 有可能默認類型也爲空,所以多判斷了一次。
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      // 如果沒有輸入類型,則默認創建的是 SimpleExecutor
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      // 如果開啓了二級緩存,則會在對執行器進行包裝
      executor = new CachingExecutor(executor);
    }
    // 攔截器連
    executor = (Executor) interceptorChain.pluginAll(executor);
    return executor;
  }

我們可以看到,執行器有三種類型,分別是BatchExecutorReuseExecutorSimpleExecutor(就不介紹三種執行器了)。默認採用的是SimpleExecutor。

2.2.2 SqlSession

我們看代碼,發現生成DefaultSqlSession傳入了Executor,在sqlsession中調用方法時,具體的執行邏輯是通過executor執行的。那一個構造函數和查詢方法看一下。

  public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
    this.configuration = configuration;
    this.executor = executor;
    this.dirty = false;
    this.autoCommit = autoCommit;
  }

  public <T> Cursor<T> selectCursor(String statement, Object parameter, RowBounds rowBounds) {
    try {
      MappedStatement ms = configuration.getMappedStatement(statement);
      // 通過執行器來執行查詢
      Cursor<T> cursor = executor.queryCursor(ms, wrapCollection(parameter), rowBounds);
      registerCursor(cursor);
      return cursor;
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error querying database.  Cause: " + e, e);
    } finally {
      ErrorContext.instance().reset();
    }
  }

2.3 Step3

通過SqlSession獲取Mapper對象。 這個就得好好說道說道了。明明是接口,爲什麼可以返回對象?我們從代碼中,一直點下去,直接發現代碼。

  public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 從最開始解析保存接口和代理工廠的map中,獲取對象的MapperProxyFactory
    final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
      throw new BindingException("Type " + type + " is not known to the MapperRegistry.");
    }
    try {
      // 創建實例代理事例,我們的好好瞅瞅
      return mapperProxyFactory.newInstance(sqlSession);
    } catch (Exception e) {
      throw new BindingException("Error getting mapper instance. Cause: " + e, e);
    }
  }

我們發現getMapper,最後返回的是代理對象,這也就是爲什麼明明是接口,但是可以返回對象。但是爲了明白具體邏輯,我們還是要進去看看具體是如何實現的,以及如何是把MappedStatement和接口關聯起來的。

	public T newInstance(SqlSession sqlSession) {
    // 創建 MapperProxy ,
    final MapperProxy<T> mapperProxy = new MapperProxy<T>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }

public class MapperProxy<T> implements InvocationHandler, Serializable {
  // 這是一個InvocationHandler,代理對象具體執行的邏輯
  ....
    @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
      } else if (isDefaultMethod(method)) {
        return invokeDefaultMethod(proxy, method, args);
      }
    } catch (Throwable t) {
      throw ExceptionUtil.unwrapThrowable(t);
    }
    // 這個裏面有核心,方法是如何和Mapper關聯起來的
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 具體執行邏輯
    return mapperMethod.execute(sqlSession, args);
  }
}

看到這, 我們應該都明白了,接口調用的方法,通過走代理,然後在代理對象中進行邏輯處理。 這個時候,我們就要進cachedMapperMethod(method);,這裏是核心。

  private MapperMethod cachedMapperMethod(Method method) {
    MapperMethod mapperMethod = methodCache.get(method);
    if (mapperMethod == null) {
      // 構造MapperMethod 對象, 這個裏面就是具體的關聯邏輯。
      mapperMethod = new MapperMethod(mapperInterface, method, sqlSession.getConfiguration());
      methodCache.put(method, mapperMethod);
    }
    return mapperMethod;
  }
	
	// MapperMethod的構造函數
  public MapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
    // 關聯邏輯
    this.command = new SqlCommand(config, mapperInterface, method);
    this.method = new MethodSignature(config, mapperInterface, method);
  }

這段代碼是構建MapperMethod, 關鍵的邏輯在構造函數裏面。我們來看SqlCommand的構造函數

   public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
      final String methodName = method.getName();
      final Class<?> declaringClass = method.getDeclaringClass();
     // 核心邏輯,顧名思義,解析mappedStatement.並且是通過Method.
      MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass,
          configuration);
    	...
    }

到這,我們就得就去瞅瞅了,是如何解析MappedStatement的,關聯接口方法和MappedStatement.

private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName,
        Class<?> declaringClass, Configuration configuration) {
  		// statementId 
      String statementId = mapperInterface.getName() + "." + methodName;
  
      if (configuration.hasStatement(statementId)) {
        // 通過statementId獲取存入的mappedStatement
        return configuration.getMappedStatement(statementId);
      } else if (mapperInterface.equals(declaringClass)) {
        return null;
      }
      for (Class<?> superInterface : mapperInterface.getInterfaces()) {
        if (declaringClass.isAssignableFrom(superInterface)) {
          // 父類接口。邏輯一樣
          MappedStatement ms = resolveMappedStatement(superInterface, methodName,
              declaringClass, configuration);
          if (ms != null) {
            return ms;
          }
        }
      }
      return null;
    }

看到這,就知道了,是通過方法的類名+方法名,組成statementId,然後去早已經解析的MappedStatement中,找到對應的MappedStatment,就關聯起來啦。

2.4 Step4

執行方法。通過上面分析,我們已經知道是通過代理,找到對應的MappedStatement,然後執行具體的sql.

3 完結撒花

ooook, 現在已經搞完全部的流程啦,要自己在捋順啦,鋼巴得!

掛上[GitHub][https://github.com/fattyca1/mybatis-demo]代碼地址,拉下來,在本地Debug起來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章