☰
Mybatis全局配置解析:XMLConfigBuilder从XML到Configuration的完整路径
2026/10/14 1:02:48 网站建设 项目流程

有过类似经历的朋友应该能会心一笑:某天全局配置里明明写好了<settings>的mapUnderscoreToCamelCase,结果查询结果依然是下划线字段名,怎么查都是null。后来翻了源码才发现,不是配置没生效,而是XMLConfigBuilder在解析<settings>节点时用了一套非常"挑剔"的规则,key 映射、值转换、覆盖顺序,任何一环不对,你都只能在运行时措手不及。

这篇文章以Mybatis 全局配置文件(mybatis-config.xml)为切入点,重点分析XMLConfigBuilder是如何把配置文件里那些 XML 节点(properties、settings、typeAliases、typeHandlers、objectFactory、objectWrapperFactory、reflectorFactory、plugins、environments、databaseIdProvider、mappers)逐项提取出来,再映射到Configuration对象的字段上。源码版本基于 Mybatis 3.5.x,这也是目前用得最广的稳定分支。阅读本文不需要你把整份源码下载下来,但建议手边备一份 IDE,对照着看效果更好。

1. 从一段调试日志说起:全局配置解析入口到底在哪儿

很多人写 Mybatis 程序时,第一步都是SqlSessionFactoryBuilder().build(inputStream),然后就进入业务代码了。至于这个build背后到底做了什么,很少有人会深究。其实全局配置解析入口非常简单,就是XMLConfigBuilder这个类,它是BaseBuilder的子类,专门负责"读取 XML 并组装 Configuration"。

1.1 SqlSessionFactoryBuilder.build 与 XMLConfigBuilder 的分工

SqlSessionFactoryBuilder是个典型的"双阶段"构建器:第一阶段用XMLConfigBuilder把 XML 配置解析成Configuration对象,第二阶段用Configuration实例去构建DefaultSqlSessionFactory。你的 XML 文件里写的每一个节点,最终都会以 Java 对象的形式挂到Configuration的各个字段上。

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); }

这里有个容易忽略的点:XMLConfigBuilder在构造时就完成了XPathParser的初始化,并且已经读取了 XML 文档头,只是还没有逐节点解析。parse()方法里做的是"获取根节点 + 调用parseConfiguration"两件事:

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

parsed这个布尔值很重要。一个XMLConfigBuilder实例只能用一次,这种设计看似简单,却能避免同一个解析器被反复调用导致的 Configuration 状态错乱。实际开发中如果你手动 new 了一个XMLConfigBuilder去解析,然后又想复用,就会直接撞上这个保护机制。

1.2 根节点校验:为什么多一个空格也会报错

parser.evalNode("/configuration")会返回根节点XNode,如果返回null,XMLConfigBuilder会抛出BuilderException: Error parsing SQL Mapper Configuration. Cause: The content of elements must consist of well-formed character data...,或者更直白的Cause: Configuration XML must be a valid XML document。

这里我要特别提醒一点:根节点必须是<configuration>。有人把文件命名为mybatis-config.xml或者mybatis.xml,这都没问题,但根元素写成了<Configuration>或<config>,Mybatis 在evalNode阶段并不会校验标签名的大小写,只是在 XPath 查找时找不到/configuration节点,最终返回null而报错。所以这不是 XML 大小写敏感的问题,而是 XPath 定位失败。遇到这类报错,第一反应不是去看 DTD,而是确认根标签确实是<configuration>全小写。

2. XMLConfigBuilder构造器里的"隐式状态":XPathParser与variables

在正式进入节点解析之前,建议先把构造器逻辑吃透,因为很多"诡异问题"的根子就埋在这里。

2.1 构造器真实执行序列

XMLConfigBuilder有两个常用构造器:

public XMLConfigBuilder(Reader reader, String environment, Properties props) { this(new XPathParser(reader, props), environment, props); } public XMLConfigBuilder(XPathParser parser, String environment, Properties props) { super(new Configuration()); this.configuration.setVariables(parser.getVariables()); this.parsed = false; this.environment = environment; this.parser = parser; }

