
我剛?cè)胄心菚旱谝淮畏璵ybatis-config.xml滿腦子都是“這玩意兒到底怎么跑起來的”。網(wǎng)上搜出來的教程清一色在教你寫settings、mappers但沒人告訴你這些標(biāo)簽從 XML 變成Configuration對象的過程中MyBatis 到底做了哪些事。后來因?yàn)橐挪橐粋€(gè)啟動(dòng)報(bào)錯(cuò)被逼著把XMLConfigBuilder的源碼從頭到尾啃了一遍才發(fā)現(xiàn)這條解析鏈路遠(yuǎn)比我想象中精巧。這篇文章就把我整理的“全局配置文件解析全流程”完整拆給你看包括解析順序、核心類職責(zé)、配置項(xiàng)背后的設(shè)計(jì)意圖以及我踩過的幾個(gè)坑。不管是想看明白源碼、準(zhǔn)備面試還是想排查配置問題應(yīng)該都能用得上。1. 全局配置文件的角色與整體設(shè)計(jì)思路1.1 為什么需要全局配置文件MyBatis 的核心對象是SqlSessionFactory而SqlSessionFactory又是基于Configuration構(gòu)建的。Configuration這個(gè)類幾乎承載了 MyBatis 運(yùn)行期的全部狀態(tài)數(shù)據(jù)源、事務(wù)管理器、映射器注冊表、類型別名、類型處理器、插件攔截器、全局緩存開關(guān)、懶加載策略……可以說MyBatis所有運(yùn)行時(shí)行為都能在Configuration上找到對應(yīng)的字段。全局配置文件mybatis-config.xml的作用就是用一種聲明式的方式把這些運(yùn)行期狀態(tài)從代碼里剝離出來。你不用在 Java 里一個(gè)個(gè)set而是寫一段 XML由框架負(fù)責(zé)把 XML 解析成Configuration對象。這樣做的好處很直接配置與代碼解耦環(huán)境切換開發(fā)、測試、生產(chǎn)只需要換配置文件不需要重新編譯。很多初學(xué)者會把全局配置文件和 Mapper XML 配置文件搞混。其實(shí)兩者是不同層級的東西全局配置負(fù)責(zé)“框架本身怎么跑”Mapper 配置負(fù)責(zé)“某一條 SQL 怎么執(zhí)行”。全局配置的解析入口是XMLConfigBuilderMapper 配置的解析入口是XMLMapperBuilder后者的調(diào)用時(shí)機(jī)多數(shù)發(fā)生在全局配置解析到mappers標(biāo)簽時(shí)。理解這條主鏈路后面的源碼讀起來就順了。1.2 配置文件的整體分層MyBatis 全局配置文件的解析有一個(gè)顯著特征對配置項(xiàng)順序極其敏感。這不是 MyBatis 故意搞事而是因?yàn)閄MLConfigBuilder在解析時(shí)用了XNode的evalNode來逐個(gè)獲取子節(jié)點(diǎn)而底層依賴的 XML 解析器在定位節(jié)點(diǎn)時(shí)也依賴文檔結(jié)構(gòu)。從官網(wǎng)的 DTD 定義的順序來看configuration下允許出現(xiàn)的子元素順序是!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)也就是說properties必須放在最前面接著是settings、typeAliases、typeHandlers、objectFactory等最后才是mappers。為什么順序這么關(guān)鍵因?yàn)榻馕鲞^程是“邊解析邊構(gòu)建”的。解析到typeAliases時(shí)Configuration的TypeAliasRegistry需要已經(jīng)被初始化解析到settings時(shí)Configuration需要已經(jīng)準(zhǔn)備好對應(yīng)的屬性 setter。如果順序亂了比如把mappers放到settings前面解析器在解析 Mapper 時(shí)可能依賴某些尚未注冊的別名或類型處理器就會直接報(bào)錯(cuò)。從我實(shí)際排查過的案例來看TypeAlias 注冊順序?qū)е碌膯栴}最隱蔽。.xml配置里mappers使用package方式掃描時(shí)如果 XML 文件里有未注冊的 resultType 簡寫別名而 typeAliases 又排在后面MyBatis 解析 Mapper 時(shí)就會拋出TypeException提示找不到對應(yīng)的別名。所以我的建議是嚴(yán)格按照 DTD 順序書寫別耍小聰明調(diào)整順序。2. XML 解析鏈路拆解從 XML 到 Configuration 對象2.1 入口SqlSessionFactoryBuilder 與 XMLConfigBuilder使用 MyBatis 時(shí)最常見的一段啟動(dòng)代碼是這樣String resource mybatis-config.xml; InputStream inputStream Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory new SqlSessionFactoryBuilder().build(inputStream);很多人沒意識到Resources.getResourceAsStream這一步其實(shí)已經(jīng)干了件大事它通過類加載器把mybatis-config.xml從 classpath 中定位并打開成二進(jìn)制流。這里有一個(gè)很容易被忽略的細(xì)節(jié)Resources會嘗試多個(gè)類加載器包括當(dāng)前線程的contextClassLoader、當(dāng)前類的加載器、系統(tǒng)類加載器逐個(gè)試錯(cuò)直到成功。這在高動(dòng)態(tài)類加載環(huán)境比如應(yīng)用服務(wù)器里很關(guān)鍵如果只用一個(gè)類加載器資源可能就找不到了。SqlSessionFactoryBuilder.build()里發(fā)生的事情更集中。它會創(chuàng)建XMLConfigBuilder并調(diào)用它的parse()方法得到Configuration對象再用這個(gè)Configuration構(gòu)建DefaultSqlSessionFactory。public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { XMLConfigBuilder parser new XMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); }XMLConfigBuilder的構(gòu)造過程本身就有玄機(jī)。它會先創(chuàng)建一個(gè)XPathParser然后在構(gòu)造方法里立刻調(diào)用parserContext.getXPathParser().evalNode(/configuration)把根節(jié)點(diǎn)拿出來再通過new Configuration()初始化一個(gè)全新的Configuration實(shí)例。這一步同時(shí)決定了后面所有配置項(xiàng)的落點(diǎn)。2.2 parse() 方法的執(zhí)行流程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; }注意parsed標(biāo)識位這個(gè)字段是用來防止同一個(gè)XMLConfigBuilder實(shí)例被重復(fù)使用的。如果你在代碼里手動(dòng)創(chuàng)建XMLConfigBuilder并調(diào)了兩次parse()第二次直接拋BuilderException。真正的干活方法是parseConfiguration它把每個(gè)子標(biāo)簽的解析拆成了獨(dú)立方法private void parseConfiguration(XNode root) { try { propertiesElement(root.evalNode(properties)); Properties settings settingsAsProperties(root.evalNode(settings)); loadCustomVfs(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode(typeAliases)); pluginsElement(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)); mappersElement(root.evalNode(mappers)); } catch (Exception e) { throw new BuilderException(Error parsing SQL Mapper Configuration. Cause: e, e); } }看到?jīng)]有雖然parseConfiguration內(nèi)部按順序執(zhí)行但實(shí)際執(zhí)行順序和 DTD 順序略有出入。尤其是typeHandlers居然排在environments之后才解析這在配置的使用層面很容易產(chǎn)生誤區(qū)。很多人寫配置文件時(shí)把 typeHandlers 放在 settings 之后以為解析順序也是如此其實(shí)源碼里它是在后面才被加載的。如果你在 typeHandlers 里注冊了一個(gè)自定義 TypeHandler而在 environments 階段就想用它做數(shù)據(jù)源連接配置那必然是不行的。還有一個(gè)細(xì)節(jié)settingsAsProperties會先把所有 settings 的值讀成Properties對象然后傳給settingsElement統(tǒng)一設(shè)置到Configuration上。這中間做了一步容錯(cuò)——如果某個(gè)屬性名在Configuration上沒有對應(yīng)的 setter會直接跳過而不報(bào)錯(cuò)。這個(gè)“靜默忽略”的設(shè)計(jì)我后面再展開講。2.3 XPathParserMyBatis 自研 XML 解析器XMLConfigBuilder之所以能這么從容地逐段解析底層依賴的是 MyBatis 自研的XPathParser。這個(gè)類封裝了 Java 標(biāo)準(zhǔn)庫的XMLMapperEntityResolver和XPath表達(dá)式求值能力。XPathParser的核心是createDocument方法它使用 Java 的DocumentBuilderFactory創(chuàng)建 DOM 文檔對象。這里有個(gè)關(guān)鍵配置factory.setFeature(http://apache.org/xml/features/disallow-doctype-decl, false); factory.setFeature(http://xml.org/sax/features/external-general-entities, false); factory.setFeature(http://xml.org/sax/features/external-parameter-entities, false); factory.setXIncludeAware(false); factory.setExpandEntityReferences(false);這套配置是為了在解析 XML 時(shí)禁用外部實(shí)體和 XInclude防止 XXE 攻擊。同時(shí)它注冊了XMLMapperEntityResolver——這個(gè)類的作用是當(dāng) XML 文件里出現(xiàn)http://mybatis.org/dtd/mybatis-3-config.dtd這類 DTD 引用時(shí)不真正去網(wǎng)絡(luò)上下載而是從 MyBatis jar 包里的本地資源直接加載。這既保證了配置合法性校驗(yàn)又避免了啟動(dòng)時(shí)依賴外網(wǎng)。理解了XPathParser你就知道 MyBatis 解析 XML 不是用一堆正則硬拆字符串而是把整個(gè)文檔加載成 DOM再用 XPath 精準(zhǔn)定位節(jié)點(diǎn)。這樣的好處是標(biāo)簽嵌套結(jié)構(gòu)天然合法屬性值可以走標(biāo)準(zhǔn) XML 轉(zhuǎn)義解析邏輯寫起來也清晰得多。3. 核心配置項(xiàng)逐個(gè)解析與背后原理3.1 settings全局行為開關(guān)settings標(biāo)簽是日常開發(fā)里用得最多的配置像mapUnderscoreToCamelCase、cacheEnabled、lazyLoadingEnabled都在這里。前面說過settingsAsProperties會先把子標(biāo)簽拼成Properties然后通過settingsElement統(tǒng)一設(shè)置。settingsElement里面有個(gè)很有意思的機(jī)制它先給Configuration設(shè)了一堆默認(rèn)值再用反射逐個(gè)匹配屬性名調(diào)用 setter。Properties defaults new Properties(); defaults.put(cacheEnabled, true); defaults.put(lazyLoadingEnabled, false); defaults.put(aggressiveLazyLoading, true); defaults.put(multipleResultSetsEnabled, true); defaults.put(useColumnLabel, true); defaults.put(useGeneratedKeys, false); defaults.put(autoMappingBehavior, PARTIAL); defaults.put(autoMappingUnknownColumnBehavior, NONE); // ... 還有一堆核心邏輯是Configuration configuration new Configuration(environments); for (Map.EntryObject, Object entry : props.entrySet()) { MetaObject metaObject configuration.getSystemMetaObject(); Object value convertValue(metaObject, entry.getKey().toString(), entry.getValue().toString()); metaObject.setValue(entry.getKey().toString(), value); }這里用的是MetaObject反射工具。convertValue會根據(jù)目標(biāo)屬性的類型做轉(zhuǎn)換比如布爾值、Integer、Long 等。所以哪怕配置文件里寫的是字符串true最終也能賦給boolean類型的屬性。如果Configuration上根本沒有這個(gè)屬性setValue會拋出反射異常。但前面說它“靜默忽略”是因?yàn)樵趕ettingsAsProperties階段已經(jīng)對未知配置做過過濾并不是所有非法配置都會直接成功——具體行為取決于 MyBatis 版本和異常處理方式。但這引出一個(gè)常見誤區(qū)MyBatis 對拼錯(cuò)的配置項(xiàng)名稱容忍度很高拼錯(cuò)了可能不會報(bào)錯(cuò)但配置不生效排查起來非常惡心。最好啟動(dòng)后主動(dòng)打印一份生效配置做對照。3.2 typeAliases 與 typeHandlers注冊規(guī)則與加載時(shí)機(jī)typeAliases解析時(shí)有三種寫法typeAliases typeAlias aliasuser typecom.example.model.User/ package namecom.example.model/ /typeAliases解析邏輯有兩個(gè)分支typeAlias標(biāo)簽逐個(gè)注冊package標(biāo)簽則掃描包下的所有類。對于包掃描MyBatis 會過濾掉匿名類、內(nèi)部類、接口并且只注冊能被class.isAnnotationPresent(Alias.class)注解標(biāo)記的類如果沒有注解就用類名首字母小寫作為別名。這里有個(gè)細(xì)節(jié)包掃描默認(rèn)不注冊沒有Alias注解的類但如果有Alias注解就以注解值為準(zhǔn)。typeHandlers的解析比 typeAliases 晚但策略類似。它支持三種注冊方式typeHandlers typeHandler handlercom.example.MyTypeHandler/ typeHandler javaTypejava.time.LocalDate handlercom.example.LocalDateTypeHandler jdbcTypeDATE/ package namecom.example.handler/ /typeHandlers源碼里typeHandlerElement會根據(jù)是否有javaType屬性決定注冊范圍。沒有javaType時(shí)只會把 handler 和對應(yīng)的MappedJdbcTypes如果存在注冊有javaType時(shí)則用typeHandlerRegistry.register(javaType, jdbcType, handler)做精確注冊。自定義 TypeHandler 最容易被坑的點(diǎn)是如果你既沒有在 handler 類上寫MappedJdbcTypes也沒有在 XML 里指定javaType和jdbcType那么注冊后 MyBatis 并不知道這個(gè) handler 該用在哪種類型上結(jié)果就是怎么都不生效。3.3 plugins攔截器解析與動(dòng)態(tài)代理鏈全局配置里的plugins就是很多人說的 MyBatis 攔截器配置入口。它的解析代碼很短private void pluginsElement(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); } } }注意它在這里只是把攔截器實(shí)例化并放進(jìn)configuration的攔截器鏈里真正創(chuàng)建動(dòng)態(tài)代理是在后面構(gòu)建Executor、StatementHandler、ParameterHandler、ResultSetHandler時(shí)才發(fā)生。這是很多面試官愛問的點(diǎn)插件解析不是“配置完就代理”而是延遲到對象創(chuàng)建時(shí)通過InterceptorChain.pluginAll()一步步包裝。3.4 environments數(shù)據(jù)源與事務(wù)工廠的組裝environments標(biāo)簽我單獨(dú)拿出來講是因?yàn)樗慕馕鲋苯雨P(guān)系到最后SqlSessionFactory使用哪個(gè)數(shù)據(jù)源、哪種事務(wù)管理器。源碼先是讀取default屬性確定默認(rèn)環(huán)境 ID然后遍歷子節(jié)點(diǎn)找匹配的environmentprivate 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()); break; } } } }這里DataSourceFactory有三種內(nèi)置實(shí)現(xiàn)UNPOOLED、POOLED、JNDI。POOLED是 MyBatis 自帶的連接池實(shí)現(xiàn)很多人以為 MyBatis 只依賴第三方連接池其實(shí)它自己有一套簡易連接池。UnpooledDataSource則是每用一次連接就新建一個(gè)Connection性能開銷大多用于測試環(huán)境。3.5 mappers映射器的加載與綁定mappers是解析流程的壓軸環(huán)節(jié)也是最容易引發(fā)級聯(lián)問題的地方。mappersElement支持四種子標(biāo)簽mapper resource、mapper url、mapper class、package name。resource從 classpath 加載 Mapper XML 文件最常見。url從文件系統(tǒng)或網(wǎng)絡(luò) URL 加載測試時(shí)常用。class直接指定 Mapper 接口如果接口和 XML 同包同名能自動(dòng)找到 XML。package掃描整個(gè)包按接口關(guān)聯(lián) XML需要接口和 XML 同名同路徑。解析到resource時(shí)會調(diào)用XMLMapperBuilder.parse()。這一步會把 Mapper XML 里的cache、resultMap、sql、select|insert|update|delete全部解析注冊進(jìn)Configuration。同時(shí)bindMapperForNamespace()會做接口綁定。如果 Mapper XML 的 namespace 對應(yīng)的接口存在會調(diào)用configuration.addMapper()最終通過MapperAnnotationBuilder解析接口上的注解 SQL。這里常會遇到一個(gè)經(jīng)典報(bào)錯(cuò)Invalid bound statement (not found)。大部分原因就是 Mapper XML 沒被加載進(jìn)來。你可以檢查全局配置里mappers的resource路徑是否和 classpath 對得上或者用package方式時(shí)接口和 XML 是否放在同一目錄、同名文件。4. 解析過程中的擴(kuò)展點(diǎn)與設(shè)計(jì)精髓4.1 為什么 MyBatis 要自研 XPathParser 而不是直接用 DOM/SAX很多初學(xué)者會問既然 Java 自帶DocumentBuilderFactoryMyBatis 為什么不直接用非得搞個(gè)XPathParser其實(shí)XPathParser本質(zhì)上還是用 JAXP 的 DOM 能力但包了一層“XPath 友好”的 API。原來的DocumentBuilderFactory用起來要用NodeList反復(fù)遍歷子節(jié)點(diǎn)代碼寫一大坨。XPathParser直接用evalNode(/configuration/settings)一步到位再通過XNode封裝了getStringAttribute、getChildrenAsProperties、evalString這些常用操作。XNode這個(gè)類是閱讀配置解析源碼時(shí)繞不開的它把 DOM 的Node包裝成更實(shí)用的模型底層存了xpathParser、node、attributes、name、body、children等字段所有取值操作都經(jīng)過它。我在讀源碼時(shí)最佩服的一點(diǎn)是XNode的evalString方法會調(diào)用PropertyParser做變量替換。這意味著你在配置里寫的${jdbc.url}這類占位符不是靠 Spring 處理而是PropertyParser從Properties里查出來替換掉。這跟properties標(biāo)簽里配置的外部文件、property子項(xiàng)以及構(gòu)造時(shí)傳入的Properties參數(shù)都有關(guān)系。解析順序不同占位符能否解析成功也不一樣。理解這一點(diǎn)你就明白為什么properties要放在配置最前面了。4.2 懶加載資源與失敗容忍機(jī)制XMLConfigBuilder里還有兩個(gè)容易被忽視的細(xì)節(jié)VFS和Log的加載。loadCustomVfs(settings)允許你通過配置vfsImpl自定義虛擬文件系統(tǒng)。MyBatis 默認(rèn)用DefaultVFS掃描包下的類文件但在某些容器環(huán)境比如老的 WebLogic里默認(rèn)掃描不到需要自定義 VFS。loadCustomLogImpl(settings)根據(jù)配置的logImpl決定使用 Slf4j、Log4j2、JDK Logging 還是 StdOut。沒有配置時(shí)會按優(yōu)先級自動(dòng)探測這個(gè)探測結(jié)果會全局決定你看到的 SQL 日志長什么樣。這兩個(gè)加載動(dòng)作同樣走“反射實(shí)例化 設(shè)置屬性”的套路失敗會拋BuilderException。但相比 settings 的“靜默容錯(cuò)”它們對錯(cuò)誤更敏感。畢竟 VFS 和日志實(shí)現(xiàn)掛了后面解析和運(yùn)行都沒法玩。4.3 二級緩存初始化與解析的關(guān)系想搞懂 MyBatis 二級緩存建議從Configuration的cacheEnabled字段看起。settingsElement里會把它設(shè)成true但二級緩存的真正注冊發(fā)生在 Mapper XML 解析時(shí)也就是說全局配置只負(fù)責(zé)開關(guān)每個(gè) Mapper 里要不要開緩存、用什么淘汰策略都是在mappers解析階段完成的。XMLMapperBuilder.cacheElement()會讀取cache標(biāo)簽的eviction、flushInterval、size、readOnly、blocking等屬性并用CacheBuilder構(gòu)建緩存實(shí)例。perpetualCache是所有緩存的底層層結(jié)構(gòu)再按配置包裹上LruCache、FifoCache、SoftCache、WeakCache等裝飾器。很多人面試被問“MyBatis 一級二級緩存有什么區(qū)別”如果只背結(jié)論不如順著這個(gè)解析鏈路理解一級緩存是Executor內(nèi)部自帶的LocalCache二級緩存是Configuration上維護(hù)的Cache對象兩者生命周期完全不同。我遇到過一個(gè)問題某個(gè) Mapper 明明配置了cache但查詢結(jié)果一直沒走二級緩存。后來排查發(fā)現(xiàn)全局配置的cacheEnabled被設(shè)置成了false導(dǎo)致Executor構(gòu)建時(shí)直接繞過了二級緩存邏輯。所以排查緩存問題時(shí)第一件事永遠(yuǎn)是看cacheEnabled到底是不是true再看 Mapper 有沒有flushCachefalse、有沒有在增刪改后清緩存。只盯著全局配置或只盯著 Mapper 配置都會掉坑。5. 實(shí)戰(zhàn)配置調(diào)優(yōu)與自定義擴(kuò)展點(diǎn)5.1 啟動(dòng)后打印實(shí)際生效的 Configuration前面提到 settings 對拼寫錯(cuò)誤容忍度高那怎么快速知道實(shí)際配置最簡單的方法是寫一個(gè)啟動(dòng)監(jiān)聽器或直接在初始化代碼里輸出SqlSessionFactory factory new SqlSessionFactoryBuilder().build(inputStream); Configuration configuration factory.getConfiguration(); System.out.println(cacheEnabled configuration.isCacheEnabled()); System.out.println(lazyLoadingEnabled configuration.isLazyLoadingEnabled()); System.out.println(mapUnderscoreToCamelCase configuration.isMapUnderscoreToCamelCase()); System.out.println(defaultExecutorType configuration.getDefaultExecutorType());像我前面說的熱詞里“mybatis配置打印”相關(guān)的問題多半就是想搞清楚當(dāng)前生效值。除了逐個(gè) getter也可以看Configuration類的settings相關(guān)字段或反射遍歷所有屬性。不過日常排查用 getter 就足夠了重點(diǎn)對照你想驗(yàn)證的那幾個(gè)開關(guān)。5.2 自定義 TypeHandler 的正確姿勢我建議自定義 TypeHandler 時(shí)統(tǒng)一用BaseTypeHandler這比實(shí)現(xiàn)TypeHandler接口省事得多。比如定義一個(gè)把LocalDateTime轉(zhuǎn)成字符串的 handlerMappedTypes(LocalDateTime.class) MappedJdbcTypes(JdbcType.VARCHAR) public class LocalDateTimeStringTypeHandler extends BaseTypeHandlerLocalDateTime { private static final DateTimeFormatter FORMATTER DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss); Override public void setNonNullParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) throws SQLException { ps.setString(i, parameter.format(FORMATTER)); } Override public LocalDateTime getNullableResult(ResultSet rs, String columnName) throws SQLException { String value rs.getString(columnName); return value null ? null : LocalDateTime.parse(value, FORMATTER); } Override public LocalDateTime getNullableResult(ResultSet rs, int columnIndex) throws SQLException { String value rs.getString(columnIndex); return value null ? null : LocalDateTime.parse(value, FORMATTER); } Override public LocalDateTime getNullableResult(CallableStatement cs, int columnIndex) throws SQLException { String value cs.getString(columnIndex); return value null ? null : LocalDateTime.parse(value, FORMATTER); } }注冊時(shí)建議用 XML 顯式指定javaType和jdbcType不要只靠注解。注解只在 MyBatis 掃描時(shí)生效而MappedJdbcTypes的值如果沒匹配上實(shí)際 JDBC 類型還是會走默認(rèn)處理器。也別在包掃描的目錄里放太多“半成品” handler掃描到非TypeHandler類型會直接注冊失敗拖慢啟動(dòng)。5.3 自定義配置項(xiàng)直接改源碼還是另起爐灶有時(shí)候全局配置滿足不了團(tuán)隊(duì)定制需求比如想在配置里加一個(gè)audit標(biāo)簽。直接改XMLConfigBuilder源碼是最徹底的方案但升級 MyBatis 時(shí)會有沖突。更推薦的做法是不要在全局配置里亂加標(biāo)簽而是在 Spring 里用ConfigurationCustomizer或者在構(gòu)建完成后手動(dòng)往Configuration里塞自定義對象。如果你確實(shí)需要完整的自定義解析鏈路一種思路是復(fù)制一份XMLConfigBuilder在parseConfiguration里插入自定義節(jié)點(diǎn)解析邏輯然后讓SqlSessionFactoryBuilder使用自定義 Builder。但這種方案離“改框架”不遠(yuǎn)團(tuán)隊(duì)里必須有人持續(xù)維護(hù)。我的經(jīng)驗(yàn)是除非需求極其特殊否則盡可能復(fù)用 MyBatis 已有的擴(kuò)展點(diǎn)比如Interceptor、TypeHandler、ObjectFactory這些擴(kuò)展點(diǎn)覆蓋了絕大多數(shù)字段映射和 SQL 執(zhí)行場景。6. 常見問題與排查技巧實(shí)錄6.1 “元素類型必須由匹配的結(jié)束標(biāo)記終止”我見過最多人問的報(bào)錯(cuò)就是The element type settings must be terminated by the matching end-tag /settings。絕大多數(shù)情況是標(biāo)簽閉合寫錯(cuò)了或者標(biāo)簽之間大小寫不對。MyBatis 的 DTD 對大小寫敏感settings寫成了Settings一樣報(bào)錯(cuò)。另一個(gè)常見原因是 XML 注釋沒寫好!-- 這里注釋了 settings 標(biāo)簽但沒注釋完整 -- settings setting namemapUnderscoreToCamelCase valuetrue/ !-- /settings --這種注釋錯(cuò)誤很難發(fā)現(xiàn)建議用 IDE 的 XML 校驗(yàn)功能提前檢查。6.2 Configuration 解析順序報(bào)錯(cuò)嚴(yán)格按 DTD 順序?qū)懪渲檬亲钍⌒牡牧?xí)慣。我見過一個(gè)項(xiàng)目把mappers寫在settings前面結(jié)果啟動(dòng)時(shí) Mapper 里引用的別名還沒注冊直接拋TypeException。這里記一個(gè)排查技巧看到BuilderException: Error parsing SQL Mapper Configuration先不要急大概率不是 Mapper 的 SQL 寫錯(cuò)了而是全局配置階段有問題。用--debug啟動(dòng)應(yīng)用在XMLConfigBuilder.parseConfiguration打上斷點(diǎn)一步步看是哪個(gè)方法拋的異常。順序相關(guān)的報(bào)錯(cuò)還有一個(gè)The content of element type configuration must match ...這是 DTD 校驗(yàn)直接攔截了順序錯(cuò)誤。這時(shí)去看 XML 中 configuration 下的子元素順序按 DTD 調(diào)整即可。6.3 typeAliases/typeHandlers 不生效不生效基本只有三種情況包掃描路徑寫錯(cuò)掃描不到目標(biāo)類。類上沒有注解并且沒在 XML 里顯式指定 type。注意包掃描對沒有Alias的類默認(rèn)不按短類名注冊但如果你顯式使用typeAlias指定是可以注冊的。字段類型和 TypeHandler 的javaType對不上。調(diào)試方法很簡單在啟動(dòng)代碼里打印configuration.getTypeAliasRegistry().getTypeAliases()或者configuration.getTypeHandlerRegistry().getTypeHandlers()。這些 Map 的 key 就是當(dāng)前能識別的別名和處理器類型一眼就能看出注冊結(jié)果。我踩過一次最離譜的坑是在typeHandlers里包掃描配置了某個(gè)目錄但目錄里連著一個(gè)舊的、異常的 handler 類MyBatis 掃描時(shí)直接拋了InstantiationException整個(gè)啟動(dòng)流程掛掉。排查時(shí)才意識到掃描機(jī)制會把目錄下所有滿足條件的類都實(shí)例化這個(gè)行為相當(dāng)于“有多少注冊多少”目錄別放無關(guān)類。6.4 settings 配置“靜默失效”拼錯(cuò)配置名、值類型不對、格式不對都可能導(dǎo)致 settings 配置不生效。比如lazyLoadingEnabled寫成lazyLoadingEnableMyBatis 很大概率不會報(bào)錯(cuò)但功能不生效。為了避免這種“不知道改沒改上”的困惑我建議團(tuán)隊(duì)統(tǒng)一在啟動(dòng)日志里輸出關(guān)鍵配置項(xiàng)或者寫個(gè)單測去讀SqlSessionFactoryBuilder得到的Configuration對象斷言關(guān)鍵開關(guān)的值。如果遇到 “配置了mapUnderscoreToCamelCasetrue但查詢結(jié)果 map 的 key 仍然帶下劃線” 這種問題還有一個(gè)可能你在代碼里手動(dòng)用ResultSetHandler或Map接收結(jié)果MyBatis 并不會對Map的 key 做駝峰轉(zhuǎn)換這個(gè)開關(guān)只對自動(dòng)映射到 POJO 屬性時(shí)生效。很多人忽略了這個(gè)邊界。6.5 不同版本之間解析差異MyBatis 3.4、3.5 這些大版本之間配置解析的容錯(cuò)行為有明顯差異。比如早期版本遇到未知 settings 配置項(xiàng)可能直接拋異常后續(xù)版本改成了更寬松的處理。升級版本后如果發(fā)現(xiàn)“之前跑得好好的配置文件突然報(bào)錯(cuò)”多半是版本差異導(dǎo)致的。建議升級時(shí)先讀一下對應(yīng)版本的release notes重點(diǎn)看Configuration類字段和XMLConfigBuilder的解析邏輯有沒有變化。另外mybatis-config.dtd里的元素順序也可能調(diào)整如果升級后 DTD 校驗(yàn)報(bào)錯(cuò)直接重新生成一份配置文件別手動(dòng)猜。下面整理一個(gè)速查表方便直接對照報(bào)錯(cuò)現(xiàn)象大概率原因快速解法The content of element type configuration must matchconfiguration 子元素順序不符合 DTD按 properties - settings - typeAliases - ... - mappers 順序調(diào)整BuilderException: Error parsing SQL Mapper Configuration全局配置階段出錯(cuò)常見為別名/TypeHandler 未注冊斷點(diǎn)定位XMLConfigBuilder.parseConfiguration看具體方法TypeException: Could not resolve type alias類型別名未注冊或包掃描路徑錯(cuò)誤打印TypeAliasRegistry的注冊表核對路徑Invalid bound statement (not found)Mapper XML 沒被mappers加載檢查 resource 路徑、package 掃描目錄、namespace 與接口全限定名自定義 TypeHandler 不生效缺少javaType/jdbcType指定或MappedJdbcTypes不匹配顯式在 XML 中指定并在注冊后打印 TypeHandlerRegistrysettings 配置未生效配置名拼錯(cuò)或值類型不對用Configuration的 getter 打印實(shí)際值斷點(diǎn)調(diào)試settingsElement7. 從源碼回到日常我的幾點(diǎn)實(shí)操體會把XMLConfigBuilder這段源碼啃完最大的收獲不是“哦原來有個(gè)類叫這個(gè)”而是以后看到配置相關(guān)報(bào)錯(cuò)腦子里會自動(dòng)浮現(xiàn)出解析流程的先后順序知道該在哪個(gè)環(huán)節(jié)打斷點(diǎn)。我個(gè)人的習(xí)慣是新建項(xiàng)目時(shí)永遠(yuǎn)保持一份最小可用的mybatis-config.xml里面只放當(dāng)前真正需要的配置項(xiàng)。MyBatis 有大量默認(rèn)值不需要的配置不寫反而能減少拼寫錯(cuò)誤和配置漂移的問題。等線上出現(xiàn)性能或行為異常再按需往里面加配置并用啟動(dòng)日志驗(yàn)證。最后再分享一個(gè)小技巧讀這類源碼時(shí)先跑一個(gè)最簡單的 Demo用--debug跟著SqlSessionFactoryBuilder進(jìn)一遍再回到XMLConfigBuilder.parseConfiguration()里逐行看每個(gè)方法的執(zhí)行路徑比你對著源碼干想效率高得多。親自跑一遍比我在這里講一萬字都管用。