拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Caused by: java.lang.ClassNotFoundException: org.apache.ibatis.cursor.Cursor解决方法:SSM 项目 mybatis-spri

Caused by: java.lang.ClassNotFoundException: org.apache.ibatis.cursor.Cursor解决方法:SSM 项目 mybatis-spri

1. SSM 启动就报 Cursor 找不到,先别急着改代码

java.lang.ClassNotFoundException: org.apache.ibatis.cursor.Cursor这个报错,第一次见的人很容易以为是自己的 Mapper 写错了,或者 XML 里 resultType 配错了。实际上它跟你的业务代码基本没关系,问题出在依赖版本上。org.apache.ibatis.cursor.Cursor是 MyBatis 从 3.4.0 开始才引入的接口,用来支持流式查询(Cursor 查询)。如果你的mybatis核心包版本低于 3.4.0,但mybatis-spring用的是 1.3.x,那么 Spring 在启动扫描 Mapper 方法签名时,会去反射读取方法参数类型,一旦碰到Cursor这个类型,类加载器找不到,就直接抛NoClassDefFoundError,紧接着Caused by: ClassNotFoundException。

这个场景在 SSM(Spring + SpringMVC + MyBatis)整合项目里特别常见,尤其是从网上抄了一份 pom,或者用 IDE 自动补全依赖时,Maven 帮你选了一个「看起来能用」的版本组合。典型症状是:项目编译通过,Tomcat 启动到finishBeanFactoryInitialization阶段突然崩,堆栈里能看到LocalVariableTableParameterNameDiscoverer、ConstructorResolver.autowireConstructor这些 Spring 内部类,最后一行才是Caused by: java.lang.ClassNotFoundException: org.apache.ibatis.cursor.Cursor。很多人盯着最后一行找,其实真正的线索在NoClassDefFoundError和mybatis-spring的版本上。

适合谁看:正在做 SSM 整合、用 Maven 管理依赖、启动时报 Cursor 缺失的 Java 后端同学。看完你能自己用mvn dependency:tree定位版本冲突,把mybatis和mybatis-spring对齐到兼容组合,并且用统一的 API 通道验证接口是否真的恢复正常,而不是靠反复重启碰运气。

我试过在一个老项目里,pom 里mybatis写的是 3.2.8,mybatis-spring写的是 1.3.2,启动必崩。把mybatis升到 3.4.1 之后,问题当场消失。下面把完整排查和修复过程拆开讲。

2. 用 mvn dependency:tree 定位 mybatis-spring 版本错配

在动手改 pom 之前,先确认到底是谁把mybatis拉成了低版本。Maven 的依赖调解规则是「最短路径优先」,如果mybatis-spring自己声明了对mybatis的依赖,而你又没显式写mybatis的版本,那最终生效的可能是mybatis-spring传递进来的版本。mybatis-spring1.3.x 的 POM 里对mybatis的依赖是provided或者带版本范围的,不同小版本行为不一样,这就是坑的来源。

第一步,在项目根目录执行依赖树命令,只看 mybatis 相关的分支:

mvn dependency:tree -Dincludes=org.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

看到没,这里出现了两个mybatis:一个是mybatis-spring:1.3.1传递进来的 3.4.1,另一个是你自己显式声明的 3.2.8。Maven 最终会选哪个?取决于声明顺序和路径长度。如果 3.2.8 是你直接写在<dependencies>里的,路径更短,它就会赢,于是运行时加载的是 3.2.8,而 3.2.8 里根本没有Cursor接口,mybatis-spring1.3.1 又偏偏要用它,冲突就爆了。

如果输出里出现omitted for conflict或者omitted for duplicate,说明 Maven 已经帮你做了取舍,你要看清楚被省略的是哪个版本。更稳妥的做法是用-Dverbose参数:

mvn dependency:tree -Dverbose -Dincludes=org.mybatis:mybatis

它会打印出被省略的节点和原因,比如(version managed from 3.4.1; omitted for conflict with 3.2.8)。这一步能让你明确知道「谁赢了、谁被丢了」。

还有一种情况是父 POM 或者dependencyManagement里锁死了mybatis版本。这时候dependency:tree显示的是最终生效版本,但你看 pom 里写的可能是另一个。检查方法是在项目里搜dependencyManagement,看有没有对org.mybatis的版本声明。如果有,子模块里再写版本号是无效的,必须改父 POM 或者用属性覆盖。

