配置解析与Configuration

配置解析与 Configuration:一份 XML 怎么变成全局注册中心

MyBatis 启动时只做一件大事:把 mybatis-config.xml(以及它引用的所有 mapper)解析成一个 Configuration 对象。
此后运行期不再碰 XML,一切查找(SQL 在哪、结果怎么映射、插件有哪些)都是查这个对象里的 Map。
入口链路:SqlSessionFactoryBuilder.build → XMLConfigBuilder.parse → parseConfiguration 逐节点解析,产物全部挂到 Configuration 上。

代码块JAVA · 81 行收起展开
// 基于本地 MyBatis 仓 (JavaSourceReadingLab, 3.5.x)
// org.apache.ibatis.session.SqlSessionFactoryBuilder
// org.apache.ibatis.builder.xml.XMLConfigBuilder
public class SqlSessionFactoryBuilder {

  // ... 省略 Reader/Properties 等 8 个重载,全部收敛到这一个

  public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
      XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
      return build(parser.parse());          // parse() 返回 Configuration,XML 的使命到此为止
    } catch (Exception e) {
      throw ExceptionFactory.wrapException("Error building SqlSession.", e);
    } finally {
      ErrorContext.instance().reset();       // ErrorContext 是 ThreadLocal,解析报错时拼出"哪个文件哪个节点"
      try {
        if (inputStream != null) {
          inputStream.close();               // 流由 Builder 负责关,调用方传进来就不用管了
        }
      } catch (IOException e) {
        // Intentionally ignore. Prefer previous error.
      }
    }
  }

  public SqlSessionFactory build(Configuration config) {
    return new DefaultSqlSessionFactory(config);   // 工厂本身极薄,就是持有一个 Configuration
  }
}

public class XMLConfigBuilder extends BaseBuilder {

  private boolean parsed;
  private final XPathParser parser;          // 封装 DOM + XPath,evalNode("/configuration") 这类查询靠它
  private String environment;

  // ... 省略 8 个重载构造器,最终都走到这个私有构造

  private XMLConfigBuilder(Class<? extends Configuration> configClass, XPathParser parser, String environment,
      Properties props) {
    super(newConfig(configClass));           // 在这里 new Configuration()!Builder 从头到尾只往里填东西
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);  // 代码里传入的 Properties 先存着,优先级最高(见 propertiesElement)
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
  }

  public Configuration parse() {
    if (parsed) {
      throw new BuilderException("Each XMLConfigBuilder can only be used once.");  // 一次性对象:解析有副作用,重复跑会重复注册
    }
    parsed = true;
    parseConfiguration(parser.evalNode("/configuration"));
    return configuration;
  }

  // 解析顺序是写死的,且顺序本身就是设计:
  private void parseConfiguration(XNode root) {
    try {
      // issue #117 read properties first
      propertiesElement(root.evalNode("properties"));    // 最先解析:后面所有节点里的 ${} 占位符都要靠它替换
      Properties settings = settingsAsProperties(root.evalNode("settings"));
      loadCustomVfsImpl(settings);
      loadCustomLogImpl(settings);                       // 日志实现要尽早生效,否则解析期的日志用错实现
      typeAliasesElement(root.evalNode("typeAliases"));  // 别名先注册,后面 plugins/environments 的 type 属性才能写短名
      pluginsElement(root.evalNode("plugins"));
      objectFactoryElement(root.evalNode("objectFactory"));
      objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
      reflectorFactoryElement(root.evalNode("reflectorFactory"));
      settingsElement(settings);                         // settings 真正落到 Configuration 的 setter 上
      // read it after objectFactory and objectWrapperFactory issue #631
      environmentsElement(root.evalNode("environments"));
      databaseIdProviderElement(root.evalNode("databaseIdProvider"));
      typeHandlersElement(root.evalNode("typeHandlers"));
      mappersElement(root.evalNode("mappers"));          // 压轴:最重的活,触发所有 Mapper XML/接口的解析
    } catch (Exception e) {
      throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
  }
}

关键的几个节点解析方法,每个套路一致:读属性 → resolveClass/反射实例化 → 塞进 configuration。

