Mybatis源码学习(21)-Mybatis中如何解析所有配置的Mapper映射文件

一、<mappers>元素的结构

在Mybatis-config.xml中,<mappers>元素的配置方式有以下几种:

<!-- 使用相对于类路径的资源引用 -->
<mappers>
  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
  <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
  <mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
  <mapper url="file:///var/mappers/AuthorMapper.xml"/>
  <mapper url="file:///var/mappers/BlogMapper.xml"/>
  <mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
  <mapper class="org.mybatis.builder.AuthorMapper"/>
  <mapper class="org.mybatis.builder.BlogMapper"/>
  <mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
  <package name="org.mybatis.builder"/>
</mappers>

从定义文档结构的mybatis-3-config.dtd文件中,可以看出<mappers>元素可以允许的结构:

  1. <mappers>元素下可以有<mapper>、<package>子元素
  2. <mapper>子元素可以有resource、url、class三个属性中的一个,且只能有一个
  3. <package>子元素必须有属性package。
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
<!ELEMENT mappers (mapper*,package*)>

<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>

<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>

  通过上面内容我们可以知道,在<mappers>元素中,有两类子元素<mapper>和<package>,其中<mapper>子元素又有三种属性可以加载对应的配置文件,分别是:resource、url和class。在这几种配置方法中,底层最终都是通过两种方式实现,一种是面向接口编程的方式:通过MapperRegisty的addMapper()方法实现了映射关系的解析和注册,另外一种是解析普通映射配置文件的方式,后面会分别讲解。

二、<mappers>元素解析入口

  <mappers>元素的解析 入口如下所示:

/**
   * 解析配置文件中的mappers元素
   * @param parent
   * @throws Exception
   */
  private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        if ("package".equals(child.getName())) {//解析<package>元素
          String mapperPackage = child.getStringAttribute("name");
          configuration.addMappers(mapperPackage);
        } else {//解析<mapper>元素
          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.");
          } 
        }
      }
    }
  }

  针对解析<mappers>子元素的方法不同,我下面分为三类来分析:

  1. 解析<package>元素
    该类型的解析,首先是通过属性name获取表示的包名,然后通过反射机制,加载包下所有符合条件的类,然后在通过MapperRegisty的addMapper()方法实现了映射关系的解析和注册。
  2. 解析属性为resource或url的<mapper>元素
    使用<mapper>子元素且使用的属性是resource或url时,他们的解析方式基本上是一样。首先通过资源路径加载资源,然后构建XMLMapperBuilder实例对象,再通过XMLMapperBuilder实例对象的parse()方法来完成解析和注册。
  3. 解析属性为class的<mapper>元素
    使用<mapper>子元素且使用的属性是class时,这种方法和解析<package>元素方法类似,区划就是:一个解析包下的多个类,一个只解析一个类而已,所以仅前置处理不一样。直接通过configuration.addMapper()方法进行解析,而该方法其实就是直接调用了MapperRegisty的addMapper()方法实现了映射关系的解析和注册。
三、解析属性为class的<mapper>元素
1、解析过程详解

  这种方法是最基础的,解析<package>元素的解析过程都是通过多步解析之后,然后底层调用了该方法。我们首先来分析它的解析过程。