定位清楚之后,记住一个兼容原则:mybatis-spring1.3.x 需要mybatis3.4.0 及以上。官方文档里mybatis-spring1.3.0 的说明是「requires MyBatis 3.4.0 or higher」。所以只要把mybatis提到 3.4.1,mybatis-spring用 1.3.1,这一对就是稳的。下面给出可复制的配置。

3. 可复制的 pom 依赖配置:mybatis 3.4.1 与 mybatis-spring 1.3.1 对齐

修复的核心就一句话:显式声明mybatis版本,并且让它不低于 3.4.0,同时mybatis-spring用 1.3.x。下面这段可以直接贴进pom.xml的<dependencies>里。注意 groupId 是org.mybatis,不是org.mybatis.spring,后者是老的包名,别写错。

<properties> <mybatis.version>3.4.1</mybatis.version> <mybatis-spring.version>1.3.1</mybatis-spring.version> <spring.version>4.3.30.RELEASE</spring.version> </properties> <dependencies> <!-- MyBatis 核心包,必须 >= 3.4.0 才有 Cursor 接口 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <!-- MyBatis 与 Spring 整合包,1.3.x 对应 mybatis 3.4.x --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <!-- Spring 相关,按你项目实际版本调整 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> </dependencies>

如果你用的是dependencyManagement统一管理版本,把上面两个 mybatis 依赖的版本声明挪到dependencyManagement里,子模块只写 groupId 和 artifactId:

<dependencyManagement> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.4.1</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.1</version> </dependency> </dependencies> </dependencyManagement>

改完之后,一定要重新拉依赖并刷新 IDE。命令行执行:

mvn clean compile -U

-U强制更新快照和 release 元数据,避免本地仓库缓存了旧的 POM。IDEA 用户再点一次 Maven 面板的刷新按钮,确保External Libraries里mybatis-3.4.1.jar已经出现,而不是 3.2.8。

这里有个容易忽略的点:mybatis-spring1.3.1 的 POM 里对mybatis的依赖 scope 是provided,意思是它不会主动帮你传递mybatis。所以你必须自己显式声明mybatis,否则运行时会报NoClassDefFoundError: org/apache/ibatis/session/SqlSessionFactory之类的错。很多人只加了mybatis-spring就以为够了,这是另一个常见坑。

配置对齐后,Spring 的SqlSessionFactoryBean在初始化时就能正常反射到Cursor类型,LocalVariableTableParameterNameDiscoverer不会再抛异常。接下来验证接口是否真的恢复。

4. 验证请求:用统一 API 通道确认依赖与接口恢复正常

依赖改完、项目能启动,不代表 Mapper 接口调用就 100% 正常。有时候Cursor类加载问题解决了,但 XML 映射或者事务配置还有隐患。这时候可以用一个统一的 API 通道来跑一次真实请求,确认从 Controller 到 Service 到 Mapper 的链路是通的。

TaoToken 提供统一的 Key 和 API 入口,适合在本地调试阶段快速验证接口。它的 API 地址是https://taotoken.net/api,控制台里可以创建 API Key,文档里有各语言调用示例。下面用 curl 演示一次请求,你可以把它替换成你项目里任意一个查询接口的路径。

先准备环境变量,避免 Key 写死在命令里:

export TAOTOKEN_API_KEY="你的APIKey" export TAOTOKEN_BASE_URL="https://taotoken.net/api"

然后发一个请求,这里以模型对话接口为例,验证通道是否可用:

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 和通道都正常。这一步的意义在于:把「网络/鉴权」和「业务代码」分开验证。如果这个请求通了,但你的 SSM 接口还是 500,那问题就在 Mapper 或事务配置,而不是依赖版本。

接着验证你的业务接口。假设你有一个/user/list的 GET 接口,用 curl 打一次:

curl -sS "http://localhost:8080/ssm-demo/user/list" -H "Accept: application/json"

预期返回一个 JSON 数组,里面是用户数据。如果返回 500,去看 Tomcat 日志里有没有Cursor相关的异常。如果Cursor异常消失了,但出现Invalid bound statement (not found),那是 Mapper XML 的 namespace 或 id 对不上,跟版本无关。