注意super(new Configuration())这一行。BaseBuilder的构造器接受一个Configuration实例,也就是说XMLConfigBuilder在new出来的那一刻,内部就已经持有了一份全新的、没有任何配置数据的Configuration。后续所有解析动作,都是在这份"空配置"上做增量填充。

还有一个细节:this.configuration.setVariables(parser.getVariables())。parser.getVariables()返回的是XPathParser内部持有的variables属性,这个属性就是从外部传进来的Properties(比如new XMLConfigBuilder(inputStream, null, props)里的props)。它会贯穿整个解析过程,用于替换 XML 中的${}占位符。很多人不知道,Configuration里variables字段的赋值发生在这里,而不是parseConfiguration阶段。

2.2 XPathParser 如何预处理XML输入流

XPathParser的构造器会做三件事:

  1. 根据输入流创建Document对象,这一步用到XMLMapperEntityResolver和MapperEntityResolver处理本地 DTD。
  2. 初始化XPath表达式求值器。
  3. 保存变量属性。

如果你传入的是InputStream,它内部会先进行** XML 实体解析**,也就是把<!DOCTYPE configuration PUBLIC ...>这种声明替换成内部 DTD。这一步在绝大多数情况下是透明的,但如果你在 XML 里用了自定义实体或者引用了外部 DTD,就可能在new XMLConfigBuilder阶段直接抛异常,而不是等你调用parse()后才出错。

这里有个实战心得:排查 XML 解析问题时,先看堆栈第一行的Caused by是不是org.apache.ibatis.builder.XMLMapperBuilder或XMLConfigBuilder构造阶段触发。如果是,说明问题根本不在业务代码里,而是 XML 本身就不是合法文档——比如文件编码不是 UTF-8、标签没闭合、特殊字符没转义。Mybatis 的 DTD 校验其实很宽松,但对 XML 结构性错误是零容忍的。

3. parseConfiguration方法:一条if语句串起全局配置的骨架

XMLConfigBuilder的核心逻辑集中在parseConfiguration(XNode root)。这个方法的代码风格非常老派——一连串的if (XNode == null)判断加上调用对应解析方法。看起来平淡无奇,但它定义了 Mybatis 全局配置解析的固定顺序,而这个顺序本身就是一种隐式的依赖关系。

3.1 节点解析的固定编排顺序

private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVars(settings); typeAliasesElement(root.evalNode("typeAliases")); pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } }

这里的顺序不是随便排的。properties必须在最前面,因为它影响后续所有节点的${}占位符解析。settings必须紧跟其后,因为它的部分属性(比如vfsImpl、logImpl)会影响Configuration的行为。typeAliases在environments之前,是因为 SQL 映射文件里的parameterType、resultType可能有类型别名引用。mappers被放在最后,是因为它加载的映射文件需要依赖前面已经注册好的类型别名、类型处理器、环境信息。

如果打乱顺序,比如把typeAliasesElement放在mappersElement之后,那么映射文件中出现的parameterType="User"在解析时就找不到对应的TypeAliasRegistry注册项,直接抛TypeException。这解释了为什么有些人把 XML 节点的物理顺序调换后,反而报"找不到别名"的错误。

3.2 每个节点解析方法的设计共性:evalNode + 判空

几乎每个xxxElement方法都遵循同一套模式:

private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.getChildren()) { // 具体处理逻辑 } } }

evalNode返回的XNode即使没有匹配到节点,也不会返回null的 "子节点集合",因为XPathParser.evalNode对不存在的路径返回 null。但一旦获取到parent,而某个子节点不存在,getChildren()获取的就是空集合而不是null,所以循环可以安全进行。

这种模式很容易被人忽略,但正是这种"判空 + 循环子节点"的组合,让 XML 里缺省某个子节点时也能安静地跳过。比如你没有配置<typeHandlers>,typeHandlerElement(null)什么也不做,不会影响其它节点解析。如果某个节点写错了然后报错,往往不是解析器的问题,而是节点内部属性冲突或者资源路径无效。

3.3 loadCustomVars:settings中自定义VFS和logImpl的提前加载

settingsAsProperties解析出的Properties对象里,有几个 key 比较特殊,包括vfsImpl、logImpl、proxyFactory。loadCustomVars方法会直接从这些 properties 里读取值,尝试反射加载对应实现类,然后设置到Configuration中:

private void loadCustomVars(Properties settings) { configuration.setVfsImpl(settings.getProperty("vfsImpl")); configuration.setLogImpl(settings.getProperty("logImpl")); configuration.setProxyFactory((ProxyFactory) createInstance(settings.getProperty("proxyFactory"))); }

这里有个陷阱:settingsAsProperties只把<settings>下所有<setting>的name/value存到Properties,并不会逐一校验 value 的合法性。比如你写了<setting name="cacheEnabled" value="truex"/>,在settingsAsProperties阶段不会报错,到了settingsElement阶段才会调用Boolean.valueOf("truex")得到false,然后静默生效。你要排查半天才能意识到是自己手滑写错字符串。后面我会专门给一张settings 属性与 Configuration 字段对照表,方便逐一核对。

4. 逐个节点拆解:settings、typeAliases、plugins、environments、mapper的赋值路径

这部分是本文的硬核内容,我会把每个节点从 XML 到Configuration字段的映射路径完整走一遍。

4.1 settings节点:为什么这里的属性名可以"驼峰缩写"

settingsAsProperties和settingsElement是分工协作的。settingsAsProperties负责"收集",settingsElement负责"赋值"。收集阶段用到了MetaClass的反射能力:

private Properties settingsAsProperties(XNode context) { if (context == null) { return new Properties(); } Properties props = context.getChildrenAsProperties(); MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); for (Object key : props.keySet()) { if (!metaConfig.hasSetter((String) key)) { throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly."); } } return props; }

注意metaConfig.hasSetter((String) key)。它用的是Configuration类上暴露的 setter 方法名来校验 key 是否合法。所以<setting name="cacheEnabled" value="true"/>里的cacheEnabled对应的是Configuration#setCacheEnabled(boolean)。而 Mybatis 的Configuration类上有setMapUnderscoreToCamelCase(boolean),因此 XML 里允许写成mapUnderscoreToCamelCase,也可以写成map-underscore-to-camel-case吗?不对,这里并不支持 kebab-case,只支持 Java 属性名。你写map-underscore-to-camel-case会被判定为 unknown setting。

这里还隐含了一个细节:MetaClass.hasSetter是基于Introspector的。Configuration类里存在isMapUnderscoreToCamelCase()的 getter 和setMapUnderscoreToCamelCase()的 setter,所以mapUnderscoreToCamelCase能通过校验。但Configuration里也有setVariables(Properties),所以<setting name="variables" value="..."/>理论上也能通过hasSetter校验,只是到了settingsElement阶段不会对variables做任何处理。这是一个极少人注意到的"伪合法"属性。

4.2 typeAliases节点:包扫描注册与别名的覆盖规则

typeAliasesElement支持两种子节点:<package>和<typeAlias>。

private void typeAliasesElement(XNode parent) { if (parent != null) { for (XNode child : parent.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"); configuration.getTypeAliasRegistry().registerAlias(alias, type); } } } }

这里有两个常见误区:

误区一:包扫描能识别所有注解别名。registerAliases(String packageName)内部会扫描该包下所有类,查找标注了@Alias注解的类,没有注解的类则用Class.getSimpleName()作为别名。结果就是,全限定类名com.example.model.User注册后别名是User,而如果另一个包下也有同名User,后注册的会把先注册的覆盖掉。而且没有报错,只有调试时通过typeAliasRegistry.getTypeAliases()才能发现。

误区二:<typeAlias>的别名大小写敏感。TypeAliasRegistry内部是一个HashMap<String, Class<?>>,key 区分大小写。<typeAlias alias="user" type="..."/>和<typeAlias alias="User" type="..."/>是同一个 key?不,不是同一个,因为 key 是user和User两个对象。但在 XML 映射文件中写parameterType="user"无法匹配到User别名,会给你报Could not resolve type alias 'user'。这点在微服务多模块场景下特别坑,因为约定俗成首字母大写,偶尔有人写成小写,排查起来很费劲。

4.3 plugins节点:拦截器为什么能生效,关键在 wrap

每个<plugin>子节点包含interceptor属性和可选的<property>子节点。解析逻辑非常直接:

private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); configuration.addInterceptor(interceptorInstance); } } }

这里要理解resolveClass(interceptor)为什么要用TypeAliasRegistry解析。Mybatis 的Interceptor实现类如果是用全限定类名写的,resolveClass能直接将字符串转成Class<?>;如果是别名,则会从TypeAliasRegistry查表。实现Interceptor接口的类必须有无参构造器,否则getDeclaredConstructor().newInstance()会抛异常。

插件在Configuration里是怎么生效的?addInterceptor只是把Interceptor实例加入InterceptorChain的interceptors列表。真正产生拦截效果,是在创建Executor、StatementHandler、ParameterHandler、ResultSetHandler时,通过interceptorChain.pluginAll(target)对目标对象进行 JDK 动态代理包装。所以如果你只配置了<plugin>但不实现接口的intercept方法,运行时日志上根本看不出插件存在,但只要目标方法被调用,代理层会触发invoke方法。

4.4 environments节点:多环境切换的配置源

environmentsElement的解析流程是:

  • 根据构造器传入的environmentid 选择一个<environment>子节点,如果没指定,默认取<environments default="...">指向的那个。
  • 从选中的<environment>里取出<transactionManager>和<dataSource>。
  • transactionManager的 type 可以是JDBC、MANAGED,也可以是自定义类全限定名;内部通过TransactionFactory实例化。
  • dataSource的 type 可以是UNPOOLED、POOLED、JNDI或自定义类全限定名,内部通过DataSourceFactory实例化。
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)) { transactionManagerElement(child.evalNode("transactionManager")); dataSourceElement(child.evalNode("dataSource")); } } } }