主要流程:

  1. XMLConfigBuilder.mapperElement()方法
    该方法直接执行后续addMapper()方法。
  2. configuration.addMapper()方法
    执行mapperRegistry.addMapper()方法。
  3. mapperRegistry.addMapper()方法
    1>、为MapperRegistry.knownMappers属性赋值,之后hasMapper()方法判断将返回true
    2>、执行MapperAnnotationBuilder.parse()方法
  4. MapperAnnotationBuilder.parse()方法
    判断configuration.isResourceLoaded(),返回false时,进入loadXmlResource()方法执行。
  5. MapperAnnotationBuilder.loadXmlResource()方法
    判断configuration.isResourceLoaded(),如果返回false,则执行XMLMapperBuilder.parse()方法。
  6. XMLMapperBuilder.parse()方法
    1>、首先判断configuration.isResourceLoaded()方法,如果返回false,继续执行。
    2>、通过configurationElement()方法解析Mapper配置文件中的元素。
    3>、为configration的loadedResources属性赋值,之后configuration.isResourceLoaded()方法将返回true。
    4>、进入bindMapperForNamespace()方法
  7. XMLMapperBuilder.bindMapperForNamespace()方法
    判断configuration.hasMapper()方法,如果返回false,即表示没有解析对应类的映射配置文件,则执行configuration.addMapper()方法,即又回到了第二步进行执行,否则则不执行任何有效逻辑代码。然后方法执行结束,回到了XMLMapperBuilder.parse()方法中,执行后续代码,然后又回到了loadXmlResource()方法,方法执行完后,又回到了MapperAnnotationBuilder.parse()方法中,继续执行后续的方法。在该流程中,configuration.hasMapper()方法会返回true,因为在第3步,已经通过mapperRegistry.addMapper()方法添加了对应的解析属性,所以该方法在该流程中其实是没有做任何处理的。
  8. 从第8步后,又回到了MapperAnnotationBuilder.parse()方法中,后续开始执行configuration.addLoadedResource()方法,即为configration的loadedResources属性赋值,之后configuration.isResourceLoaded()方法将返回true。然后设置MapperBuilderAssistant的currentNamespace值,即设置当前映射文件的命名空间。
  9. 执行parseCache()、parseCacheRef()方法
    这两个方法主要是用来解析对应类上@CacheNamespaceRef、@CacheNamespace 注解,需要注意的是,解析这两个注解的结果会覆盖上述第六步XMLMapperBuilder.parse()方法中configurationElement()方法解析<cache-ref>、<cache>两个元素的结果,即注解配置优先XML配置。
  10. 循环执行parseStatement()方法
    该方法主要是解析对应类上的sqlAnnotationType(@Select、@Insert、@Update、@Delete)、sqlProviderAnnotationTypes(@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider)两种类型的注解,解析结果会覆盖中上述第六步XMLMapperBuilder.parse()方法中configurationElement()方法解析select|insert|update|delete等元素节点的结果,和上一步一样,即注解配置优先XML配置。
  11. 当第10步的方法执行完后,就会回到了mapperRegistry.addMapper()方法中,执行后续方法后,然后又回到configuration.addMapper(),最后回到了XMLConfigBuilder.mapperElement()方法中,然后就完成了一个mybatis配置文件中一个mapper元素的解析过程。

  接下来,哦们详细分析每一步执行的操作逻辑,首先当resource和url都为空,且class不为空时,进入该类型的解析过程,首先获取mapperClass对应的Class实例,然后调用configuration.addMapper()方法,代码如下:

//XMLConfigBuilder.java文件中的mapperElement()方法
 Class<?> mapperInterface = Resources.classForName(mapperClass);
 configuration.addMapper(mapperInterface);

  在configuration.addMapper()方法中,调用了mapperRegistry.addMapper()方法了,上面已经提到,该方法其实就是解析和注册Mapper映射信息的核心方法。

参考文档:《binding模块之MapperProxy、MapperRegistry类》

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

  下面就详细分析mapperRegistry.addMapper()方法,该方法的逻辑如下

  1. 首先判断当前type是接口类型,否则不做任何处理。
  2. 判断是否已经解析并注册
    通过MapperRegistry的hasMapper()方法验证是否已经注册,如果已经注册,直接抛出BindingException异常。其中hasMapper()方法,实际上就是通过MapperRegistry的knownMappers字段是否包含对应type的key键来判断的,因为每次解析一个type类时,都会添加到knownMappers字段中。代码如下:
    public <T> boolean hasMapper(Class<T> type) {
        return knownMappers.containsKey(type);
    }
    
  3. 定义loadCompleted=false,表示开始解析type对应的映射文件,且表示当前还未解析完成。
  4. 把type及其对应的代理工厂类添加到knownMappers字段中
  5. MapperAnnotationBuilder.parse()方法解析配置文件
    该过程比较复杂,下面单独分析。
  6. 修改注册状态,loadCompleted = true;表示完成了该文件的解析和注册工作。
  7. 执行finally代码块
    在finally代码块中,主要是处理因异常未完成文件解析和注册工作,当时已经添加到knownMappers属性中的type,即移除knownMappers中的type即可。

