配置解析与Configuration
配置解析与 Configuration:一份 XML 怎么变成全局注册中心
MyBatis 启动时只做一件大事:把 mybatis-config.xml(以及它引用的所有 mapper)解析成一个 Configuration 对象。
此后运行期不再碰 XML,一切查找(SQL 在哪、结果怎么映射、插件有哪些)都是查这个对象里的 Map。
入口链路:SqlSessionFactoryBuilder.build → XMLConfigBuilder.parse → parseConfiguration 逐节点解析,产物全部挂到 Configuration 上。
代码块收起展开
// 基于本地 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。
代码块收起展开
// 基于本地 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)的宿主、运行期核心组件的工厂。
代码块收起展开
// 基于本地 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 里解析出的每个 MappedStatement、ResultMap、Cache 都通过 configuration.addXxx() 写回同一个对象,不需要任何”合并中间结果”的步骤;
甚至跨 mapper 的前向引用(A 的 <cache-ref> 指向还没解析的 B)也能处理——解析失败的条目挂进 incompleteCacheRefs 等集合,等 getMappedStatement 触发 buildAllStatements() 时统一重试,这就是 mapper 文件不需要按依赖顺序书写的原因。
parse() 只允许调用一次,然后 parseConfiguration 按写死的顺序逐节点走。
为什么顺序写死而不是按 XML 出现顺序?因为节点之间有真实的依赖:
代码块收起展开
`properties` 必须第一个解析(源码注释 issue #117),因为它产出的变量表要通过 `parser.setVariables(defaults)` 回填给 XPathParser,此后 `environments` 里的 `${jdbc.url}` 才能被替换——properties 自身还定义了三层覆盖优先级,
内联 `<property>` < 外部文件 < 代码传入的 Properties,实现就是三次 `putAll` 的先后顺序;
`typeAliases` 必须在 `plugins`、`environments` 之前,否则 `resolveClass("POOLED")` 这类短名解析不出来(内置别名则更早,在 `Configuration` 构造器里就注册好了);settingsElement 刻意排在 objectFactory 之后(issue #631)。
另一个容易忽略的细节:settingsAsProperties 用 MetaClass 反射检查每个 setting 名在 Configuration 类上有没有对应 setter,拼写错误在启动期就炸,这和 StrictMap 的 fail-fast 是同一哲学——配置错误绝不能拖到运行期变成诡异行为。
为什么插件(Interceptor)只是注册进 interceptorChain,而不在解析时就包装什么?因为插件拦截的四大对象(Executor / StatementHandler / ParameterHandler / ResultSetHandler)在启动期根本不存在,它们是每次会话/每次语句执行时现建的。
所以 Configuration 同时兼任这四个组件的工厂:newExecutor、newStatementHandler 等方法在 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 语义:重复注册、查无此键、短名歧义全部抛异常。配置问题在启动期爆炸,好过运行期静默出错。
- 环境可以配多个但只实例化命中的那一个(
environmentsElement里break),未选中环境的连接池不会被创建——配置存在不等于资源被分配。 - 常见误区:以为 settings 拼错会被忽略。实际
settingsAsProperties用 MetaClass 校验 setter,拼错直接 BuilderException,而且大小写敏感。