isSpecifiedEnvironment(id)判断当前子环境 id 是否匹配前面确定的environment。一旦匹配,就执行transactionManagerElement和dataSourceElement,这两个方法会把解析出的工厂对象累加到configuration.environment字段上(实际上是new Environment(id, transactionFactory, dataSource))。注意,Configuration只保存一个environment字段,所以如果你在 XML 里配置了多个 environment,但当前选择的只是其中一个,其余的并不会被加载到Configuration里。

4.5 mappers节点:XMLConfigBuilder 结束前的最后一次大力出奇迹

mapperElement是全局配置解析的收尾节点,也是和 SQL 映射文件关联最紧密的一步。它支持<package>和<mapper>两种子节点。

  • <package name="com.example.mapper"/>:扫描包下所有接口,逐个调用MapperRegistry.addMapper注册 Mapper。
  • <mapper resource="..."/>:加载 XML 映射文件。
  • <mapper url="..."/>:从 URL 加载 XML 映射文件。
  • <mapper class="..."/>:只注册接口类,不加载 XML。
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"); if (resource != null && url == null && mapperClass == null) { try (InputStream inputStream = Resources.getResourceAsStream(resource)) { XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); mapperParser.parse(); } } else if (resource == null && url != null && mapperClass == null) { // 类似处理 URL } 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."); } } } } }

从这里可以看到,全局配置的解析本质上就是在做"资源定位 + 注册",真正复杂的 SQL 语句解析发生在XMLMapperBuilder、XMLStatementBuilder和XMLIncludeTransformer这些下游类中。所以XMLConfigBuilder承担的是"承上启下"的角色——它本身不解析 SQL,但它决定了这些 SQL 映射文件在什么时候、以什么顺序被加载。

5. Configuration对象里那些"藏起来的"属性:mappedStatements、resultMaps等

XMLConfigBuilder最终所做的所有事情,都是为了填充Configuration这个"信息中心"。但Configuration里的很多字段,并不会在全局配置 XML 里直接出现,比如mappedStatements、resultMaps、parameterMaps、keyGenerators、sqlFragments,这些是在解析 Mapper XML 时动态填充的。XMLConfigBuilder只是负责初始化这些容器的空状态并触发上游填充。

5.1 全局配置阶段初始化了哪些容器

Configuration的构造器里定义了大量集合:

protected final MapperRegistry mapperRegistry = new MapperRegistry(this); protected final InterceptorChain interceptorChain = new InterceptorChain(); protected final TypeHandlerRegistry typeHandlerRegistry = new TypeHandlerRegistry(this); protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry(); protected final LanguageDriverRegistry languageDriverRegistry = new LanguageDriverRegistry(); protected final Map<String, MappedStatement> mappedStatements = new StrictMap<>("Mapped Statements collection"); 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 Map<String, XNode> sqlFragments = new StrictMap<>("XML fragments");

其中mappedStatements、resultMaps、parameterMaps、keyGenerators、sqlFragments都是StrictMap。这个内部类很特别:当你用put放入重复的 key 时,它会抛IllegalArgumentException(除非 value 之前存的是一个"占位符")。这也解释了为什么在多个 Mapper XML 里定义同 id 的<select>会直接启动失败,而不是让运行时去"二选一"。

5.2 StrictMap 对重复 key 的强制约束

StrictMap被设计为"如果 put 时发现 key 已经存在,且原值不是自己",则抛出异常。它的代码大致是这样:

@Override public V put(String key, V value) { if (containsKey(key)) { throw new IllegalArgumentException(name + " already contains value for " + key + (conflictMessage == null ? "" : ". " + conflictMessage)); } return super.put(key, value); }

所以,如果你在全局配置的<mappers>里既配置了<package name="com.example.mapper"/>,又手动配置了<mapper resource="com/example/mapper/UserMapper.xml"/>,同一个 Mapper 接口会被注册两次吗?答案是:MapperRegistry.addMapper会校验knownMappers中是否已有同一个Class,有则跳过;但如果同一个 Mapper XML 被不同的 resource 路径加载两次,XMLMapperBuilder调用configuration.addMappedStatement时,就会因为重复的 statement id 直接抛IllegalArgumentException。这种情况的典型表现是:启动时正常,第一次执行查询时报"Statement already exists"。排查方向基本都是<mappers>子节点写重复了。

5.3 全局配置阶段尚未加载但已具备的默认能力

Configuration在构造时已经初始化了很多默认值:

  • cacheEnabled = true
  • lazyLoadingEnabled = false
  • multipleResultSetsEnabled = true
  • useGeneratedKeys = false
  • defaultStatementTimeout = null
  • defaultFetchSize = null
  • defaultExecutorType = ExecutorType.SIMPLE
  • localCacheScope = LocalCacheScope.SESSION
  • jdbcTypeForNull = JdbcType.OTHER

这些默认值会在settingsElement中被覆盖。settingsElement的赋值逻辑特别值得注意,它不是用Properties直接 bulk 设置,而是逐个读取已知的 setting 名称,然后调用对应的 setter。如果你在<settings>里写了defaultExecutorType,它就能识别;但如果你写了defaultExecutorTypex,settingsAsProperties阶段就会报 unknown setting,而不是到了settingsElement才报。

private void settingsElement(Properties props) { configuration.setAutoMappingBehavior(AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))); configuration.setAutoMappingUnknownColumnBehavior(AutoMappingUnknownColumnBehavior.valueOf(props.getProperty("autoMappingUnknownColumnBehavior", "NONE"))); configuration.setCacheEnabled(booleanValueOf(props.getProperty("cacheEnabled"), true)); // ... 各种 setter }

这里有个坑:AutoMappingBehavior.valueOf(props.getProperty("autoMappingBehavior", "PARTIAL"))。如果你在 XML 里写autoMappingBehavior=PARTIAL没问题,但写autoMappingBehavior=partial就会抛IllegalArgumentException: No enum constant org.apache.ibatis.session.AutoMappingBehavior.partial。Mybatis 对枚举类型的值非常挑剔,大小写不匹配就直接启动失败。很多老手在从 Mybatis 3.4 升 3.5 时,就因为把UNMAPPED之类写成了小写,导致启动报错。

6. 踩坑与自查:解析顺序依赖、占位符替换、重复资源下发的诡异现象

最后这部分,我想集中写几个我在实际项目中遇到过的,且容易被表象迷惑的问题。这些问题根源都在XMLConfigBuilder的机制上,但症状五花八门。

6.1 properties 占位符解析到底发生在哪个阶段

一个典型异常:<properties resource="jdbc.properties"/>里的值在<dataSource>中被正确注入,但在<typeAliases>的package属性中就没有被替换。原因是XMLConfigBuilder针对不同节点调用了解析方法,而这些方法内部使用${}占位符的方式并不一致。

propertiesElement做了两件事:加载外部 properties 文件的内容,并将这些内容合并到parser.getVariables()中;同时把<properties>子节点<property>的内容也加入这个变量集合。

在dataSourceElement里拿到连接参数时,调用的是XNode的evalString,它会同时使用XPathParser的variables进行占位符替换。所以url = ${jdbc.url}这样的写法能正常展开。

但typeAliasesElement里的<package name="${mapper.package}"/>这个写法,在XNode.getStringAttribute("name")取值时,并不会自动做占位符替换。XNode.getStringAttribute直接返回原始字符串,不会触发evalString。想要它支持占位符,必须在mapperElement或其它地方使用parser.evalString类的方法,而不是直接getStringAttribute。

实际表现就是:properties 文件里的mapper.package不变,包扫描解析后注册的别名集合为空,但又不报错。等你写映射文件时才发现找不到类型。这种问题非常隐蔽。我的经验是:凡是在全局配置 XML 中使用${}的地方,优先考虑它是否在一个"会被 evalString 处理"的上下文中。比如<property name="..." value="${...}"/>的 value 确实会经过XNode.getStringAttribute之后再进行变量替换吗?这里要区分XNode.getStringAttribute和evalString,前者不替换。

源码里的getStringAttribute(String name)实现:先拿到 attribute 原始值,然后直接返回。而evalString(String expression)会调用parser.evalString,才会用variables替换。所以如果你在<properties resource="..."/>中定义了一个值,希望被<package name="${mapper.package}"/>引用,实际是做不到的。我试过把包名改成占位符,启动后观察typeAliasRegistry.getTypeAliases(),没有任何对应注册。

经验结论:全局配置里能用占位符的地方,通常是XNode上显式调用了evalString的路径(比如dataSource的 url/username/password),而getStringAttribute的直接属性,如typeAlias的alias、plugin的interceptor,都不能使用外部占位符。这一点和 Mybatis 的 Mapper XML 中${}的语义完全不一样,容易搞混。

6.2 configuration.getVariables() 与 XML 变量的作用域边界

如果你通过编程方式给XMLConfigBuilder传入了Properties,那这些属性会进入parser.getVariables(),并在settingsElement里被读取吗?不一定。因为settingsAsProperties是从<settings>XML 节点直接读取的,和getVariables()没有交集。getVariables()主要用于evalString占位符替换。也就是说:

  • XML 里的${jdbc.url}可以引用外部传入 Properties 或<properties>子节点。
  • <settings>里<setting name="cacheEnabled" value="${cache.enabled}"/>这个 value 会先走getChildrenAsProperties,其底层XNode.getChildrenAsProperties()会对每个<setting>的 value 调用evalString吗?

看一下XNode.getChildrenAsProperties()的实现:

public Properties getChildrenAsProperties() { Properties properties = new Properties(); for (XNode child : getChildren()) { String name = child.getStringAttribute("name"); String value = child.getStringAttribute("value"); properties.setProperty(name, value); } return properties; }