代码块JAVA · 131 行收起展开
// 基于本地 MyBatis 仓 (JavaSourceReadingLab, 3.5.x), org.apache.ibatis.builder.xml.XMLConfigBuilder
  private void propertiesElement(XNode context) throws Exception {
    if (context == null) {
      return;
    }
    Properties defaults = context.getChildrenAsProperties();     // ① XML 里内联的 <property>,优先级最低
    String resource = context.getStringAttribute("resource");
    String url = context.getStringAttribute("url");
    if (resource != null && url != null) {
      throw new BuilderException(
          "The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
    }
    if (resource != null) {
      defaults.putAll(Resources.getResourceAsProperties(resource));  // ② 外部文件覆盖内联值
    } else if (url != null) {
      defaults.putAll(Resources.getUrlAsProperties(url));
    }
    Properties vars = configuration.getVariables();
    if (vars != null) {
      defaults.putAll(vars);                 // ③ build() 时代码传入的 Properties 最后 putAll,优先级最高
    }
    parser.setVariables(defaults);           // 回填给 XPathParser:从此之后解析到的 ${} 都能被替换
    configuration.setVariables(defaults);    // 同时存进 Configuration,后续 mapper 解析也用同一份
  }

  private Properties settingsAsProperties(XNode context) {
    if (context == null) {
      return new Properties();
    }
    Properties props = context.getChildrenAsProperties();
    // Check that all settings are known to the configuration class
    MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
    for (Object key : props.keySet()) {
      if (!metaConfig.hasSetter(String.valueOf(key))) {   // 用反射元信息校验 setting 名:拼错直接启动失败,不静默吞掉
        throw new BuilderException(
            "The setting " + key + " is not known.  Make sure you spelled it correctly (case sensitive).");
      }
    }
    return props;
  }

  private void typeAliasesElement(XNode context) {
    if (context == null) {
      return;
    }
    for (XNode child : context.getChildren()) {
      if ("package".equals(child.getName())) {            // 包扫描注册 和 逐个注册 两种写法
        String typeAliasPackage = child.getStringAttribute("name");
        configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
      } else {
        String alias = child.getStringAttribute("alias");
        String type = child.getStringAttribute("type");
        try {
          Class<?> clazz = Resources.classForName(type);
          if (alias == null) {
            typeAliasRegistry.registerAlias(clazz);       // 不写 alias 就用类的简单名(小写)
          } else {
            typeAliasRegistry.registerAlias(alias, clazz);
          }
        } catch (ClassNotFoundException e) {
          throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
        }
      }
    }
  }

  private void pluginsElement(XNode context) throws Exception {
    if (context != null) {
      for (XNode child : context.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor()
            .newInstance();                               // 插件在启动期就实例化好,运行期只做包装
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);  // 进 Configuration 的 interceptorChain,注册顺序 = XML 书写顺序
      }
    }
  }

  private void environmentsElement(XNode context) throws Exception {
    if (context == null) {
      return;
    }
    if (environment == null) {
      environment = context.getStringAttribute("default");  // build() 没指定环境时用 default 属性
    }
    for (XNode child : context.getChildren()) {
      String id = child.getStringAttribute("id");
      if (isSpecifiedEnvironment(id)) {                   // 可以配 N 个环境,但只解析命中的那一个
        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());  // Environment = id + 事务工厂 + 数据源,三元组
        break;                                            // 命中即止,其余环境的 DataSource 根本不会创建
      }
    }
  }

  private void mappersElement(XNode context) throws Exception {
    if (context == null) {
      return;
    }
    for (XNode child : context.getChildren()) {
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        configuration.addMappers(mapperPackage);          // 包扫描:找接口 → MapperRegistry 注册 → 顺带解析同名 XML
      } else {
        String resource = child.getStringAttribute("resource");
        String url = child.getStringAttribute("url");
        String mapperClass = child.getStringAttribute("class");
        if (resource != null && url == null && mapperClass == null) {   // 三选一,互斥校验在最后的 else
          ErrorContext.instance().resource(resource);
          try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
                configuration.getSqlFragments());          // 把 configuration 递下去:mapper 的解析产物直接写进同一个对象
            mapperParser.parse();
          }
        } else if (resource == null && url != null && mapperClass == null) {
          // ... 省略 url 分支,与 resource 分支同构
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface);       // 接口方式:走注解解析(内部也会尝试找同名 XML)
        } else {
          throw new BuilderException(
              "A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }

解析的终点是 Configuration。它同时是三种东西:全量配置项的持有者、各种注册表(Map)的宿主、运行期核心组件的工厂。

代码块JAVA · 121 行收起展开
// 基于本地 MyBatis 仓 (JavaSourceReadingLab, 3.5.x), org.apache.ibatis.session.Configuration
public class Configuration {

  protected Environment environment;

  // ... 省略 mapUnderscoreToCamelCase / cacheEnabled 等约 30 个 settings 字段,
  //     settingsElement 里那一大串 setter 就是往这些字段上灌值

  // ============ 注册中心的本体:五个子注册表 + 一堆 StrictMap ============
  protected final MapperRegistry mapperRegistry = new MapperRegistry(this);       // Mapper 接口 → 代理工厂(01 篇主角)
  protected final InterceptorChain interceptorChain = new InterceptorChain();     // 插件链,pluginsElement 往这里加
  protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this);
  protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry();
  protected final LanguageDriverRegistry languageRegistry = new LanguageDriverRegistry();

  protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
      "Mapped Statements collection")                    // key = namespace.id,一条 <select> 对应一个 MappedStatement
          .conflictMessageProducer((savedValue, targetValue) -> ". please check " + savedValue.getResource() + " and "
              + targetValue.getResource());              // 重复 id 时报错顺带指出两个冲突文件,排错友好
  protected final Map<String, Cache> caches = new StrictMap<>("Caches collection");
  protected final Map<String, ResultMap> resultMaps = new StrictMap<>("Result Maps collection");
  protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
  protected final Map<String, KeyGenerator> keyGenerators = new StrictMap<>("Key Generators collection");

  protected final Set<String> loadedResources = new HashSet<>();   // 防止同一 mapper 资源被解析两次
  protected final Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments parsed from previous mappers"); // <sql> 片段
  protected final Collection<XMLStatementBuilder> incompleteStatements = new LinkedList<>();  // 解析时依赖没就绪(如引用了
  protected final Collection<CacheRefResolver> incompleteCacheRefs = new LinkedList<>();      // 还没解析的 namespace)的条目
  protected final Collection<ResultMapResolver> incompleteResultMaps = new LinkedList<>();    // 先挂起,之后重试——
  protected final Collection<MethodResolver> incompleteMethods = new LinkedList<>();          // 这就是 mapper 之间无需关心加载顺序的原因

  public Configuration() {
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);   // 内置别名在构造器里预注册,
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);  // 所以 XML 里 type="JDBC"/"POOLED" 能直接解析
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    // ... 省略 JNDI/PERPETUAL/FIFO/SOFT/WEAK/日志实现/CGLIB/JAVASSIST 等其余别名
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    languageRegistry.register(RawLanguageDriver.class);
  }

  // ============ 注册与查找 ============
  public void addMappedStatement(MappedStatement ms) {
    mappedStatements.put(ms.getId(), ms);    // XMLStatementBuilder 解析完一条 SQL 就调这里
  }

  public MappedStatement getMappedStatement(String id, boolean validateIncompleteStatements) {
    if (validateIncompleteStatements) {
      buildAllStatements();                  // 取之前先把挂起的 incomplete* 全部重试解析完
    }
    return mappedStatements.get(id);         // 01 篇里 MapperMethod 执行 SQL,SQL 就是从这里查出来的
  }

  public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
  }

  // ============ 四大组件工厂:拦截器链在"出厂"处统一织入 ============
  public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
      Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
        rowBounds, resultHandler, boundSql);
    return (StatementHandler) interceptorChain.pluginAll(statementHandler);  // 每个组件出厂前都过一遍插件链
  }

  // ... 省略 newParameterHandler / newResultSetHandler,结构与上面完全一致,同样以 pluginAll 收尾

  public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    Executor executor;
    if (ExecutorType.BATCH == executorType) {
      executor = new BatchExecutor(this, transaction);
    } else if (ExecutorType.REUSE == executorType) {
      executor = new ReuseExecutor(this, transaction);
    } else {
      executor = new SimpleExecutor(this, transaction);
    }
    if (cacheEnabled) {
      executor = new CachingExecutor(executor);   // 二级缓存装饰器(02 篇讲过),也在这里套上
    }
    return (Executor) interceptorChain.pluginAll(executor);
  }

  // ============ StrictMap:注册中心专用的"严格版"Map ============
  protected static class StrictMap<V> extends ConcurrentHashMap<String, V> {

    // ... 省略构造器与 conflictMessageProducer

    @Override
    @SuppressWarnings("unchecked")
    public V put(String key, V value) {
      if (containsKey(key)) {                     // 重复注册直接抛异常:配置冲突必须在启动期暴露
        throw new IllegalArgumentException(name + " already contains key " + key
            + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
      }
      if (key.contains(".")) {
        final String shortKey = getShortName(key);   // "com.x.UserMapper.selectById" 额外存一份短键 "selectById"
        if (super.get(shortKey) == null) {
          super.put(shortKey, value);
        } else {
          super.put(shortKey, (V) new Ambiguity(shortKey));  // 短键撞了不覆盖,放个"歧义"哨兵占位
        }
      }
      return super.put(key, value);
    }

    @Override
    public V get(Object key) {
      V value = super.get(key);
      if (value == null) {
        throw new IllegalArgumentException(name + " does not contain value for " + key);  // 查不到也抛,不返回 null
      }
      if (value instanceof Ambiguity) {           // 用短名查到哨兵 = 两个 namespace 有同名 statement,逼你写全名
        throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name
            + " (try using the full name including the namespace, or rename one of the entries)");
      }
      return value;
    }
  }
}