addMapper()方法的完整代码如下:

//MapperRegistry.java 
/**
   * 在MyBatis初始化过程中会读取映射配置文件以及Mapper接口中的注解信息,
   * 并调用MapperRegisty.addMapper()方法填充MapperRegistry.knownMappers集合, 
   * 该集合的key是Mapper接口对应的Class对象, value为MapperProxyFactory工厂对象。
   * 
   * @param type
   */
  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值,默认为false,标识是否加载完成
      boolean loadCompleted = false;
      try {
    	//将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合
        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);
        parser.parse();
        //设置loadCompleted的值为true,表示注册完成
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {//对注册失败的类型进行清除
          knownMappers.remove(type);
        }
      }
    }
2、addMapper()方法中MapperAnnotationBuilder.parse()方法解析配置文件的逻辑

  这里主要分析上面提到的解析配置文件的逻辑,这一步是整个addMapper()方法最核心的代码,代码如下:

    //MapperRegistry.java文件addMapper()方法的代码片段
	MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
	parser.parse();

  首先构建了MapperAnnotationBuilder 实例对象,然后通过parse()方法完成解析和注册逻辑。在这里有一点儿不太好理解的是:MapperAnnotationBuilder类的命名方法,好像应该是处理注解方式的映射配置,其实不是这样子的,这里是通用的处理方式,可以处理xml方法和注解方式两种配置,只不过如果同时存在,注解配置会覆盖掉XML方式的配置。

  构建实例的代码如下:
  构建实例的过程主要完成了一下几件事:

  1. 根据type类型,预测了对应的类文件路径。即把全限定类型中“.”换成“/”,然后添加“.java”后缀生成的预测类文件路径。
  2. 然后根据该resource构建MapperBuilderAssistant实例对象,该对象主要是辅助解析映射文件中各类元素,这里不再展开分析。
  3. 为sqlAnnotationTypes和sqlProviderAnnotationTypes等赋值
//MapperAnnotationBuilder.java
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);
  }

  parse()方法
  parse()方法是完成解析和注册的核心方法。代码如下:

