Mybatis源码分析(三):mapper.xml的解析及namespace与Mapper接口的映射

版权声明:非商业目的可自由转载,转载请标明出处 https://blog.csdn.net/u010013573/article/details/87971650

概述

  • 由上一篇文章:Mybatis源码分析(二):SqlSessionFactory与框架启动加载
    分析可知,在调用SqlSessionFactoryBuilder的build方法创建SqlSessionFactory对象实例时,会首先调用builder包的xml子包的XMLConfigBuilder解析mybatisConfig.xml文件并创建和保存配置信息到Configuration对象,然后使用该Configuration对象作为参数创建SqlSessionFactory对象实例。
  • XMLConfigBuilder在解析mybatisConfig.xml文件时,会解析mappers节点,其中mappers节点主要用于配置mapper.xml或者mapper接口,然后将每个mapper注册到Configuration的MapperRegistry中,MapperRegistry在内部维护了mapper接口和对应的mapperProxy代理对象的映射。
  • 应用代码在调用mapper接口的方法时,在mybatis内部是通过该mapper接口对应的mapperProxy代理对象来进行方法调用的,即调用执行对应的SQL。

XMLConfigBuilder解析mappers节点

  • XMLConfigBuilder内部定义了mapperElement方法来对mybatisConfig.xml文件的mappers节点的解析,方法定义如下:mappers节点支持配置package子节点和mapper子节点,其中package对应mapper接口所在的包,mapper节点对应一个具体的mapper接口。

    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          for (XNode child : parent.getChildren()) {
            // 查找指定包package下面的mapper接口
            // 如下:
            // <mappers>
            //    <package name="com/yzxie/demo/mapper"/>
            // </mappers>
            if ("package".equals(child.getName())) {
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
              // mapper节点
              // <mappers>
              //  <mapper resource="org/mybatis/builder/AuthorMapper.xml"/> <!-- 使用相对于类路径的资源引用 -->
              //  <mapper url="file:///var/mappers/BlogMapper.xml"/> <!-- 使用完全限定资源定位符(URL) -->
              //  <mapper class="org.mybatis.builder.PostMapper"/> <!-- 使用映射器接口实现类的完全限定类名 -->
              // </mappers>
              String resource = child.getStringAttribute("resource");
              String url = child.getStringAttribute("url");
              String mapperClass = child.getStringAttribute("class");
              // 通过resource指定
              if (resource != null && url == null && mapperClass == null) {
                ErrorContext.instance().resource(resource);
                InputStream inputStream = Resources.getResourceAsStream(resource);
                // 解析mapper节点
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                mapperParser.parse();
              // 通过url
              } 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();
              // 通过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.");
              }
            }
          }
        }
    }
    
    1. 首先解析package子节点,在内部从类路径下,自动加载该package下面的mapper接口;
    2. 如果是mapper子节点,则mapper接口可能通过resource,url,class三种方式来指定加载该mapper接口的方式。

package子节点的解析

  • 对应package子节点的解析主要是通过Configuration的addMappers方法来执行的,其中参数为package包名。在Configuration的addMappers方法中,是通过MapperRegistry的addMappers方法来执行该package在类路径下的mapper接口的加载的。

    public void addMappers(String packageName, Class<?> superType) {
        // 通过ResolverUtil获取指定包下面的类集合
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        // 获取mapper接口对应的类对象集合
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        for (Class<?> mapperClass : mapperSet) {
          // 添加指定的mapper节点
          addMapper(mapperClass);
        }
    }
    
    

XMLMapperBuidler解析mapper子节点

  • 对应mapper子节点,则是通过builder包的xml子包的XMLMapperBuilder的parse方法来执行节点解析的:

    public void parse() {
        if (!configuration.isResourceLoaded(resource)) {
          // mapper.xml文件解析
          configurationElement(parser.evalNode("/mapper"));
          configuration.addLoadedResource(resource);
          
          // 根据mapper.xml的namespace对应的mapper接口,生成对应的MapperProxy代理对象,并保存到Configuration的mapperRegistry中
          // 在mapperRegistry的knownMappers中,以该mapper接口的类对象作为key,MapperProxyFactory对象作为value
          bindMapperForNamespace();
        }
    
        // 处理incomplement相关的元素,主要是在上面步骤执行发生异常时产生的
        // resultMap
        parsePendingResultMaps();
        // cacheRef
        parsePendingCacheRefs();
        parsePendingStatements();
    }
    
    1. 调用configurationElement方法来解析mapper.xml文件内部的各个节点,实现如下:

      // 从mapper标签开始解析mapper.xml,主要为解析mapper.xml内部的各个节点
      private void configurationElement(XNode context) {
          try {
            // mapper自身属性:namespace, cache-ref, cache
            String namespace = context.getStringAttribute("namespace");
            if (namespace == null || namespace.equals("")) {
              throw new BuilderException("Mapper's namespace cannot be empty");
            }
      
            // 设置当前的namespace
            builderAssistant.setCurrentNamespace(namespace);
      
            // 缓存
            cacheRefElement(context.evalNode("cache-ref"));
            cacheElement(context.evalNode("cache"));
      
            // mapper -> parameterMap子节点
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      
            // mapper -> resultMap子节点
            resultMapElements(context.evalNodes("/mapper/resultMap"));
      
            // mapper -> sql子节点
            sqlElement(context.evalNodes("/mapper/sql"));
      
            // mapper -> select/insert/update/delete子节点
            // 对SQL语句操作相关的每个SQL标签生成对应的XMLStatementBuilder保存到configuration的incompleteStatements集合中
            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);
          }
      }
      
    2. 调用bindMapperForNamespace来生成mapper.xml对应的mapper接口的MapperProxy代理对象。根据mapper.xml的namespace对应的mapper接口,生成对应的MapperProxy代理对象,并保存到Configuration的mapperRegistry中。在mapperRegistry的knownMappers中,以该mapper接口的类对象作为key,MapperProxyFactory对象作为value。

      private void bindMapperForNamespace() {
          // 在通过上一步configurationElement方法对mapper.xml文件解析时已经设置好该namespace,
          // 即mapper接口的全限定名
          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);
                
                // 创建该mapper接口对应的MapperProxy代理对象,
                // 然后保存到Configuration的mapperRegistry的knownMappers中
                configuration.addMapper(boundType);
              }
            }
          }
      }
      

总结

由上面分析可知,在mybatis框架启动,即创建SqlSessionFactory对象实例时,通过解析mybatisConfig.xml和mapper.xml文件,将对应的配置信息和mapper信息保存到Configuration对象实例中,如mapper接口与代理对象MapperProxy映射保存到了Configuration的mapperRegistry的knownMappers集合中,SQL信息保存到了Configuration的mappedStatements集合中等,故后续应用代码执行,进行mapper接口的方法调用时,则可以直接通过Configuration获取相关信息,然后执行SQL请求。

猜你喜欢

转载自blog.csdn.net/u010013573/article/details/87971650