原理串讲

一次完整的启动:new SqlSessionFactoryBuilder().build(inputStream) 里 new 出 XMLConfigBuilder,其私有构造器先干了最重要的一件事——super(newConfig(configClass)),即创建 Configuration 实例。
也就是说 Configuration 不是解析的”结果”,它在解析开始前就存在了,整个解析过程是持续往这个对象上写入的过程。
这个设计的好处在 mappersElement 里看得最清楚:XMLMapperBuilder 构造时直接把 configuration 引用递进去,mapper 里解析出的每个 MappedStatementResultMapCache 都通过 configuration.addXxx() 写回同一个对象,不需要任何”合并中间结果”的步骤;
甚至跨 mapper 的前向引用(A 的 <cache-ref> 指向还没解析的 B)也能处理——解析失败的条目挂进 incompleteCacheRefs 等集合,等 getMappedStatement 触发 buildAllStatements() 时统一重试,这就是 mapper 文件不需要按依赖顺序书写的原因。

parse() 只允许调用一次,然后 parseConfiguration 按写死的顺序逐节点走。
为什么顺序写死而不是按 XML 出现顺序?因为节点之间有真实的依赖:

代码块JAVA · 3 行收起展开
`properties` 必须第一个解析(源码注释 issue #117),因为它产出的变量表要通过 `parser.setVariables(defaults)` 回填给 XPathParser,此后 `environments` 里的 `${jdbc.url}` 才能被替换——properties 自身还定义了三层覆盖优先级,
内联 `<property>` < 外部文件 < 代码传入的 Properties,实现就是三次 `putAll` 的先后顺序;
`typeAliases` 必须在 `plugins`、`environments` 之前,否则 `resolveClass("POOLED")` 这类短名解析不出来(内置别名则更早,在 `Configuration` 构造器里就注册好了);

settingsElement 刻意排在 objectFactory 之后(issue #631)。
另一个容易忽略的细节:settingsAsPropertiesMetaClass 反射检查每个 setting 名在 Configuration 类上有没有对应 setter,拼写错误在启动期就炸,这和 StrictMap 的 fail-fast 是同一哲学——配置错误绝不能拖到运行期变成诡异行为。

为什么插件(Interceptor)只是注册进 interceptorChain,而不在解析时就包装什么?因为插件拦截的四大对象(Executor / StatementHandler / ParameterHandler / ResultSetHandler)在启动期根本不存在,它们是每次会话/每次语句执行时现建的。
所以 Configuration 同时兼任这四个组件的工厂:newExecutornewStatementHandler 等方法在 new 出原始对象后统一走 interceptorChain.pluginAll(...),让每个插件有机会用动态代理把对象包一层。
把工厂方法收口在 Configuration 上,插件织入就只有这一个入口,任何路径创建的组件都不会漏掉拦截——这是”注册中心 + 工厂”合体的直接收益。

StrictMap 是理解”注册中心”性格的最佳样本:put 时 key 已存在直接抛异常(两个 mapper 撞了同一个 namespace.id,启动失败并指出冲突的两个文件);
对带 . 的全限定 key 额外存一份短名 key,这让 sqlSession.selectOne("selectById") 这种偷懒写法成为可能,但一旦两个 namespace 出现同名短键,短键位置会被替换成 Ambiguity 哨兵——之后谁用短名查,就在 get 时抛”is ambiguous”逼你写全名。
get 查不到也抛异常而不返回 null,把”statement id 写错”从 NPE 变成一条带集合名的明确报错。

设计取舍

  • Configuration 是个上帝对象(1200+ 行,字段几十个),MyBatis 有意为之:单一入口换来”所有运行期查找一跳可达”,代价是这个类什么都管。框架内核不大时,这个交换是划算的。
  • XMLConfigBuilder 是一次性对象(parsed 标志位),Builder 与产物分离:解析的临时状态(XPathParser、environment 字符串)不会污染长命的 Configuration。
  • StrictMap 继承 ConcurrentHashMap 但价值不在并发,在于 fail-fast 语义:重复注册、查无此键、短名歧义全部抛异常。配置问题在启动期爆炸,好过运行期静默出错。
  • 环境可以配多个但只实例化命中的那一个(environmentsElementbreak),未选中环境的连接池不会被创建——配置存在不等于资源被分配。
  • 常见误区:以为 settings 拼错会被忽略。实际 settingsAsProperties 用 MetaClass 校验 setter,拼错直接 BuilderException,而且大小写敏感。

延伸阅读