mybatis源碼配置文件解析之五:解析mappers標簽(解析class屬性)


在上篇文章中分析了mybatis解析mapper標簽中的resource、url屬性的過程,《mybatis源碼配置文件解析之五:解析mappers標簽(解析XML映射文件)》。通過分析可以知道在解析這兩個屬性的時候首先解析的是對應的XML映射文件,然后解析XML映射文件中的namespace屬性配置的接口,在上篇中說到該解析過程和mapper標簽中的class屬性的解析過程是一樣的,因為class屬性配置的即是一個接口的全限類名。

一、概述

在mybatis的核心配置文件中配置mappers標簽有以下方式,

<mappers>
        <mapper class="cn.com.mybatis.dao.UserMapper"/> 
</mappers>

上面這種方式便是mapper標簽的class屬性配置方式,其解析部分過程如下,

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

可以看到主要是調用了configuration.addMapper方法,和上篇文章中解析namespace調用的方法是一致的。看其具體實現

public <T> void addMapper(Class<T> type) {
    mapperRegistry.addMapper(type);
  }

下方分析mapperRegistry.addMapper方法。

二、詳述

mapperRegistry.addMapper方法的定義如下,

 public <T> void addMapper(Class<T> type) {
    if (type.isInterface()) {//判斷是否為接口
      if (hasMapper(type)) {//如果knownMappers中已經存在該type,則拋出異常
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }
      boolean loadCompleted = false;
      try {
          //1、把type放入knownMappers中,其value為一個MapperProxyFactory對象
        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.
        //2、對mapper文件及注解進行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes兩個變量
      //具體的解析過程,1、先解析對應的XML映射文件,2、再解析接口方法中的注解信息
        /**sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
         * 
         */
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {//3、如果解析失敗,則刪除knowMapper中的信息
          knownMappers.remove(type);
        }
      }
    }
  }

該方法主要分為下面幾個步驟。

1、檢查是否解析過接口

首先會判斷knowMappers中是否已經存在該接口,如果存在則會拋出異常

if (hasMapper(type)) {//如果knownMappers中已經存在該type,則拋出異常
        throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
      }

如果不存在則放入knownMappers中,

 //1、把type放入knownMappers中,其value為一個MapperProxyFactory對象
        knownMappers.put(type, new MapperProxyFactory<T>(type));

繼續解析對應的映射文件及接口方法注解

2、解析接口對應的映射文件及接口方法注解

上面把mapper接口放入了knownMappers中,接着需要解析映射文件及注解,

//2、對mapper文件及注解進行解析,初始化了sqlAnnotationTypessqlProviderAnnotationTypes兩個變量
        //具體的解析過程,1、先解析對應的XML映射文件,2、再解析接口方法中的注解信息
        /**sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
         * 
         */
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        
        parser.parse();
        loadCompleted = true;

上面的代碼,生成了一個MapperAnnotationBuilder實例,

public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
    String resource = type.getName().replace('.', '/') + ".java (best guess)";
    this.assistant = new MapperBuilderAssistant(configuration, resource);
    this.configuration = configuration;
    this.type = type;

    sqlAnnotationTypes.add(Select.class);
    sqlAnnotationTypes.add(Insert.class);
    sqlAnnotationTypes.add(Update.class);
    sqlAnnotationTypes.add(Delete.class);

    sqlProviderAnnotationTypes.add(SelectProvider.class);
    sqlProviderAnnotationTypes.add(InsertProvider.class);
    sqlProviderAnnotationTypes.add(UpdateProvider.class);
    sqlProviderAnnotationTypes.add(DeleteProvider.class);
  }

給sqlAnnotationTypes和sqlProviderAnnotationTypes進行了賦值。

下面看具體的解析過程,

parser.parse();

MapperAnnotationBuilder的parse方法如下,

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {//判斷是否加載過該Mapper接口
        //解析和接口同名的xml文件,前提是存在該文件,如果不存在該文件要怎么解析那?答案是解析接口中方法上的注解
        /**
         * 1、解析和接口同名的xml配置文件,最終要做的是把xml文件中的標簽,轉化為mapperStatement,
         * 並放入mappedStatements中
         * 
         */
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      //解析接口上的@CacheNamespace注解
      parseCache();
      parseCacheRef();
      //2、獲得接口中的所有方法,並解析方法上的注解
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
              //解析方法上的注解
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
  }

首先判斷是否加載過該資源,

if (!configuration.isResourceLoaded(resource)) {

}

只有未加載過,才會執行該方法的邏輯,否則該方法執行完畢。

public boolean isResourceLoaded(String resource) {
    return loadedResources.contains(resource);
  }

從loadResources中進行判斷,判斷是否解析過該Mapper接口,答案是沒有解析過,則會繼續解析。

1.1、解析對應的XML文件

首先會解析XML文件,調用下面的方法,

//解析和接口同名的xml文件,前提是存在該文件,如果不存在該文件要怎么解析那?答案是解析接口中方法上的注解
        /**
         * 1、解析和接口同名的xml配置文件,最終要做的是把xml文件中的標簽,轉化為mapperStatement,
         * 並放入mappedStatements中
         * 
         */
      loadXmlResource();

看loadXmlResource方法

/**
 * 解析mapper配置文件
 */
  private void loadXmlResource() {
    // Spring may not know the real resource name so we check a flag
    // to prevent loading again a resource twice
    // this flag is set at XMLMapperBuilder#bindMapperForNamespace
    if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
        //解析對應的XML映射文件,其名稱為接口類+"."+xml,即和接口類同名且在同一個包下。
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      InputStream inputStream = null;
      try {
        inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
      } catch (IOException e) {
        // ignore, resource is not required
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        //解析xml映射文件
        xmlParser.parse();
      }
    }
  }

