mybatis(一):加载和解析配置文件

在这里记录一下自己学习mybatis源码过程中的一些学习体会,文章内容基于mybatis3.5.3-SNAPSHOT:

下面是mybatis一个测试用例中配置文件的截图,配置文件详情参考mybatis中文官网:

image

1.事例

下面是mybatis测试用例中加载配置文件,并且运行的过程,这篇文章主要记录一下mybatis加载配置文件的过程

@BeforeAll
static void setUp() throws Exception {
// create a SqlSessionFactory
try (Reader reader = Resources.getResourceAsReader("org/apache/ibatis/submitted/permissions/mybatis-config.xml")) {
  sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
}

// populate in-memory database
BaseDataTest.runScript(sqlSessionFactory.getConfiguration().getEnvironment().getDataSource(),
        "org/apache/ibatis/submitted/permissions/CreateDB.sql");
}

从以上的实例代码可以看到关于mybatis读取默认配置文件的过程,接下来就是详细的看看整体的过程。

2.源码分析

2.1创建SqlSessionFactory

SqlSession是mybatis的关键,这个接口包含了sql执行,事务,缓存等许多的方法。要获取SqlSession就要先得到SqlSessionFactory。为了得到SqlSessionFactory就需要使用SqlSessionFactoryBuilder来解析配置文件,SqlSessionFactoryBuilder有多个build方法,基本一致,挑一个来看看。

public SqlSessionFactorybuild(Reader reader, String environment, Properties properties) {

try {
    // 创建 XMLConfigBuilder 对象,底层使用的是jdk的XPath解析xml文件
    XMLConfigBuilder parser =new XMLConfigBuilder(reader, environment, properties);
    // 执行 XML 解析
    // 创建 DefaultSqlSessionFactory 对象
    return build(parser.parse());
  }catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  }finally {
ErrorContext.instance().reset();
    try {
reader.close();
    }catch (IOException e) {
    // Intentionally ignore. Prefer previous error.
    }
}

}

2.2 解析配置文件

下面我们来看看parser.parse():

public Configurationparse() {
  // 判断是否已经加载过
  if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
  }
  parsed =true;
  // 解析configuration节点
  parseConfiguration(parser.evalNode("/configuration"));
  return configuration;
}

最重要的是parseConfiguration方法:

private void parseConfiguration(XNode root) {
try {
    //issue #117 read properties first
    // 属性
    propertiesElement(root.evalNode("properties"));
    // 设置,这是 MyBatis 中极为重要的调整设置,它们会改变 MyBatis 的运行时行为
    Properties settings = settingsAsProperties(root.evalNode("settings"));
    // 加载自定义 VFS 实现类
    loadCustomVfs(settings);
    // 指定 MyBatis 所用日志的具体实现,未指定时将自动查找
    loadCustomLogImpl(settings);
    // 类型别名,为 Java 类型设置一个短的名字
    typeAliasesElement(root.evalNode("typeAliases"));
    // 插件,在已映射语句执行过程中的某一点进行拦截调用
    pluginElement(root.evalNode("plugins"));
    // 对象工厂,MyBatis 每次创建结果对象的新实例时,它都会使用一个对象工厂实例来完成
    objectFactoryElement(root.evalNode("objectFactory"));
    // 对象包装工厂
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
    // 反射工厂
    reflectorFactoryElement(root.evalNode("reflectorFactory"));
    // 设置settings属性到configuration中,没有时设置默认配置
    settingsElement(settings);
    // read it after objectFactory and objectWrapperFactory issue #631
    // 环境配置
    environmentsElement(root.evalNode("environments"));
    // 数据库厂商标识
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));
    // 类型处理器,MyBatis 在预处理语句(PreparedStatement)中设置一个参数时,
    // 还是从结果集中取出一个值时, 都会用类型处理器将获取的值以合适的方式转换成 Java 类型
    typeHandlerElement(root.evalNode("typeHandlers"));
    // SQL 映射语句
    mapperElement(root.evalNode("mappers"));
  }catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

大多数都是属性的设置,最终所有的设置都会配置到XMLConfigBuilder以及父类BaseBuilder的属性对象中,其中mapperElement方法是解析mapper.xml,即我们的mapper.xml文件或者*mapper.java接口(针对在java文件中通过注解创建sql和加上一些配置等)。

2.3 xml文件以及接口解析

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 如果是配置的package那就扫描包,针对已经在方法上使用注解实现功能
        <1>
        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");
          // 解析本地的xml文件
          <2>
          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();
          }
          // 解析远程地址上的xml文件
          <3>
          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();
          }
          // 单个文件解析,也是针对已经在方法上使用注解实现功能
          <4>
          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>,<2>,<3>,<4>处的代码,最终的解析方式都是解析解析xml的同时解析对应的接口内的方法,或者是先解析接口内的方法再解析接口对应的xml文件
configuration.addMappers,先来看下MapperRegistry.addMapper方法:

public  void addMapper(Class 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.
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        <1>
        parser.parse();
        loadCompleted = true;
      } finally {
        // 解析错误,留到后面解析
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

<1>处代码最关键

public void parse() {
    String resource = type.toString();
    // 判断是否加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 加载对应的*mapper.xml文件
      <1>
      loadXmlResource();
      // 用于判断是否加载
      configuration.addLoadedResource(resource);
      // 设置当前命名空间,如果与当前命名空间不一致,抛出错误
      // 我理解可能是防止多线程下同时解析不同文件
      assistant.setCurrentNamespace(type.getName());
      // 解析@CacheNamespace,二级缓存相关
      parseCache();
      // 解析@CacheNamespaceRef,二级缓存相关
      parseCacheRef();
      Method[] methods = type.getMethods();
      // 遍历每个方法,解析其上的注解
      for (Method method : methods) {
        try {
          // issue #237
          if (!method.isBridge()) {
            <2>
            parseStatement(method);
          }
        } catch (IncompleteElementException e) {
          // 解析失败,添加到 configuration 中
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    // 解析上面for循环解析失败的方法
    parsePendingMethods();
  }

其中<1>处代码是解析xml文件的,<2>处代码是解析对应的java接口
先来看看<1>处代码是怎么找到并且解析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的路径
      String xmlResource = type.getName().replace('.', '/') + ".xml";
      // #1347
      // 获取当前模块中的xml文件流对象
      InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
      if (inputStream == null) {
        // Search XML mapper that is not in the module but in the classpath.
        try {
          // 获取不在当前模块,但是在对应路径下的xml文件
          inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource);
        } catch (IOException e2) {
          // ignore, resource is not required
        }
      }
      if (inputStream != null) {
        XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName());
        xmlParser.parse();
      }
    }
  }

继续来看看XMLMapperBuilder.parse()是如何解析xml文件的。

2.3.1解析xml

public void parse() {
    // 如果没有加载过
    if (!configuration.isResourceLoaded(resource)) {
      // 解析xml文件中的所有标签
      <1>
      configurationElement(parser.evalNode("/mapper"));
      // 标记该 Mapper 已经加载过
      configuration.addLoadedResource(resource);
      // 解析对应的*mapper.java文件,
      // 解析xml或者java文件的时候都会去解析对应的另外一个文件
      // 在解析对应的文件时都要判断是否已经解析过
      bindMapperForNamespace();
    }

    // 解析待定的  节点
    parsePendingResultMaps();
    // 解析待定的  节点
    parsePendingCacheRefs();
    // 解析待定的 SQL 语句的节点
    parsePendingStatements();
  }

重点看看<1>处的代码,是如何解析整个xml文件中的所有节点的,最后面的3个方法是继续尝试解析前面解析xml文件时没有解析成功的节点。

private void configurationElement(XNode context) {
    try {
      // 获得 namespace 属性
      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"));
      // 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。
      parameterMapElement(context.evalNodes("/mapper/parameterMap"));
      // <1> 解析  节点们,解析成resultMap对象保存在 configuration 中
      resultMapElements(context.evalNodes("/mapper/resultMap"));
      // 解析  节点们,保存id和node对应关系到 sqlFragments 中
      sqlElement(context.evalNodes("/mapper/sql"));
      // <2> 解析 
    String id = context.getStringAttribute("id");
    // 例如