资讯动态

Mybatis全局配置解析:XMLConfigBuilder从XML到Configuration的完整路径

发布时间:2026/10/14 1:02:49 来源:尧图企业网站定制
有过类似经历的朋友应该能会心一笑某天全局配置里明明写好了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如果返回nullXMLConfigBuilder会抛出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或configMybatis 在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的构造器会做三件事根据输入流创建Document对象这一步用到XMLMapperEntityResolver和MapperEntityResolver处理本地 DTD。初始化XPath表达式求值器。保存变量属性。如果你传入的是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之后那么映射文件中出现的parameterTypeUser在解析时就找不到对应的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 里缺省某个子节点时也能安静地跳过。比如你没有配置typeHandlerstypeHandlerElement(null)什么也不做不会影响其它节点解析。如果某个节点写错了然后报错往往不是解析器的问题而是节点内部属性冲突或者资源路径无效。3.3 loadCustomVarssettings中自定义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 namecacheEnabled valuetruex/在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 namecacheEnabled valuetrue/里的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 namevariables 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内部是一个HashMapString, Class?key 区分大小写。typeAlias aliasuser type.../和typeAlias aliasUser type.../是同一个 key不不是同一个因为 key 是user和User两个对象。但在 XML 映射文件中写parameterTypeuser无法匹配到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 namecom.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 MapString, MappedStatement mappedStatements new StrictMap(Mapped Statements collection); protected final MapString, Cache caches new StrictMap(Caches collection); protected final MapString, ResultMap resultMaps new StrictMap(Result Maps collection); protected final MapString, ParameterMap parameterMaps new StrictMap(Parameter Maps collection); protected final MapString, KeyGenerator keyGenerators new StrictMap(Key Generators collection); protected final MapString, 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 namecom.example.mapper/又手动配置了mapper resourcecom/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 truelazyLoadingEnabled falsemultipleResultSetsEnabled trueuseGeneratedKeys falsedefaultStatementTimeout nulldefaultFetchSize nulldefaultExecutorType ExecutorType.SIMPLElocalCacheScope LocalCacheScope.SESSIONjdbcTypeForNull JdbcType.OTHER这些默认值会在settingsElement中被覆盖。settingsElement的赋值逻辑特别值得注意它不是用Properties直接 bulk 设置而是逐个读取已知的 setting 名称然后调用对应的 setter。如果你在settings里写了defaultExecutorType它就能识别但如果你写了defaultExecutorTypexsettingsAsProperties阶段就会报 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 里写autoMappingBehaviorPARTIAL没问题但写autoMappingBehaviorpartial就会抛IllegalArgumentException: No enum constant org.apache.ibatis.session.AutoMappingBehavior.partial。Mybatis 对枚举类型的值非常挑剔大小写不匹配就直接启动失败。很多老手在从 Mybatis 3.4 升 3.5 时就因为把UNMAPPED之类写成了小写导致启动报错。6. 踩坑与自查解析顺序依赖、占位符替换、重复资源下发的诡异现象最后这部分我想集中写几个我在实际项目中遇到过的且容易被表象迷惑的问题。这些问题根源都在XMLConfigBuilder的机制上但症状五花八门。6.1 properties 占位符解析到底发生在哪个阶段一个典型异常properties resourcejdbc.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是从settingsXML 节点直接读取的和getVariables()没有交集。getVariables()主要用于evalString占位符替换。也就是说XML 里的${jdbc.url}可以引用外部传入 Properties 或properties子节点。settings里setting namecacheEnabled 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 namecacheEnabled 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 existsmappers重复加载了同一 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——每一步都会影响后续步骤的上下文。理解了这条流水线再遇到诡异的配置问题至少知道该去哪一环打断点。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