MyBatis 源码解析MyBatis如何解析配置 ?(五)

作者: java高级架构F六 | 来源:发表于2020-01-02 16:14 被阅读0次

    配置解析最后一篇,MyBatis解析mapper

    //    <mappers>
    //        <mapper resource="com/test/demo/mapper/CountryMapper.xml"/>-
    //        <package name="com.test.demo.mapper"/>
    //    </mappers>
    private void mapperElement(XNode parent) throws Exception {
        if (parent != null) {
          //获取所有子节点
          for (XNode child : parent.getChildren()) {
            //如果节点名称是`package` 则说明需要自动解析  
            if ("package".equals(child.getName())) {
              String mapperPackage = child.getStringAttribute("name");
              configuration.addMappers(mapperPackage);
            } else {
    
              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);
                //读取resource流   
                InputStream inputStream = Resources.getResourceAsStream(resource);
                //使用XMLMapperBuilder解析
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
                mapperParser.parse();
              } 
                //如果是以url配置的
                else if (resource == null && url != null && mapperClass == null) {
                ErrorContext.instance().resource(url);
                //读取url流
                InputStream inputStream = Resources.getUrlAsStream(url);
                XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
                mapperParser.parse();
              }
               //如果是以mapperClass配置的  
              else if (resource == null && url == null && mapperClass != null) {
                //直接读取class
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
              } else {
                //如果在一个节点中配置了多个,则抛出异常
                //类似     <mapper  resource="xx" url="xx"/> 
                throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
              }
            }
          }
        }
      }
    
    

    没什么好说的,继续往下看

    首先看处理package

    MapperRegistry###addMappers()

    //Configuration.addMappers()内部调用的便是这个方法
    public void addMappers(String packageName, Class<?> superType) {
        //这里已经很熟悉了,通过VFS读取包中所有的类
        ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
        //通过`IsA`过滤掉不符合要求的类
        resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
        Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
        //处理获取到的类
        for (Class<?> mapperClass : mapperSet) {
            addMapper(mapperClass);
        }
    }
    
    

    这里和前面的代码对比可以发现,少了一段过滤所有匿名类,接口以及内部成员类.并不是不需要过滤,而且Mapper对应点Class只需要接口即可,看后面的代码便能知道

    MapperRegistry###addMappers()

      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 {
            knownMappers.put(type, new MapperProxyFactory<>(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.
            //创建注解解析器,用来解析接口上的通过注解配置的SQL  
            MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
            //解析
            parser.parse();
            loadCompleted = true;
          } finally {
            if (!loadCompleted) {
              knownMappers.remove(type);
            }
          }
        }
      }
    
    

    可以看到这里创建了一个MapperAnnotationBuilder来解析Mapper接口上的注解,

    接口的注解分为两种:

    第一种是类似@Select()这种直接将SQL配置在接口中,这种方式的配置不灵活,所以我们暂时不分析,不过最后注册的机制可能和还是和XML配置差不多

    第二种便是常用的参数注解@Param,这种需要简单看一看

    MapperAnnotationBuilder###MapperAnnotationBuilder()

    public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
        //best guess ???
        String resource = type.getName().replace('.', '/') + ".java (best guess)";
        //获取class的加载路劲,创建Mapper组建助手
        //这里传入的configuration已经初始化差不多了,因为`Mapper`解析被放在了最后
        this.assistant = new MapperBuilderAssistant(configuration, resource);
        this.configuration = configuration;
        this.type = type;
    
    }
    
    

    这里可以看见,我们需要查看的方法主要应该就在MapperBuilderAssistant类中,我们首先看看MapperAnnotationBuilderparse()方法

    MapperAnnotationBuilder###parse()

      public void parse() 
        //获取加载的接口的具体路径/对应Mapper的命名空间
        String resource = type.toString();
        //如果没有加载过,则加载
        if (!configuration.isResourceLoaded(resource)) {
          //加载对应的`xml`文件  
          loadXmlResource();
          //在configuration对象中标记此命名空间已经加载完成 
          configuration.addLoadedResource(resource);
          //设置加载助手的命名空间  
          assistant.setCurrentNamespace(type.getName());
          //加载缓存/MyBatis中的一级缓存为一个Mapper一个缓存
          parseCache();
          //加载指定的共享缓存 
          parseCacheRef();
          //解析方法接口中的方法的注解,比如@Param  
          Method[] methods = type.getMethods();
          for (Method method : methods) {
            try {
              // 过滤掉所有的桥接方法
              if (!method.isBridge()) {
                //初始化Statement  
                parseStatement(method);
              }
            } catch (IncompleteElementException e) {
              configuration.addIncompleteMethod(new MethodResolver(this, method));
            }
          }
        }
        //解析方法
        parsePendingMethods();
      }
    
    

    上面的代码中有个isBridge(),那么isBridge()是什么意思呢?其实就是用来判断一个方法是否是桥接方法。至于什么是桥接方法,这里简单说两句:

    我们都知道Java的泛型是通过擦除实现的,对于一个泛型接口,

    public interface InterfaceA<T>{
        void methodA(T t);
    }
    
    

    如果某个类实现了这个接口,并且指定了泛型:

    public class Imple implements InterfaceA<String>{
         @override
         void methodA(String t){
             
         }
    }
    
    

    那么问题来了,经过编译以后,InterfaceA<T>中的方法经过编译后变成了methodA(Object a),但是Imple中重载的方法为methodA(String t),这根本没有重载啊。。。

    于是java编译器在编译Imple类的时候会自动生成一个桥接方法:

    public class Imple implements InterfaceA<String>{
         @override
         void methodA(Object t){
             methodA((String)t);
         }
        
         void methodA(String t){
             
         }
    }
    
    

    这便是桥接方法的由来.

    接下来首先看加载Mapper XML配置文件

    MapperAnnotationBuilder###loadXmlResource()

      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
        // 首先通过命名空间检查是否已经加载过此资源  
        //Spring-MyBatis可以通过`Mapper-Scaner`的方式加载`mapper`
        if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
          //通过类全限定名称查找相同目录下的xml文件  
          String xmlResource = type.getName().replace('.', '/') + ".xml";
          //通过class获取此文件的流  
          InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
          //如果获取失败  
          if (inputStream == null) {
            // Search XML mapper that is not in the module but in the classpath.
            try {
              //尝试通过classLoader再次获取流  
              inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
            } catch (IOException e2) {
              // ignore, resource is not required
              //忽略抛出的异常,因为可以通过注解配置SQL  
            }
          }
           //如果成功获取到,则通过`XMLMapperBuilder`解析XML文件
          if (inputStream != null) {
            XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
            xmlParser.parse();
          }
        }
      }
    
    

    第一,从type.getName()我们能明白MyBatis中命名空间与类所在的包的对应关系。这也是官方文档中所要求的对应关系。

    第二,上面查找XML文件的过程中,首先使用的是classgetResourceAsStream(),没有找到才使用的classLoader#getResourceAsStream(),区别在于class查找之前会使用resolveName()来解析路径,如果是相对路径,则解析绝对路径,再调用classLoader加载

    第三,可以注意到上面查找XML文件的方式是通过ClassLoader查找的,也就是说,不管你的XML配置在哪里,只要是classPath,都能被查找到,比如resource,甚至是%JAVA_HOME%/jre/classes/文件夹下都能被扫描到

    第四,MyBatis源码也标记了一个fix bug,用于解决Java 9之后classclassLoader加载权限的不同

    第五,这里可以看见,原生MyBatis只能加载class全限定名称的同级目录下的XML mapper,只有Spring-Mapper才增加了Mapper扫描的功能

    记下来继续看解析XML文件

    XMLMapperBuilder###parse()

      public void parse() {
        //如果没有加载过再加载,防止重复加载
        //和前面的功能一样,不知道为什么这行代码到处都是,有点像是为了集成到`Spring`中
        //将原本的代码结构破坏了一样
        if (!configuration.isResourceLoaded(resource)) {
          //获取mapper节点
          //<mapper>
          //</mapper>  
          configurationElement(parser.evalNode("/mapper"));
          //标记加载
          configuration.addLoadedResource(resource);
          //将namespace绑定在解析助手上
          bindMapperForNamespace();
        }
    
        //重新加载那些因为有加载依赖而加载失败节点  
        parsePendingResultMaps();  
        parsePendingChacheRefs();
        parsePendingStatements();
      }
    
    

    XMLMapperBuilder###configurationElement()

    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);
            //解析cache-ref配置
            cacheRefElement(context.evalNode("cache-ref"));
            //解析cache配置
            cacheElement(context.evalNode("cache"));
            //解析参数类型配置(已经废弃,最好使用@param)
            parameterMapElement(context.evalNodes("/mapper/parameterMap"));
            //解析resultMap配置
            resultMapElements(context.evalNodes("/mapper/resultMap"));
            //解析sql代码段配置
            sqlElement(context.evalNodes("/mapper/sql"));
            //通过sql语句创建statement
            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);
        }
    }
    
    

    builderAssistant.setCurrentNamespace(namespace);代码很简单,

      public void setCurrentNamespace(String currentNamespace) {
        if (currentNamespace == null) {
          throw new BuilderException("The mapper element requires a namespace attribute to be specified.");
        }
    
        if (this.currentNamespace != null && !this.currentNamespace.equals(currentNamespace)) {
          throw new BuilderException("Wrong namespace. Expected '"
              + this.currentNamespace + "' but found '" + currentNamespace + "'.");
        }
    
        this.currentNamespace = currentNamespace;
      }
    
    

    大约就是如果namespace之前被赋值了,那么就检查传入的namespace是否和期望的一致,如果不一致则报错。

    前面我们看过代码,第一次的传入在于通过class#getName()进行赋值,也就是类的全量名称

    XMLMapperBuilder###cacheRefElement()

    private void cacheRefElement(XNode context) {
        if (context != null) {
            //给configuration 设置联动缓存
            //configuration 联动缓存是通过map配置的,也就是联动缓存只能额外配置一个
            configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
            //创建缓存解析器
            CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
            try {
                //尝试加载联合缓存
                cacheRefResolver.resolveCacheRef();
            } catch (IncompleteElementException e) {
                //如果加载失败,则留在后面重新加载
                configuration.addIncompleteCacheRef(cacheRefResolver);
            }
        }
    }
    
    

    这里可以看到MyBatis是如何处理加载的先后顺序的。

    cache-ref有个问题就是解析namesapce的先后问题,如果所引用的缓存在被引用的时候还没加载,那么一般的操作都是提前去加载,这样就会涉及到分析依赖问题,加载顺序问题等,比较麻烦。

    MyBatis就直接用了一个未完成集合解决了这个问题,加载的时候发现还需要引用的缓存还没有加载,就先不暂存起来,当加载完其他配置的时候,再尝试一下加载,很方便

    可以看下具体代码:

    MapperBuilderAssistant###useCacheRef()

      public Cache useCacheRef(String namespace) {
        if (namespace == null) {
          throw new BuilderException("cache-ref element requires a namespace attribute.");
        }
        try {
          //加载成功标记  
          unresolvedCacheRef = true;
          //从configuration对象中尝试获取联合的缓存  
          Cache cache = configuration.getCache(namespace);
          //如果没有找到,说明可能当时这个缓存的XML还没有被解析
          //抛出IncompleteElementException让上层处理,上层的处理就是稍后重试
          if (cache == null) {
            throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
          }
          //如果加载成功了,则直接使用这个缓存  
          currentCache = cache;
          //标志加载成功  
          unresolvedCacheRef = false;
          return cache;
        } catch (IllegalArgumentException e) {
          throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
        }
      }
    
    

    这里可以看到,联合缓存其实就是使用的同一个缓存

    XMLMapperBuilder###cacheElement()

      private void cacheElement(XNode context) {
        if (context != null) {
          //获取缓存的实现类,如果没有设置则为`PERPETUAL`
          String type = context.getStringAttribute("type", "PERPETUAL");
          //通过别名注册器获取class
          Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
          //获取缓存清除算法,默认而最近最少使用算法
          String eviction = context.getStringAttribute("eviction", "LRU");
          //通过别名注册器获取算法使用的类
          Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
          //获取缓存刷新间隔时间配置  
          Long flushInterval = context.getLongAttribute("flushInterval");
          //获取缓存大小配置
          Integer size = context.getIntAttribute("size");
          //获取缓存是否为只读属性
          boolean readWrite = !context.getBooleanAttribute("readOnly", false);
          //新配置?文档中并没有
          boolean blocking = context.getBooleanAttribute("blocking", false);
          //获取配置的属性节点
          Properties props = context.getChildrenAsProperties();
          //通过构建助手通过这些参数创建cache
          builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
        }
      }
    
    

    MapperBuilderAssistant###useNewCache()

      public Cache useNewCache(Class<? extends Cache> typeClass,
          Class<? extends Cache> evictionClass,
          Long flushInterval,
          Integer size,
          boolean readWrite,
          boolean blocking,
          Properties props) {
        Cache cache = new CacheBuilder(currentNamespace)
            .implementation(valueOrDefault(typeClass, PerpetualCache.class))
            .addDecorator(valueOrDefault(evictionClass, LruCache.class))
            .clearInterval(flushInterval)
            .size(size)
            .readWrite(readWrite)
            .blocking(blocking)
            .properties(props)
            .build();
        //将新建的cache传入给configuration 
        //configuration所维护的cache是一个map,key为namespace  
        configuration.addCache(cache);  
        currentCache = cache;
        return cache;
      }
    
    

    看到这里发现了关于cache-ref的问题:

    • 第一,是先加载的cache-ref,再加载的cache,而且看代码,cache中新建的cache会将cache-ref中获取到其他域名空间的缓存给替换掉,也就是说,同时配置cachecache-ref会使cache-ref失效?
    • 第二,我们可以看到cache-ref除了一行currentCache = cache有点修改的意思外,其他包括返回值都没有被使用或者记录,而currentCache也会被后续的解析cache节点给覆盖掉,cache-ref究竟是怎么解析的?

    疑问先保存起来,先继续看后面的代码。


    未完待续

    相关文章

      网友评论

        本文标题:MyBatis 源码解析MyBatis如何解析配置 ?(五)

        本文链接:https://www.haomeiwen.com/subject/jusooctx.html