//MapperAnnotationBuilder.java
public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      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();
  }

  通过分析上面代码,可以知道parse()方法逻辑过程如下:

  1. 首先判断该映射文件是否被加载
    主要是configuration.isResourceLoaded()方法进行判断,其实就是在configration全局变量中维护了一个Set<String>类型的loadedResources变量,isResourceLoaded()方法就是判断该变量是否已经包括了对应的type。向loadedResources变量添加数据的方式,就是通过configuration.addLoadedResource()方法完成,即解析过一个XML映射文件,就把对应的信息记录到loadedResources变量中。

  2. loadXmlResource()方法加载对应的映射文件
    该方法的逻辑:首先根据configuration.isResourceLoaded()方法判断是否已经加载对应的资源,如果没有就根据type的全限定类型推测对应的映射文件位置,然后加载对应的文件资源生成对应的文件流对象inputStream,最后通过XMLMapperBuilder的parse()方法完成XML映射文件的真正的解析过程。XMLMapperBuilder的parse()方法后续单独分析。

    //MapperAnnotationBuilder.java
    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())) {
          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());
            xmlParser.parse();
          }
        }
      }
    
  3. 添加type到loadedResources变量,即第一步提到的添加记录操作。设置当前映射文件的命名空间,即为对应的MapperBuilderAssistant实例的currentNamespace属性赋值。

  4. 解析CacheNamespace注解
    解析CacheNamespace注解,该注解对应的对象与XML配置文件中的cache元素作用一样。如果在loadXmlResource()方法中,通过调用XMLMapperBuilder.parse()已经解析了cache元素,这个时候如果也存在对应注解的话,注解的实例对象就会覆盖XML对应,即注解配置优先于XML配置。 解析CacheNamespace注解的代码如下,最后使用MapperBuilderAssistant实例的辅助方法useNewCache()创建对应的Cache实例,并注册到configuration全局变量中。MapperBuilderAssistant的useNewCache()方法比较简单,这里不再贴出代码。

    //MapperAnnotationBuilder.java
    private void parseCache() {
        CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class);
        if (cacheDomain != null) {
          Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size();
          Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval();
          Properties props = convertToProperties(cacheDomain.properties());
          assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props);
        }
      }
    
  5. 解析CacheNamespaceRef注解
    该方法和上述第4步中解析CacheNamespace注解的作用类似,主要用来解析CacheNamespaceRef注解,且同样是注解配置优先于XML配置。解析CacheNamespaceRef注解的代码如下,最后使用MapperBuilderAssistant实例的辅助方法useCacheRef()创建对应的Cache实例,并更新configuration全局变量中对应的caches数据。MapperBuilderAssistant的useCacheRef()方法比较简单,这里不再贴出代码。

    //MapperAnnotationBuilder.java
    private void parseCacheRef() {
        CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class);
        if (cacheDomainRef != null) {
          Class<?> refType = cacheDomainRef.value();
          String refName = cacheDomainRef.name();
          if (refType == void.class && refName.isEmpty()) {
            throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef");
          }
          if (refType != void.class && !refName.isEmpty()) {
            throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef");
          }
          String namespace = (refType != void.class) ? refType.getName() : refName;
          assistant.useCacheRef(namespace);
        }
      }
    
  6. parseStatement()方法解析对应update、insert、delete、select等注解。
    该方法和上述第4、5步中解析CacheNamespace、CacheNamespaceRef注解的作用类似,parseStatement方法主要用来解析方法上对应的update、insert、delete、select等注解,且同样是注解配置优先于XML配置。解析注解的代码如下。该方法在循环处理每个注解时,出现解析异常的方法会记录到configration的全局变量incompleteMethods属性中,循环结束后,会通过parsePendingMethods()方法处理掉异常的数据记录。

 void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    if (sqlSource != null) {
      Options options = method.getAnnotation(Options.class);
      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);
    }
  }
  1. parsePendingMethods()方法处理解析异常的SQL节点。
    处理过程非常简单,即把全局变量configration的incompleteMethods属性中的记录,依次删除。

    //MapperAnnotationBuilder.java
    private void parsePendingMethods() {
        Collection<MethodResolver> incompleteMethods = configuration.getIncompleteMethods();
        synchronized (incompleteMethods) {
          Iterator<MethodResolver> iter = incompleteMethods.iterator();
          while (iter.hasNext()) {
            try {
              iter.next().resolve();
              iter.remove();
            } catch (IncompleteElementException e) {
              // This method is still missing a resource
            }
          }
        }
      }
    
3、XMLMapperBuilder.parse()方法

  在上述介绍loadXmlResource()方法的时候,提到真正加载XML配置文件的方法,其实就是在XMLMapperBuilder.parse()方法中进行的。这里就专门来分析parse()方法的用法。首先贴出代码,如下:

//XMLMapperBuilder.java
public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

  通过上面代码我们可以看到,该方法的执行的逻辑如下:

  1. 首先判断映射文件是否已经加载,如果没有加载,就执行后续方法进行加载,否则该方法就执行完成(即不执行任何有效代码)。
  2. 如果没有加载,就通过configurationElement()方法加载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"));
      buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e);
    }
  }
  1. 通过configuration.addLoadedResource();添加已加载的资源路径,用于后续判断对应Mapper映射文件是否已经加载。
  2. bindMapperForNamespace()方法
    用来判断是否已经加载了对应类的映射配置信息,如果没有加载(当仅使用注解进行配置映射信息时发生)就重新开始执行configuration.addMapper()进行加载。
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        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);
          configuration.addMapper(boundType);
        }
      }
    }
  }