首先進行了判斷,進入if判斷,看判斷上的注解

// Spring may not know the real resource name so we check a flag
// to prevent loading again a resource twice
// this flag is set at XMLMapperBuilder#bindMapperForNamespace

第一句注解沒理解什么意思,第二句的意思是方式兩次加載資源,第三句是說明了該標識是在XMLMapperBuilder類中的bindMapperForNamespace中進行的設置,如下

為什么這樣設置,后面會總結mapper的加載流程詳細說明該問題。

判斷之后尋找相應的XML映射文件,映射文件的文件路徑如下,

//解析對應的XML映射文件,其名稱為接口類+"."+xml,即和接口類同名且在同一個包下。
      String xmlResource = type.getName().replace('.', '/') + ".xml";

從上面可以看出Mapper接口文件和XML映射文件在同一個包下,且文件名稱相同(擴展名不同)。接着便是解析XML映射文件的邏輯。

if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        //解析xml映射文件
        xmlParser.parse();
      }

該邏輯和《mybatis源碼配置文件解析之五:解析mappers標簽(解析XML映射文件)》的過程是一樣的,調用XMLMapperBuilder的parse方法進行解析,解析的結果為MapperStatement對象。

1.2、解析接口方法上的注解

上面是解析接口對應的XML映射文件,解析完成之后,還要解析接口方法上的注解,因為mybatis的sql配置有兩種方式,一種是通過XML映射文件,另一種便是注解(當SQL比較復雜建議使用映射文件的方式),下面看解析注解的過程,

//2、獲得接口中的所有方法,並解析方法上的注解
      Method[] methods = type.getMethods();
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
              //解析方法上的注解
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }

通過反射的方式獲得接口中的所有方法,遍歷方法執行parseStatement方法

void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    //獲得方法上的注解,並生成SqlSource
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      //生成mappedStatementId,為接口的權限類名+方法名。從這里可以得出同一個接口或namespace中不允許有同名的方法名或id
      final String mappedStatementId = type.getName() + "." + method.getName();
      Integer fetchSize = null;
      Integer timeout = null;
      StatementType statementType = StatementType.PREPARED;
      ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
      SqlCommandType sqlCommandType = getSqlCommandType(method);
      boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
      boolean flushCache = !isSelect;
      boolean useCache = isSelect;

      KeyGenerator keyGenerator;
      String keyProperty = "id";
      String keyColumn = null;
      if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
        // first check for SelectKey annotation - that overrides everything else
        SelectKey selectKey = method.getAnnotation(SelectKey.class);
        if (selectKey != null) {
          keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
          keyProperty = selectKey.keyProperty();
        } else if (options == null) {
          keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        } else {
          keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
          keyProperty = options.keyProperty();
          keyColumn = options.keyColumn();
        }
      } else {
        keyGenerator = NoKeyGenerator.INSTANCE;
      }

      if (options != null) {
        if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
          flushCache = true;
        } else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
          flushCache = false;
        }
        useCache = options.useCache();
        fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
        timeout = options.timeout() > -1 ? options.timeout() : null;
        statementType = options.statementType();
        resultSetType = options.resultSetType();
      }

      String resultMapId = null;
      ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
      if (resultMapAnnotation != null) {
        String[] resultMaps = resultMapAnnotation.value();
        StringBuilder sb = new StringBuilder();
        for (String resultMap : resultMaps) {
          if (sb.length() > 0) {
            sb.append(",");
          }
          sb.append(resultMap);
        }
        resultMapId = sb.toString();
      } else if (isSelect) {
        resultMapId = parseResultMap(method);
      }

      assistant.addMappedStatement(
          mappedStatementId,
          sqlSource,
          statementType,
          sqlCommandType,
          fetchSize,
          timeout,
          // ParameterMapID
          null,
          parameterTypeClass,
          resultMapId,
          getReturnType(method),
          resultSetType,
          flushCache,
          useCache,
          // TODO gcode issue #577
          false,
          keyGenerator,
          keyProperty,
          keyColumn,
          // DatabaseID
          null,
          languageDriver,
          // ResultSets
          options != null ? nullOrEmpty(options.resultSets()) : null);
    }
  }

注解的解析和解析XML映射文件的方式是一樣的,解析的屬性是一致的。需要注意下面的注解

@SelectProvider(type=BaseUserProvider.class,method="getUser")

該注解的意思是定義select語句的提供者,需要配置type和method,即提供類的Class對象和相應的方法(返回一個字符串)

3、解析失敗回退

如果在繼續過程中失敗或拋出異常,則進行回退,回退的意思是從knownMappers中刪除該類型。

finally {
        if (!loadCompleted) {//3、如果解析失敗,則刪除knowMapper中的信息
          knownMappers.remove(type);
        }
      }

因為Mapper解析的過程有兩個結果一個是放入到configuration.knownMappers中的MapperProxyFactory對象,一個是放入到configuration.mappedStatements中MappedStatement對象,由於生產MappedStatement對象失敗,所以要回退生成MapperProxyFactory對象過程。

三、總結

本文分析了mybatis解析<mapper class=""/>的過程,依舊是包含MapperProxyFactory和MappedStatement兩個過程。

 

 

有不當之處,歡迎指正,感謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM