mybatis源码分析01:解析配置文件

249 阅读10分钟

注:本系列源码分析基于mybatis 3.5.6,源码的gitee仓库仓库地址:funcy/mybatis.

上一篇文章中,我们提供了一个mybatis使用demo,本文就基于该demo进行分析。

1. SqlSessionFactory的构建:SqlSessionFactoryBuilder#build(...)

从本节开始,就开始正式分析 mybatis 源码了,我们从SqlSessionFactory的构建入手:

SqlSessionFactory factory = builder.build(inputStream);

对应方法为SqlSessionFactoryBuilder#build(InputStream)

public SqlSessionFactory build(InputStream inputStream) {
  return build(inputStream, null, null);
}

/**
 * 具体的解析操作
 */
public SqlSessionFactory build(InputStream inputStream, String environment, 
        Properties properties) {
  try {
    XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
    // 1. parser.parse():解析配置文件
    // 2. build(...):构建 SqlSessionFactory
    return build(parser.parse());
  } catch (Exception e) {
    throw ExceptionFactory.wrapException("Error building SqlSession.", e);
  } finally {
    ErrorContext.instance().reset();
    try {
      inputStream.close();
    } catch (IOException e) {
      // Intentionally ignore. Prefer previous error.
    }
  }
}

/**
 * 构建 SqlSessionFactory
 */
public SqlSessionFactory build(Configuration config) {
  return new DefaultSqlSessionFactory(config);
}

这部分的代码比较简单,主要功能在 SqlSessionFactoryBuilder#build(InputStream, String, Properties)方法中,其实这个方法就包含两个操作:

  1. parser.parse():解析配置文件
  2. build(...):返回 SqlSessionFactory,最终返回的是SqlSessionFactory类型是DefaultSqlSessionFactory

我们主要来看看解析配置文件的操作,跟进方法XMLConfigBuilder#parse

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    parsed = true;
    // 解析配置类
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

XMLConfigBuilder#parse中又调用了parseConfiguration(...)方法,传入的值为/configuration,从这里开始就是在解析mybatis配置文件了,我们在mybatis-config.xml文件中指定的根节点就是configuration.继续进入XMLConfigBuilder#parseConfiguration方法:

/**
 *  解析`mybatis`配置文件的根节点
 */
private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfs(settings);
      loadCustomLogImpl(settings);
      typeAliasesElement(root.evalNode("typeAliases"));
      pluginElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);
      // read it after objectFactory and objectWrapperFactory issue #631
      // 解析 environments,数据源也是在这里获取的
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlerElement(root.evalNode("typeHandlers"));
      // 解析 sql 的 xml 文件
      mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }

对比下 mybatis 配置文件里支持的节点(可参考xml配置):

可以看到,这里正是解析mybatis配置文件的各节点。

这里解析的内容比较多,我们主要关注两个:

  1. 解析数据源(datasource
  2. 解析映射器(mappers

2. 解析数据源

配置文件中 ,数据源的配置在environments节点下,对应的方法为XMLConfigBuilder#environmentsElement

private void environmentsElement(XNode context) throws Exception {
  if (context != null) {
    if (environment == null) {
      environment = context.getStringAttribute("default");
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {
        // 解析事务管理器
        TransactionFactory txFactory = transactionManagerElement(
            child.evalNode("transactionManager"));
        // 获取数据源
        DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
        DataSource dataSource = dsFactory.getDataSource();
        Environment.Builder environmentBuilder = new Environment.Builder(id)
            .transactionFactory(txFactory)
            .dataSource(dataSource);
        configuration.setEnvironment(environmentBuilder.build());
      }
    }
  }
}

继续跟进XMLConfigBuilder#dataSourceElement方法:

private DataSourceFactory dataSourceElement(XNode context) throws Exception {
    if (context != null) {
      String type = context.getStringAttribute("type");
      // 处理节点下的配置
      Properties props = context.getChildrenAsProperties();
      // 得到数据源工厂
      DataSourceFactory factory = (DataSourceFactory) resolveClass(type)
            .getDeclaredConstructor().newInstance();
      factory.setProperties(props);
      return factory;
    }
    throw new BuilderException("Environment declaration requires a DataSourceFactory.");
}

这个解析还是处理 xml 文件,会获取dataSource节点下的所有配置,然后将这些配置转化为Properties对象,得到的内容如下:

然后根据dataSource节点配置的type值初始化对应的数据源工厂,这里配置的的type值为POOLED

<dataSource type="POOLED">

对应的DataSourceFactoryorg.apache.ibatis.datasource.pooled.PooledDataSourceFactory,最终得到的对象为:

这里注意到PooledDataSourceFactory,还有个UnpooledDataSource,这两者的区别就是连接池与非连接池的区别了,这里就不多作分析了。

再回到XMLConfigBuilder#environmentsElement方法,得到的数据源会设置到configuration中,这一步得到的configuration如下:

可以看到,environment 已经有值了,数据源也在其中了。

3 解析映射器

解析映射器的方法为XMLConfigBuilder#mapperElement,代码如下:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 处理包的加载方式
      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);
          InputStream inputStream = Resources.getResourceAsStream(resource);
          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();
        //  处理文件方式的加载
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);
        } else {
          throw new BuilderException("...");
        }
      }
    }
  }
}

这里需要说明下,mybatis加载sql映射语句有几种方式(内容来自于 mybatis文档-映射器):

<!-- 使用相对于类路径的资源引用 -->
<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>

上述内容中介绍了4种加载sql映射语句,这里我们重点关注两种方式:

  1. 使用相对于类路径的资源引用(resource 加载)
  2. 将包内的映射器接口实现全部注册为映射器(package 加载)

3.1 处理 resource 加载方式

处理 resource 加载方式的代码如下:

private void mapperElement(XNode parent) throws Exception {
    if (parent != null) {
      for (XNode child : parent.getChildren()) {
        // 处理包的加载方式
        if ("package".equals(child.getName())) {
          ...
        } 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);
            // 转换为输入流
            InputStream inputStream = Resources.getResourceAsStream(resource);
            // 解析输入流
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, 
                    resource, configuration.getSqlFragments());
            mapperParser.parse();
          }
          ...
        }
      }
    }
}

从代码来看,mybatis会把resource路径读取为输入流,然后使用xml解析来处理输入流,这与前面解析mybatis-config.xml配置文件的方式一致。我们进入XMLMapperBuilder#parse

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      // 解析mapper节点  
      configurationElement(parser.evalNode("/mapper"));
      configuration.addLoadedResource(resource);
      bindMapperForNamespace();
    }

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

我们先来看看mapper节点的解析,进入XMLMapperBuilder#configurationElement方法:

  private void configurationElement(XNode context) {
    try {
      String namespace = context.getStringAttribute("namespace");
      if (namespace == null || namespace.isEmpty()) {
        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"));
      // 解析 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);
    }
  }

这里就是解析mapper.xml文件里的标签了,关于这个文件内容的配置,可以参考 mybatis xml映射器.

我们重点关注sql的解析,方法入口为XMLMapperBuilder#buildStatementFromContext(List<XNode>),一路跟下去,最终到了XMLStatementBuilder#parseStatementNode:

public void parseStatementNode() {
    String id = context.getStringAttribute("id");
    String databaseId = context.getStringAttribute("databaseId");

    if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
      return;
    }

    String nodeName = context.getNode().getNodeName();
    SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
    boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
    boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
    boolean useCache = context.getBooleanAttribute("useCache", isSelect);
    boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);

    // Include Fragments before parsing
    XMLIncludeTransformer includeParser 
            = new XMLIncludeTransformer(configuration, builderAssistant);
    includeParser.applyIncludes(context.getNode());

    String parameterType = context.getStringAttribute("parameterType");
    Class<?> parameterTypeClass = resolveClass(parameterType);

    String lang = context.getStringAttribute("lang");
    LanguageDriver langDriver = getLanguageDriver(lang);

    // Parse selectKey after includes and remove them.
    processSelectKeyNodes(id, parameterTypeClass, langDriver);

    // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
    KeyGenerator keyGenerator;
    String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
    keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
    if (configuration.hasKeyGenerator(keyStatementId)) {
      keyGenerator = configuration.getKeyGenerator(keyStatementId);
    } else {
      keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
          configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
          ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
    }

    // 在这里解析sql内容
    SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
    StatementType statementType = StatementType.valueOf(
            context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
    Integer fetchSize = context.getIntAttribute("fetchSize");
    Integer timeout = context.getIntAttribute("timeout");
    String parameterMap = context.getStringAttribute("parameterMap");
    String resultType = context.getStringAttribute("resultType");
    Class<?> resultTypeClass = resolveClass(resultType);
    String resultMap = context.getStringAttribute("resultMap");
    String resultSetType = context.getStringAttribute("resultSetType");
    ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
    if (resultSetTypeEnum == null) {
      resultSetTypeEnum = configuration.getDefaultResultSetType();
    }
    String keyProperty = context.getStringAttribute("keyProperty");
    String keyColumn = context.getStringAttribute("keyColumn");
    String resultSets = context.getStringAttribute("resultSets");

    builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
        fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
        resultSetTypeEnum, flushCache, useCache, resultOrdered,
        keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
  }

这个方法看着长,实际上还是在解析sql语句的内容,这块内容就不多说了。

解析到xml文件里的内容后,会把参数传给builderAssistant.addMappedStatement(...)方法,这个方法的参数真不是一般的多,我们简单地看下他最终做了什么:

public MappedStatement addMappedStatement(...) {
    // 省略参数组装
    ...

    // 将解析到的sql语句(select,insert,update,delete)添加到configuration中
    MappedStatement statement = statementBuilder.build();
    configuration.addMappedStatement(statement);
    return statement;
}

这一步是将statement添加到configuration了,这里的statement类型为MappedStatement,这是对sql内容的封装,得到的statement

具体的sql语句在MappedStatement.sqlSource中:

sql的解析是在XMLScriptBuilder#parseScriptNode中处理的,这里就不多说了。

添加到configuration后,Configuration#mappedStatements内容如下:

Configuration#mappedStatements是一个map,key 是"包名.类名.方法名",value 是 MappedStatement,key表示了xml中唯一的方法,从这里可以看出,xml 中的方法并不支持重载

到这里,mapper.xml文件就解析好了,那么UserMapper.java何时处理呢?我们再回到XMLMapperBuilder#parse方法:

  public void parse() {
    if (!configuration.isResourceLoaded(resource)) {
      ...
      // 绑定 Namespace
      bindMapperForNamespace();
    }
    ...
  }

进入XMLMapperBuilder#bindMapperForNamespace方法:

  private void bindMapperForNamespace() {
    // 拿到 namespace,这里的 namespace 就是 mapper.java 文件的 "包名.类名"
    String namespace = builderAssistant.getCurrentNamespace();
    if (namespace != null) {
      Class<?> boundType = null;
      try {
        boundType = Resources.classForName(namespace);
      } catch (ClassNotFoundException e) {
        // ignore, bound type is not required
        // 代码中忽略了报错信息,也就是说如果仅有 mapper.xml 文件但没 mapper.java,代码也不报错
      }
      if (boundType != null && !configuration.hasMapper(boundType)) {
        configuration.addLoadedResource("namespace:" + namespace);
        // 继续看这里的操作
        configuration.addMapper(boundType);
      }
    }
  }

继续跟进MapperRegistry#addMapper方法:

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

最终调用的是MapperRegistry#addMapper方法,关于这个方法,我们在分析处理 package 加载方式一节再分析。

3.2 处理 package 加载方式

让我们回到XMLConfigBuilder#mapperElement,处理 package 加载方式的方法如下:

private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    for (XNode child : parent.getChildren()) {
      // 处理包的加载方式
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);
      }
      ...
    }
  }
}

进入Configuration#addMappers(java.lang.String)方法:

  public void addMappers(String packageName) {
    mapperRegistry.addMappers(packageName);
  }

继续进入MapperRegistry#addMappers(String)

  public void addMappers(String packageName) {
    addMappers(packageName, Object.class);
  }

  public void addMappers(String packageName, Class<?> superType) {
    // 根据包名获取包下的所有类
    ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
    resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
    Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
    for (Class<?> mapperClass : mapperSet) {
      // 处理添加操作
      addMapper(mapperClass);
    }
  }

这一块操作很清晰,就是获取包名下的所有类,然后遍历,对每一个类逐一调用MapperRegistry#addMapper(Class)方法。在分析处理 resource 加载方式时,从xml文件中拿到namespace后,也会调用MapperRegistry#addMapper(Class)方法,我们跟进去看看这个方法做了什么:

  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 中,创建的是 MapperProxyFactory 对象
        knownMappers.put(type, new MapperProxyFactory<>(type));
        MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
        // 在这里会解析 xml 文件,当然了,解析前会先判断是否已经解析过了,解析过就不再解析
        parser.parse();
        loadCompleted = true;
      } finally {
        if (!loadCompleted) {
          knownMappers.remove(type);
        }
      }
    }
  }

