目 mybatis-spri)
1. SSM 啟動(dòng)就報(bào) Cursor 找不到先別急著改代碼java.lang.ClassNotFoundException: org.apache.ibatis.cursor.Cursor這個(gè)報(bào)錯(cuò)第一次見的人很容易以為是自己的 Mapper 寫錯(cuò)了或者 XML 里 resultType 配錯(cuò)了。實(shí)際上它跟你的業(yè)務(wù)代碼基本沒關(guān)系問題出在依賴版本上。org.apache.ibatis.cursor.Cursor是 MyBatis 從 3.4.0 開始才引入的接口用來支持流式查詢Cursor 查詢。如果你的mybatis核心包版本低于 3.4.0但mybatis-spring用的是 1.3.x那么 Spring 在啟動(dòng)掃描 Mapper 方法簽名時(shí)會(huì)去反射讀取方法參數(shù)類型一旦碰到Cursor這個(gè)類型類加載器找不到就直接拋NoClassDefFoundError緊接著Caused by: ClassNotFoundException。這個(gè)場景在 SSMSpring SpringMVC MyBatis整合項(xiàng)目里特別常見尤其是從網(wǎng)上抄了一份 pom或者用 IDE 自動(dòng)補(bǔ)全依賴時(shí)Maven 幫你選了一個(gè)「看起來能用」的版本組合。典型癥狀是項(xiàng)目編譯通過Tomcat 啟動(dòng)到finishBeanFactoryInitialization階段突然崩堆棧里能看到LocalVariableTableParameterNameDiscoverer、ConstructorResolver.autowireConstructor這些 Spring 內(nèi)部類最后一行才是Caused by: java.lang.ClassNotFoundException: org.apache.ibatis.cursor.Cursor。很多人盯著最后一行找其實(shí)真正的線索在NoClassDefFoundError和mybatis-spring的版本上。適合誰看正在做 SSM 整合、用 Maven 管理依賴、啟動(dòng)時(shí)報(bào) Cursor 缺失的 Java 后端同學(xué)。看完你能自己用mvn dependency:tree定位版本沖突把mybatis和mybatis-spring對(duì)齊到兼容組合并且用統(tǒng)一的 API 通道驗(yàn)證接口是否真的恢復(fù)正常而不是靠反復(fù)重啟碰運(yùn)氣。我試過在一個(gè)老項(xiàng)目里pom 里mybatis寫的是 3.2.8mybatis-spring寫的是 1.3.2啟動(dòng)必崩。把mybatis升到 3.4.1 之后問題當(dāng)場消失。下面把完整排查和修復(fù)過程拆開講。2. 用 mvn dependency:tree 定位 mybatis-spring 版本錯(cuò)配在動(dòng)手改 pom 之前先確認(rèn)到底是誰把mybatis拉成了低版本。Maven 的依賴調(diào)解規(guī)則是「最短路徑優(yōu)先」如果mybatis-spring自己聲明了對(duì)mybatis的依賴而你又沒顯式寫mybatis的版本那最終生效的可能是mybatis-spring傳遞進(jìn)來的版本。mybatis-spring1.3.x 的 POM 里對(duì)mybatis的依賴是provided或者帶版本范圍的不同小版本行為不一樣這就是坑的來源。第一步在項(xiàng)目根目錄執(zhí)行依賴樹命令只看 mybatis 相關(guān)的分支mvn dependency:tree -Dincludesorg.mybatis:mybatis,org.mybatis:mybatis-spring輸出大概長這樣[INFO] --- maven-dependency-plugin:3.1.1:tree (default-cli) --- [INFO] com.example:ssm-demo:war:1.0-SNAPSHOT [INFO] - org.mybatis:mybatis-spring:jar:1.3.1:compile [INFO] | \- org.mybatis:mybatis:jar:3.4.1:compile [INFO] \- org.mybatis:mybatis:jar:3.2.8:compile看到?jīng)]這里出現(xiàn)了兩個(gè)mybatis一個(gè)是mybatis-spring:1.3.1傳遞進(jìn)來的 3.4.1另一個(gè)是你自己顯式聲明的 3.2.8。Maven 最終會(huì)選哪個(gè)取決于聲明順序和路徑長度。如果 3.2.8 是你直接寫在dependencies里的路徑更短它就會(huì)贏于是運(yùn)行時(shí)加載的是 3.2.8而 3.2.8 里根本沒有Cursor接口mybatis-spring1.3.1 又偏偏要用它沖突就爆了。如果輸出里出現(xiàn)omitted for conflict或者omitted for duplicate說明 Maven 已經(jīng)幫你做了取舍你要看清楚被省略的是哪個(gè)版本。更穩(wěn)妥的做法是用-Dverbose參數(shù)mvn dependency:tree -Dverbose -Dincludesorg.mybatis:mybatis它會(huì)打印出被省略的節(jié)點(diǎn)和原因比如(version managed from 3.4.1; omitted for conflict with 3.2.8)。這一步能讓你明確知道「誰贏了、誰被丟了」。還有一種情況是父 POM 或者dependencyManagement里鎖死了mybatis版本。這時(shí)候dependency:tree顯示的是最終生效版本但你看 pom 里寫的可能是另一個(gè)。檢查方法是在項(xiàng)目里搜dependencyManagement看有沒有對(duì)org.mybatis的版本聲明。如果有子模塊里再寫版本號(hào)是無效的必須改父 POM 或者用屬性覆蓋。定位清楚之后記住一個(gè)兼容原則mybatis-spring1.3.x 需要mybatis3.4.0 及以上。官方文檔里mybatis-spring1.3.0 的說明是「requires MyBatis 3.4.0 or higher」。所以只要把mybatis提到 3.4.1mybatis-spring用 1.3.1這一對(duì)就是穩(wěn)的。下面給出可復(fù)制的配置。3. 可復(fù)制的 pom 依賴配置mybatis 3.4.1 與 mybatis-spring 1.3.1 對(duì)齊修復(fù)的核心就一句話顯式聲明mybatis版本并且讓它不低于 3.4.0同時(shí)mybatis-spring用 1.3.x。下面這段可以直接貼進(jìn)pom.xml的dependencies里。注意 groupId 是org.mybatis不是org.mybatis.spring后者是老的包名別寫錯(cuò)。properties mybatis.version3.4.1/mybatis.version mybatis-spring.version1.3.1/mybatis-spring.version spring.version4.3.30.RELEASE/spring.version /properties dependencies !-- MyBatis 核心包必須 3.4.0 才有 Cursor 接口 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- MyBatis 與 Spring 整合包1.3.x 對(duì)應(yīng) mybatis 3.4.x -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- Spring 相關(guān)按你項(xiàng)目實(shí)際版本調(diào)整 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId version${spring.version}/version /dependency /dependencies如果你用的是dependencyManagement統(tǒng)一管理版本把上面兩個(gè) mybatis 依賴的版本聲明挪到dependencyManagement里子模塊只寫 groupId 和 artifactIddependencyManagement dependencies dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.4.1/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version1.3.1/version /dependency /dependencies /dependencyManagement改完之后一定要重新拉依賴并刷新 IDE。命令行執(zhí)行mvn clean compile -U-U強(qiáng)制更新快照和 release 元數(shù)據(jù)避免本地倉庫緩存了舊的 POM。IDEA 用戶再點(diǎn)一次 Maven 面板的刷新按鈕確保External Libraries里mybatis-3.4.1.jar已經(jīng)出現(xiàn)而不是 3.2.8。這里有個(gè)容易忽略的點(diǎn)mybatis-spring1.3.1 的 POM 里對(duì)mybatis的依賴 scope 是provided意思是它不會(huì)主動(dòng)幫你傳遞mybatis。所以你必須自己顯式聲明mybatis否則運(yùn)行時(shí)會(huì)報(bào)NoClassDefFoundError: org/apache/ibatis/session/SqlSessionFactory之類的錯(cuò)。很多人只加了mybatis-spring就以為夠了這是另一個(gè)常見坑。配置對(duì)齊后Spring 的SqlSessionFactoryBean在初始化時(shí)就能正常反射到Cursor類型LocalVariableTableParameterNameDiscoverer不會(huì)再拋異常。接下來驗(yàn)證接口是否真的恢復(fù)。4. 驗(yàn)證請(qǐng)求用統(tǒng)一 API 通道確認(rèn)依賴與接口恢復(fù)正常依賴改完、項(xiàng)目能啟動(dòng)不代表 Mapper 接口調(diào)用就 100% 正常。有時(shí)候Cursor類加載問題解決了但 XML 映射或者事務(wù)配置還有隱患。這時(shí)候可以用一個(gè)統(tǒng)一的 API 通道來跑一次真實(shí)請(qǐng)求確認(rèn)從 Controller 到 Service 到 Mapper 的鏈路是通的。TaoToken 提供統(tǒng)一的 Key 和 API 入口適合在本地調(diào)試階段快速驗(yàn)證接口。它的 API 地址是https://taotoken.net/api控制臺(tái)里可以創(chuàng)建 API Key文檔里有各語言調(diào)用示例。下面用 curl 演示一次請(qǐng)求你可以把它替換成你項(xiàng)目里任意一個(gè)查詢接口的路徑。先準(zhǔn)備環(huán)境變量避免 Key 寫死在命令里export TAOTOKEN_API_KEY你的APIKey export TAOTOKEN_BASE_URLhttps://taotoken.net/api然后發(fā)一個(gè)請(qǐng)求這里以模型對(duì)話接口為例驗(yàn)證通道是否可用curl -sS ${TAOTOKEN_BASE_URL}/v1/chat/completions \ -H Authorization: Bearer ${TAOTOKEN_API_KEY} \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [ {role: user, content: ping} ], max_tokens: 16 }如果返回 JSON 里帶choices字段說明 Key 和通道都正常。這一步的意義在于把「網(wǎng)絡(luò)/鑒權(quán)」和「業(yè)務(wù)代碼」分開驗(yàn)證。如果這個(gè)請(qǐng)求通了但你的 SSM 接口還是 500那問題就在 Mapper 或事務(wù)配置而不是依賴版本。接著驗(yàn)證你的業(yè)務(wù)接口。假設(shè)你有一個(gè)/user/list的 GET 接口用 curl 打一次curl -sS http://localhost:8080/ssm-demo/user/list -H Accept: application/json預(yù)期返回一個(gè) JSON 數(shù)組里面是用戶數(shù)據(jù)。如果返回 500去看 Tomcat 日志里有沒有Cursor相關(guān)的異常。如果Cursor異常消失了但出現(xiàn)Invalid bound statement (not found)那是 Mapper XML 的 namespace 或 id 對(duì)不上跟版本無關(guān)。對(duì)于 Cursor 流式查詢本身可以寫一個(gè)最小的測試 Mapper 方法來驗(yàn)證。在UserMapper接口里加CursorUser selectAllByCursor();XML 里對(duì)應(yīng)select idselectAllByCursor resultTypecom.example.entity.User select id, name, age from user /selectService 里調(diào)用時(shí)注意Cursor 必須在事務(wù)內(nèi)使用否則會(huì)報(bào)Cursor is closedTransactional public void streamUsers() { try (CursorUser cursor userMapper.selectAllByCursor()) { cursor.forEach(user - System.out.println(user.getName())); } catch (IOException e) { throw new RuntimeException(e); } }如果這段代碼能跑通說明org.apache.ibatis.cursor.Cursor已經(jīng)被正確加載版本對(duì)齊徹底完成。實(shí)測下來只要mybatis是 3.4.1、mybatis-spring是 1.3.1這個(gè) Cursor 查詢在 SSM 里是穩(wěn)定的。5. 本篇常見錯(cuò)排查401、local proxy failed、reading choices、OAuth修完版本問題后驗(yàn)證階段還可能碰到幾類報(bào)錯(cuò)。下面按真實(shí)報(bào)錯(cuò)信息對(duì)照排查。第一類401 Unauthorized。用 curl 調(diào) TaoToken 接口時(shí)返回{error:{message:Invalid API key provided,type:invalid_request_error}}原因通常是 Key 復(fù)制時(shí)帶了空格或者環(huán)境變量沒生效。檢查方法echo ${TAOTOKEN_API_KEY} | wc -c如果長度明顯不對(duì)重新在控制臺(tái)創(chuàng)建 Key。注意請(qǐng)求頭格式是Authorization: Bearer sk-xxxBearer 和 Key 之間一個(gè)空格別多別少。第二類local proxy failed或connection refused。這通常是你本地配了 HTTP 代理但代理沒啟動(dòng)或者代理地址寫錯(cuò)了。檢查環(huán)境變量env | grep -i proxy如果有http_proxy或https_proxy臨時(shí)清掉再試unset http_proxy https_proxy第三類reading choices相關(guān)報(bào)錯(cuò)比如json: cannot unmarshal ... reading choices。這多半是請(qǐng)求體格式不對(duì)比如messages寫成了字符串而不是數(shù)組或者model字段拼錯(cuò)。對(duì)照文檔里的請(qǐng)求示例逐字段檢查。還有一種可能是返回的不是 JSON而是 HTML 錯(cuò)誤頁用curl -i看響應(yīng)頭里的Content-Type就能確認(rèn)。第四類OAuth相關(guān)報(bào)錯(cuò)。如果你在配置里用了 OAuth 流程但回調(diào)地址或者 client_id 不對(duì)會(huì)報(bào)invalid_grant或redirect_uri_mismatch。這類問題跟 MyBatis 無關(guān)屬于鑒權(quán)配置檢查控制臺(tái)里的回調(diào)地址是否和代碼里一致。第五類回到 MyBatis 本身。如果啟動(dòng)時(shí)報(bào)NoClassDefFoundError: org/apache/ibatis/cursor/Cursor變成了NoSuchMethodError說明版本對(duì)了但方法簽名不匹配通常是mybatis-spring和mybatis跨了大版本。堅(jiān)持 3.4.1 1.3.1 這一對(duì)不要混用 2.x 的mybatis-spring。排查時(shí)記住一個(gè)順序先看Caused by最后一行是什么類缺失再用mvn dependency:tree確認(rèn)實(shí)際生效版本最后用 curl 把網(wǎng)絡(luò)和業(yè)務(wù)分開驗(yàn)證。這樣能避免在無關(guān)的地方浪費(fèi)時(shí)間。6. 把 Key、Base URL、Model ID 三件套固定下來后續(xù)接入更省事版本問題解決后如果你還要在項(xiàng)目里接入模型能力或者用 Coding Plan 做長期編碼輔助建議把三件套固定成配置項(xiàng)Base URL、API Key、Model ID。這樣換環(huán)境時(shí)只改配置不動(dòng)代碼。以settings.json或auth.json這類配置文件為例結(jié)構(gòu)大致如下{ baseUrl: https://taotoken.net/api, apiKey: sk-你的Key, model: gpt-4o-mini }如果你用的是 Cline 或類似的編碼插件MCP 配置里同樣需要這三項(xiàng)。Base URL 填https://taotoken.net/apiKey 從控制臺(tái)的 API Keys 頁面獲取Model ID 按文檔里支持的模型名填。三件套對(duì)齊后本地調(diào)試和線上切換只需要改一個(gè)文件。需要?jiǎng)?chuàng)建 Key 的話走 API Keys 頁面想看完整接入示例走接入文檔想先驗(yàn)證模型是否可用用模型對(duì)話頁面發(fā)一條消息即可如果是長期編碼或 Agent 場景Coding Plan 更合適。把依賴版本和 API 通道都固定下來下次再遇到ClassNotFoundException你就能直接定位到是版本還是配置而不是從頭猜。