对于 Cursor 流式查询本身,可以写一个最小的测试 Mapper 方法来验证。在UserMapper接口里加:

Cursor<User> selectAllByCursor();

XML 里对应:

<select id="selectAllByCursor" resultType="com.example.entity.User"> select id, name, age from user </select>

Service 里调用时注意,Cursor 必须在事务内使用,否则会报Cursor is closed:

@Transactional public void streamUsers() { try (Cursor<User> cursor = userMapper.selectAllByCursor()) { cursor.forEach(user -> System.out.println(user.getName())); } catch (IOException e) { throw new RuntimeException(e); } }

如果这段代码能跑通,说明org.apache.ibatis.cursor.Cursor已经被正确加载,版本对齐彻底完成。实测下来,只要mybatis是 3.4.1、mybatis-spring是 1.3.1,这个 Cursor 查询在 SSM 里是稳定的。

5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth

修完版本问题后,验证阶段还可能碰到几类报错。下面按真实报错信息对照排查。

第一类,401 Unauthorized。用 curl 调 TaoToken 接口时返回:

{"error":{"message":"Invalid API key provided","type":"invalid_request_error"}}

原因通常是 Key 复制时带了空格,或者环境变量没生效。检查方法:

echo "${TAOTOKEN_API_KEY}" | wc -c

如果长度明显不对,重新在控制台创建 Key。注意请求头格式是Authorization: Bearer sk-xxx,Bearer 和 Key 之间一个空格,别多别少。

第二类,local proxy failed或connection refused。这通常是你本地配了 HTTP 代理,但代理没启动,或者代理地址写错了。检查环境变量:

env | grep -i proxy

如果有http_proxy或https_proxy,临时清掉再试:

unset http_proxy https_proxy

第三类,reading choices相关报错,比如json: cannot unmarshal ... reading 'choices'。这多半是请求体格式不对,比如messages写成了字符串而不是数组,或者model字段拼错。对照文档里的请求示例逐字段检查。还有一种可能是返回的不是 JSON,而是 HTML 错误页,用curl -i看响应头里的Content-Type就能确认。

第四类,OAuth相关报错。如果你在配置里用了 OAuth 流程,但回调地址或者 client_id 不对,会报invalid_grant或redirect_uri_mismatch。这类问题跟 MyBatis 无关,属于鉴权配置,检查控制台里的回调地址是否和代码里一致。

第五类,回到 MyBatis 本身。如果启动时报NoClassDefFoundError: org/apache/ibatis/cursor/Cursor变成了NoSuchMethodError,说明版本对了但方法签名不匹配,通常是mybatis-spring和mybatis跨了大版本。坚持 3.4.1 + 1.3.1 这一对,不要混用 2.x 的mybatis-spring。

排查时记住一个顺序:先看Caused by最后一行是什么类缺失,再用mvn dependency:tree确认实际生效版本,最后用 curl 把网络和业务分开验证。这样能避免在无关的地方浪费时间。

6. 把 Key、Base URL、Model ID 三件套固定下来,后续接入更省事

版本问题解决后,如果你还要在项目里接入模型能力,或者用 Coding Plan 做长期编码辅助,建议把三件套固定成配置项:Base URL、API Key、Model ID。这样换环境时只改配置,不动代码。

以settings.json或auth.json这类配置文件为例,结构大致如下:

{ "baseUrl": "https://taotoken.net/api", "apiKey": "sk-你的Key", "model": "gpt-4o-mini" }

如果你用的是 Cline 或类似的编码插件,MCP 配置里同样需要这三项。Base URL 填https://taotoken.net/api,Key 从控制台的 API Keys 页面获取,Model ID 按文档里支持的模型名填。三件套对齐后,本地调试和线上切换只需要改一个文件。

需要创建 Key 的话,走 API Keys 页面;想看完整接入示例,走接入文档;想先验证模型是否可用,用模型对话页面发一条消息即可;如果是长期编码或 Agent 场景,Coding Plan 更合适。把依赖版本和 API 通道都固定下来,下次再遇到ClassNotFoundException,你就能直接定位到是版本还是配置,而不是从头猜。

返回列表