MapperRegistry中,有一个MapMap<Class<?>, MapperProxyFactory<?>> knownMappers,该Map用来存放MapperProxyFactory,我们来看看MapperProxyFactory有啥:

public class MapperProxyFactory<T> {
  // Mapper.java 
  private final Class<T> mapperInterface;
  // 方法调用缓存
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();

  ...
}

MapperProxyFactory从名字来看,是Mapper 代理工厂Mapper实例在这里产生,其中有一个成员变量mapperInterface保存Mapper的类型,另一个成员变量methodCache存放方法缓存。

我们再回到MapperRegistry#addMapper(Class)方法,这个方法关键地方有两处:

  1. 添加MapperProxyFactory 对象knownMappers
  2. 生成MapperAnnotationBuilder并解析

关于第1点,就是根据传入的Mapper类型生成MapperProxyFactory对象,然后保存到MapperRegistryknownMappers,该结构是一个Map,关于MapperProxyFactory对象的妙用,后面会再分析。

我们重点关注MapperAnnotationBuilder的解析,方法为MapperAnnotationBuilder#parse:

public void parse() {
    String resource = type.toString();
    if (!configuration.isResourceLoaded(resource)) {
      // 加载xml文件
      loadXmlResource();
      configuration.addLoadedResource(resource);
      assistant.setCurrentNamespace(type.getName());
      parseCache();
      parseCacheRef();
      // 解析方法上的注解
      for (Method method : type.getMethods()) {
        if (!canHaveStatement(method)) {
          continue;
        }
        // 解析 @select 注解
        if (getAnnotationWrapper(method, false, Select.class, SelectProvider.class).isPresent()
            && method.getAnnotation(ResultMap.class) == null) {
          parseResultMap(method);
        }
        try {
          parseStatement(method);
        } catch (IncompleteElementException e) {
          configuration.addIncompleteMethod(new MethodResolver(this, method));
        }
      }
    }
    parsePendingMethods();
}

这一块会解析xml文件,同时也会解析方法上的注解。在实际项目中,mybatis 的注解方式使用并不多,本文就不分析了,我们重点还是来看xml解析,进入MapperAnnotationBuilder#loadXmlResource

private void loadXmlResource() {
  // 判断xml是否解析过,如果已解析过,就不再解析
  if (!configuration.isResourceLoaded("namespace:" + type.getName())) {
    // 获取xml文件路径
    String xmlResource = type.getName().replace('.', '/') + ".xml";
    // #1347
    InputStream inputStream = type.getResourceAsStream("/" + xmlResource);
    if (inputStream == null) {
      try {
        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());
      // 解析xml
      xmlParser.parse();
    }
  }
}

这个方法所做工作并不多,先是判断xml是否解析过了,解析过了就不再处理了,然后将类名转换为xml路径名,如Mapper的名称为org.apache.ibatis.demo.mapper.UserMapper,转换后得到的xml路径名为org/apache/ibatis/demo/mapper/UserMapper.xml,也就是说,这种方式下,Mapper与对应的xml要放在同一包下才能被解析到。

具体解析xml的方法为XMLMapperBuilder#parse,在前面分析 resource 加载方式时,使用的也是这个方法来解析xml的,这里就不于分析了。

最后,我们来小结下这两种加载方式的区别:resource 加载方式 会先解析xml,然后由xml文件中的namespace加载Mapper.java;而 package 加载方式会根据Mapper.java找到对应的xml然后进行解析。

4. 总结

本文主要分析了mybatis解析配置文件的流程,其中重点分析了数据源的解析及映射器的解析。

  1. 数据源在配置文件的environments节点下配置,解析后得到的类为Environment,其中包含了数据源的配置;
  2. 映射器的配置共有4种,本文重点介绍了两种:resource 加载方式与package 加载方式,resource 加载方式 会先解析xml,然后由xml文件中的namespace加载Mapper.java;而 package 加载方式会根据Mapper.java找到对应的xml然后进行解析;
  3. mapper.xml文件中的select|insert|delete|update语句会被解析成一个个MappedStatementMappedStatement中的成员变量id的值为包名.类名.方法名Mapper.java类会被包装成MapperProxyFactory,其成员变量mapperInterface就是Mapper的类型;
  4. 最终配置文件会被解析为Configuration类,这个类包含了mybatis配置文件的所有内容。

本文原文链接:my.oschina.net/funcy/blog/… ,限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。