注意它用的是getStringAttribute,不是evalString。所以<setting name="cacheEnabled" value="${cache.enabled}"/>并不会把cache.enabled替换成对外部 Properties 的值。你要是这么写,得到的cacheEnabled的值就是字符串"${cache.enabled}",然后booleanValueOf解析时会返回默认值true(因为该字符串不是"true"也不是"false"),不报错,但你的配置实际没生效。这个坑我记忆犹新,当时为了查一个二级缓存为什么始终开启,翻了半天源码才发现是占位符被原样传给了Boolean.parseBoolean,返回false,导致cacheEnabled意外变成了默认值。

所以正确做法是:不要在<settings>中依赖外部 Properties 做占位符替换。如果确实需要动态调整设置项,就直接改 XML 或者用多个 environment。

6.3 同一全局配置被多次 build 引发的状态污染

有人会在单元测试里多次调用:

SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(Resources.getResourceAsStream("mybatis-config.xml"));

每次调用都会new Configuration(),所以多次 build 本身是安全的。但如果你手动复用了Configuration实例:

Configuration configuration = new Configuration(); // 手动设置某些属性 XMLConfigBuilder builder = new XMLConfigBuilder(..., configuration); builder.parse();

你要小心,XMLConfigBuilder构造器里的super(new Configuration())无法传入外部 Configuration,因为XMLConfigBuilder只有一个显式构造器是把Configuration作为 super 构造参数,且该参数是新创建的。也就是说,你没有办法通过XMLConfigBuilder直接解析到一个已经存在的Configuration里。Mybatis 的设计意图就是你得从零构建一份配置。如果想在既有Configuration基础上补充全局配置,只能走Configuration自身的 setter 方法,而不是再来一遍全局 XML 解析。

这也是为什么 Mybatis 的Configuration对象一旦被SqlSessionFactory持有后,再去修改它的属性,往往不会影响到已经创建的 SqlSession 的某些行为(比如defaultExecutorType只在创建 Executor 时读取)。所以千万不要在运行时修改Configuration的全局设置来"动态开关",行为是不可预期的。

6.4 快速自查清单:你的 mybatis-config.xml 哪里可能不对劲

根据我和身边同事的踩坑经验,我整理了一份自查清单,适合在 XML 配置解析报错时快速定位问题:

症状大概率根源排查方向
启动报Unknown settingsettings中的 name 和Configurationsetter 不匹配检查拼写与大小写,参考 Configuration 类 setter 方法名
启动报Could not resolve type alias包扫描路径有误或别名未注册打印typeAliasRegistry.getTypeAliases()确认
启动报Statement already exists<mappers>重复加载了同一 Mapper检查 resource 路径是否与 package 扫描重叠
启动报No enum constantsettings 中枚举值大小写错误使用枚举原名字,如PARTIAL、SESSION
运行时配置不生效${}占位符在 getStringAttribute 上下文失效避免在 settings / typeAlias 中使用占位符
插件不生效interceptor类型加载失败或没有无参构造器check stack trace 和构造器
数据源连不上environment id 没匹配上确认environment属性指定的 id 与<environment id>一致
映射的 XML 没生效mapper 资源的路径不合法或没有 resources检查Resources.getResourceAsStream路径

这张表不能覆盖所有异常,但排查顺序基本有效。Mybatis 的全局配置解析整体是直白的——每一行代码都在做"从 XNode 取值,塞进 Configuration"。但正因为直白,很多错误会被掩藏在默认值里,导致你从业务代码里怎么都查不到原因。回头一看,原来是settings的一个value拼错了,或者占位符被当成了字面量。

个人体会是,阅读XMLConfigBuilder源码最大的收获,不是学会调用哪个 API,而是建立了一种"配置生效边界"的意识。Mybatis 的全局配置不是声明式魔法,而是严格的命令式赋值流程:先 properties,再 settings,再 typeAliases,再 environments,最后 mappers——每一步都会影响后续步骤的上下文。理解了这条流水线,再遇到诡异的配置问题,至少知道该去哪一环打断点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询