四、解析<package>元素

  该类型的解析,首先是通过解析<package>元素的属性name获取表示的包名,然后通过反射机制,加载包下所有符合条件的类,然后在通过MapperRegisty的addMapper()方法实现了映射关系的解析和注册。本质上就是上述解析属性为class的元素的多次执行。具体过程如下所示:
首先,在解析<mappers>的子元素过程中,当子元素是<package>时,进入如下代码:

//XMLConfigBuilder.java文件中的mapperElement()方法
 if ("package".equals(child.getName())) {//解析<package>元素
  String mapperPackage = child.getStringAttribute("name");
   configuration.addMappers(mapperPackage);
 }

根据上述代码,可以知道,在该方法中又调用了configuration.addMappers()方法,在configuration.addMappers()方法中,又调用了mapperRegistry.addMappers()方法,具体代码如下:

//Configuration.java
public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }
//MapperRegistry.java
/**
   * 用于仅指定包名的情况下,扫描包下的每个映射器进行注册
   * @since 3.2.2
   */
  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }

在MapperRegistry的addMappers()方法中,通过重载方法addMappers(String packageName, Class<?> superType),把对应包下面的所有符合要求的类通过反射机制全部解析出来,然后再调用MapperRegistry的addMapper()方法实现具体的解析过程,该过程在上一节《解析属性为class的元素》中,已经分析,这里不再重复。

//MapperRegistry.java
/**
   * 将包下满足以superType为超类的Mapper接口及其对应的代理对象工厂注册到注册中心中
   * @since 3.2.2
   */
  public void addMappers(String packageName, Class<?> superType) {
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      addMapper(mapperClass);
    }
  }
五、解析属性为resource或url的<mapper>元素

  解析属性为resource或url的<mapper>元素时,和上面解析<package>元素和属性为class的<mapper>元素不太一样,这两者在解析的过程中,直接通过XMLMapperBuilder类的parse()方法解析对应的XML配置文件,在parse()方法中,首先通过configurationElement()方法实现XML配置的解析,然后又通过bindMapperForNamespace()方法去解析可能存在的注解配置,如果解析注解配置就回到上述解析属性为class时的<mapper>元素的流程。

入口:

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

  通过上述代码,我们可以清楚的看到,解析属性为resource或url时,都是先通过Resources获取对应映射文件的输入流,然后根据输入流,构建XMLMapperBuilder实例对象,然后在通过实例对象mapperParser的parse()方法进行解析过程。看到上面代码,我们会有一种很熟悉的感觉,因为在解析属性为class的<mapper>元素时,从第5步,执行MapperAnnotationBuilder.loadXmlResource()方法时,在loadXmlResource()方法中,就是通过构建XMLMapperBuilder实例对象,然后再通过实例对象mapperParser的parse()方法进行解析XML配置文件。

解析过程如下:

  1. XMLMapperBuilder.parse()方法
    1>、首先判断configuration.isResourceLoaded()方法,如果返回false,继续执行。
    2>、通过configurationElement()方法解析Mapper配置文件中的元素。
    3>、为configration的loadedResources属性赋值,之后configuration.isResourceLoaded()方法将返回true。
    4>、进入bindMapperForNamespace()方法
  2. XMLMapperBuilder.bindMapperForNamespace()方法
    如果解析属性为resource或url的<mapper>元素时,只有对应的映射配置文件,而没有对应的接口类,所以这个时候 Resources.classForName(namespace);对应类实例为Null,所以,在该方法中不会执行任何的有效逻辑。执行完这个方法,整个解析过程也就完成了。
private void bindMapperForNamespace() {
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        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);
          configuration.addMapper(boundType);
        }
      }
    }
  }
六、总结

  解析映射配置过程分为了XML类型映射配置和注解类型映射配置两种类型,其中注解类型映射配置又涉及到了面向接口编程的相关应用,具体可以结合本篇介绍和Mybatis的源码好好的体会。

发布了48 篇原创文章 · 获赞 3 · 访问量 3119

猜你喜欢

转载自blog.csdn.net/hou_ge/article/details/103302857
今日推荐