mybatis源碼配置文件解析之五:解析mappers標簽


在上篇博客中分析了plugins標簽,《mybatis源碼配置文件解析之四:解析plugins標簽 》,了解了其使用方式及背后的原理。現在來分析<mappers>標簽。

一、概述

在mybatis的核心配置文件(mybatis-config.xml)中,有關mappers的配置如下,

<mappers>
        <!-- <mapper resource="cn/com/mybatis/dao/UserMapper.xml"/> 
        <mapper resource="cn/com/mybatis/dao/MenuMapper.xml"/> -->
        <!--第二種做法 -->
        <package name="cn.com.mybatis.dao" />

    </mappers>

從上面的配置文件,可以看到配置mappers文件有兩種方式,一種是配置mapper標簽,另一種是配置package標簽。從配置的內容上來看,其配置的方式也是存在差別,配置mapper標簽配置的是一個xml文件,該文件中存在相關的sql語句;配置package標簽配置的是一個包的權限路徑(在spring和mybatis結合的時候使用了此種方式),該包表示的是mapper的接口文件。

最終上面的兩種方式都會被解析到mybatis的configuration類中,供用戶使用。如果存在重復配置mybatis會如何處理,下面在分析過程中會解答該問題。

二、詳述

上面了解了<mappers>標簽的使用方式,下面看mybatis是如何解析該標簽的。

在XMLConfigBuilder類中的parseConfiguration方法

private void parseConfiguration(XNode root) {
    try {
      //issue #117 read properties first
      //解析properties標簽    
      propertiesElement(root.evalNode("properties"));
      //解析settings標簽,1、把<setting>標簽解析為Properties對象
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      /*2、對<settings>標簽中的<setting>標簽中的內容進行解析,這里解析的是<setting name="vfsImpl" value=",">
      * VFS是mybatis中用來表示虛擬文件系統的一個抽象類,用來查找指定路徑下的資源。上面的key為vfsImpl的value可以是VFS的具體實現,必須
      * 是權限類名,多個使用逗號隔開,如果存在則設置到configuration中的vfsImpl屬性中,如果存在多個,則設置到configuration中的僅是最后一個
      * */
      loadCustomVfs(settings);
      //解析別名標簽,例<typeAlias alias="user" type="cn.com.bean.User"/>
      typeAliasesElement(root.evalNode("typeAliases"));
      //解析插件標簽
      pluginElement(root.evalNode("plugins"));
      //解析objectFactory標簽,此標簽的作用是mybatis每次創建結果對象的新實例時都會使用ObjectFactory,如果不設置
      //則默認使用DefaultObjectFactory來創建,設置之后使用設置的
      objectFactoryElement(root.evalNode("objectFactory"));
      //解析objectWrapperFactory標簽
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      //解析reflectorFactory標簽
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      //解析environments標簽
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      //解析<mappers>標簽
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

在該方法的最下方,看下面這行代碼

//解析<mappers>標簽
      mapperElement(root.evalNode("mappers"));

上面這行代碼便是解析mappers標簽的方法的調用。看其方法定義,

/**
 * 解析<mappers>標簽,在此標簽中可以配置<mapper>和<package>兩種標簽,其中<mapper>標簽可以配置resource、url、class三種屬性,
 * 這里的三種屬性,僅可以同時出現一個;<package>標簽只需要配置包名即可。
 * @param parent
 * @throws Exception
 */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
          //1、解析package標簽,獲得name屬性即包名
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          //掃描包名,把
          configuration.addMappers(mapperPackage);
        } else {//2、解析<mapper>標簽,標簽中可以配置resource、url、class三個屬性,但只能配置其中一個。
          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());
            /**
             * 處理mapper文件和對應的接口
             */
            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.");
          }
        }
      }
    }
  }

通過上面代碼的分析及在配置文件中的配置,解析<mappers>標簽分為兩部分,分別解析package和mapper子標簽。且是循環解析,也就是在含義多個包的時候需要配置多個package子標簽。

1、解析package子標簽

從上面的方法也就是mapperElement方法中,可以知道在解析<mappers>標簽時首先解析的是package子標簽,也就是說在同時配置package和mapper子標簽時,先解析的是package子標簽,解析標簽是有順序的。下面解析package子標簽的過程,僅給出和解析package有關的代碼,

  //1、解析package標簽,獲得name屬性即包名
        if ("package".equals(child.getName())) {
          String mapperPackage = child.getStringAttribute("name");
          //掃描包名,把
          configuration.addMappers(mapperPackage);
        }

上面的代碼,解析出package子標簽中的包名,調用了configuration.addMappers方法,

public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }

調用了mapperRegistry.addMappers方法,

/**
   * @since 3.2.2
   */
  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }

下面看addMappers方法,

public void addMappers(String packageName, Class<?> superType) {
      //解析packageName下的class文件
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    //處理解析好的mapper接口文件
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }

上面的方法首先會解析指定包下的class文件,看下面的解析過程,

resolverUtil.find(new ResolverUtil.IsA(superType), packageName);

看find方法,

public ResolverUtil<T> find(Test test, String packageName) {
      //把包名中的“.”替換成“/”
    String path = getPackagePath(packageName);

    try {
        //獲得包路徑下的所有文件名稱
      List<String> children = VFS.getInstance().list(path);
      for (String child : children) {
        if (child.endsWith(".class")) {
          addIfMatching(test, child);
        }
      }
    } catch (IOException ioe) {
      log.error("Could not read package: " + packageName, ioe);
    }

    return this;
  }

遍歷包下的所有class文件,調用addIfMatching方法,

@SuppressWarnings("unchecked")
  protected void addIfMatching(Test test, String fqn) {
    try {
      String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.');
      ClassLoader loader = getClassLoader();
      if (log.isDebugEnabled()) {
        log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]");
      }

      Class<?> type = loader.loadClass(externalName);
      if (test.matches(type)) {
        matches.add((Class<T>) type);
      }
    } catch (Throwable t) {
      log.warn("Could not examine class '" + fqn + "'" + " due to a " +
          t.getClass().getName() + " with message: " + t.getMessage());
    }
  }

加載class文件,判斷是否符合test.matches,該方法如下,

/** Returns true if type is assignable to the parent type supplied in the constructor. */
    @Override
    public boolean matches(Class<?> type) {
      return type != null && parent.isAssignableFrom(type);
    }

如果符合條件則放入matches中,matches定義在ResolverUtil中。回到addMappers方法中,find方法結束后調用下面的方法,獲取matches中的值,

Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();

然后循環解析mapperSet,

//處理解析好的mapper接口文件
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }

解析過程如下,

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 {
          //把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.
        //對mapper文件進行解析,
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        //具體的解析過程,1、先解析對應的XML映射文件,2、再解析接口方法中的注解信息
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {//如果解析失敗,則刪除knowMapper中的信息
          knownMappers.remove(type);
        }
      }
    }
  }

把mapper接口類封裝為MapperProxyFactory對象,並放入knownMappers中,接着對接口類進行解析,如果解析失敗會把剛才放入knownMappers中的值從knownMappers中移除。下面看如何解析接口類(解析對應的XML文件),

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
        //解析和接口同名的xml文件,前提是存在該文件,如果不存在該文件要怎么解析那?答案是解析接口中方法上的注解
        /**
         * 解析和接口同名的xml配置文件,最終要做的是把xml文件中的標簽,轉化為mapperStatement,
         * 並放入mappedStatements中
         * 
         */
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      //解析接口上的@CacheNamespace注解
      parseCache();
      parseCacheRef();
      //獲得接口中的所有方法,並解析方法上的注解
      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();
  }

上面的解析分為兩個過程,首先解析對應的XML映射文件,再解析方法上的注解。

1.1、解析xml文件

下面看如何繼續對應的XML文件,

loadXmlResource();

看如何解析xml文件,

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

首先確定XML映射文件的位置,和接口類同名且在同一個包下。如下的例子,

確定好對應的映射文件位置,接着便是解析該xml文件,

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

解析過程如下,

public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
        //解析mapper文件中的<mapper>標簽及其子標簽
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

    parsePendingResultMaps();
    parsePendingCacheRefs();
    parsePendingStatements();
  }

解析的過程在解析<mapper>標簽的時候再詳細分析。解析的最終結果是把XML中的select|update|delete|insert標簽轉化為MappedStatement對象,放入configuration中。

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

上面解析了接口對於的XML文件,下面看如何解析接口中的方法,

//獲得接口中的所有方法,並解析方法上的注解
      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);
    }
  }

從上面的代碼,可以看出最終調用了assistant.addMappedStatement方法,該方法會把注解信息封裝為MappedStatement對象,放入configuration中。詳細過程,后面分析。

2、解析mapper子標簽

上面分析了mybatis解析<package>標簽的過程,下面看直接解析<mapper>子標簽。代碼為部分代碼

else {//2、解析<mapper>標簽,標簽中可以配置resource、url、class三個屬性,但只能配置其中一個。
          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());
            /**
             * 處理mapper文件和對應的接口
             */
            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、class三個屬性,但是只能配置其中一個,上面分別對其進行了解析,其解析過程和上面解析<packge>中的過程類似,解析resource和url屬性的時候都是把XML映射文件解析為inputSream,然后對文件進行解析;解析class屬性的時候和解析<package>的過程一樣。

三、總結

本文分析了mybatis解析<mappers>標簽的過程,分為解析<package>、<mapper>子標簽,其解析過程主要為解析Mapper接口和XML映射文件,其詳細過程后面詳細分析。

 

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


免責聲明!

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



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