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

配置解析最后一篇,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){
         
     }
}

这便是桥接方法的由来.

参考链接: Java反射中method.isBridge()由来,含义和使用场景? – 木女孩的回答 – 知乎

接下来首先看加载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究竟是怎么解析的?

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


未完待续

发表评论