Druid Cursor 流式查询报 SQLFeatureNotSupportedException?TaoToken 这样改 Codex 接入后查 closeOnCompletion
2026/9/17 1:40:29 网站建设 项目流程

1. 报错现场:Cursor 流式查询在 executeForCursor 撞上 closeOnCompletion

1.1 报错栈里真正有用的三行

用 Druid 处理 Cursor 流式查询时,项目在MybatisMapperMethod.executeForCursor处直接抛java.sql.SQLFeatureNotSupportedException,这种报错通常和 SQL 无关。TaoToken 只是 Codex 的兼容通道,不负责连接池行为,但排查这类版本问题,我会先把 Key 从 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 拿到手,再让 Codex 顺着报错栈读 Druid 源码。整套走下来,问题最终集中在 Druid 的一个 JDBC 4.1 可选特性上。

日志去掉 Spring AOP 的几百行后,关键信息只有三段:

Caused by: java.sql.SQLFeatureNotSupportedException at com.alibaba.druid.pool.DruidPooledStatement.closeOnCompletion(DruidPooledStatement.java:871) ... at org.apache.ibatis.executor.SimpleExecutor.doQueryCursor(SimpleExecutor.java:75)

SimpleExecutor.doQueryCursor执行完handler.queryCursor(stmt)后,紧接着调用了stmt.closeOnCompletion()。这个调用被 MyBatis 的日志代理PreparedStatementLogger.invoke转发到 Druid 的DruidPooledStatement,结果老版本 Druid 对这个方法完全不买账,直接抛异常。

closeOnCompletion是 JDBC 4.1 为“结果集耗尽后自动关闭 Statement”设计的钩子。MyBatis 的 Cursor 采用流式遍历,遍历完结果集再让 Statement 自动关闭,正好命中这个特性。但 Druid 1.1.13 对这个方法的实现比较敷衍,于是流式查询连第一个next()都到不了。

1.2 这是连接池版本问题,不是 Mapper 写法问题

这个异常真正让人头疼的地方在于发生时机。它不是连接获取失败,不是 SQL 执行失败,而是 Cursor 已经构造好、MyBatis 准备优雅收尾的那一刻炸出来。很多团队看到SQLFeatureNotSupportedException会先怀疑数据库驱动不支持游标,或者 MyBatis-Plus 的Cursor用法不对,实际上只要看到DruidPooledStatement这七个字,基本就能把怀疑对象锁定在 Druid 版本上。

当时项目里druid-spring-boot-starter锁的是 1.1.13。它和 MyBatis-Plus 的BaseMapper配合时,普通selectList不受影响,因为那种查询不需要调用closeOnCompletion;只有返回Cursor<T>的方法会走到这条路径。所以问题很隐蔽,往往在大数据量导出、分批处理的场景里突然冒出来,还容易被误判成“结果集太大导致驱动不支持”。

2. 排查思路:从 MyBatis 源码一路读到 Druid 连接池

2.1 SimpleExecutor 为什么要先拿 Cursor 再关 Statement

既然报错栈指向SimpleExecutor.doQueryCursor,那就先看这个方法。MyBatis 在这里的逻辑可以拆成四步:

  1. newStatementHandler创建语句处理器;
  2. 调用prepareStatement准备一个 JDBCStatement
  3. 调用handler.queryCursor(stmt)得到Cursor<E>对象;
  4. 立刻执行stmt.closeOnCompletion(),再把 Cursor 返回给调用方。

第 4 步是 MyBatis 的一个设计约定:你应用层慢慢遍历 Cursor,遍历完后驱动会自动关闭 Statement,这样连接不会被提前回收,也不至于因为忘记finally { cursor.close(); }而导致连接泄漏。

但这个约定依赖底层 JDBC 驱动的“自觉”。Druid 作为连接池,对外暴露的是DruidPooledStatement,MyBatis 拿到的其实是经过 JDBC 动态代理的包装对象。如果 Druid 不支持closeOnCompletion,问题就集中在第 4 步爆发,后面的 Mapper 调用链再长都是陪跑。

2.2 PreparedStatementLogger 只是忠实的传话筒

MyBatis 开启日志后,PreparedStatement会通过java.lang.reflect.Proxy做一层代理,由PreparedStatementLogger.invoke统一拦截。这个类对方法的处理分成几类:

  • executeQueryexecuteUpdate等执行方法会打印参数;
  • setStringsetInt等绑定参数的方法会记录列信息;
  • getResultSetgetUpdateCount会包装返回值;
  • 其他方法一律method.invoke(statement, params)原样转发。

closeOnCompletion就落在“其他”这一类。所以PreparedStatementLogger.invoke看到异常后,只是通过ExceptionUtil.unwrapThrowable把最底层的异常解包出来,没有做任何额外处理。这说明问题不在 MyBatis 的日志代理,而在被代理的statement对象本身。

从排障角度讲,看到这种“MyBatis 代理层原样抛出”的模式,就应该直接放弃在 Mapper 层找原因,转而对比连接池实现。这也是为什么后来我会选择用 Codex 来做源码对比,而不是继续人肉翻 JAR 包。

3. 用 TaoToken 配通 Codex,再让 AI 帮我读源码

3.1 准备材料:在模型广场创建 API Key

到了这一步,单纯靠肉眼读源码也能定位,但效率不高。我的做法是让 Codex 帮忙把DruidPooledStatement在旧版和新版之间的实现差异列出来。要用 Codex,先得准备一把能访问模型的 API Key。

打开 TaoToken,注册登录后进控制台创建 Key。注意 Key 的占位符是YOUR_API_KEY,不要填成控制台页面里的示例字符。TaoToken 的模型广场会展示当前可用的模型 ID,你需要记下自己准备用的那一个,后面填进 Codex 配置里。

这样做的价值在于:官方额度分散在多把 Key 里时,切来切去很容易把模型搞混。这个统一接入通道允许在一个控制台里看到多个模型的调用情况,Codex 这类 OpenAI 兼容工具只需要改一个 base_url 就能共用密钥,省去为某个模型单独充值的麻烦。

3.2 Codex 配置:~/.codex/config.toml 里加一个 provider

Codex CLI 的配置放在~/.codex/config.toml,建议先备份再改。下面是一份可用的配置:

model = "YOUR_MODEL_ID" model_provider = "taotoken" [model_providers.taotoken] name = "TaoToken" base_url = "https://taotoken.net/api" env_key = "TAOTOKEN_API_KEY"

字段说明:

  • model:以模型广场当时列表为准,填你要用的模型 ID;
  • model_provider:指定使用下面定义的这个 provider;
  • base_url:固定填https://taotoken.net/api,末尾不要加/v1
  • env_key:Codex 会读取这个环境变量的值作为 API Key。

写完后,在当前 shell 导出环境变量再启动:

export TAOTOKEN_API_KEY=YOUR_API_KEY codex

如果你已经在用其他 provider,不用删旧配置,[model_providers.taotoken]是独立的一段,Codex 支持多个 provider 并存。以后想切回官方模型,只要把model_provider改回去就行。

3.3 给 Codex 喂什么信息,它才能快速定位 closeOnCompletion

配好 Codex 之后,不要只说一句“帮我看下报错”。我通常会把下面三样东西一次性贴过去:

  1. 完整异常栈里Caused by附近的位置,特别是DruidPooledStatement.closeOnCompletion那一行;
  2. Mapper 接口中返回Cursor<T>的方法签名;
  3. pom.xml里 Druid 和 MyBatis-Plus 的版本号。

然后直接问:为什么 Cursor 流式查询会抛出SQLFeatureNotSupportedException

Codex 会先解释 MyBatis 的Cursor需要 Statement 实现closeOnCompletion,接着会建议去对比 Druid 1.1.13 和 1.2.19 的源码差异。它给出的判断基于你贴进来的信息,不会连接到生产库执行任何操作。后续要不要升级依赖、怎么改 pom,仍然由你在本地决定和验证。

4. 关键差异:Druid 两个版本的 closeOnCompletion 实现

4.1 1.1.13 版本的实现:直接抛异常

从 1.1.13 的源码中可以看到,DruidPooledStatement.closeOnCompletion的实现非常直白:

public void closeOnCompletion() throws SQLException { throw new SQLFeatureNotSupportedException(); }

这不是一个严格意义上的 bug,而是 Druid 在当时选择不实现 JDBC 4.1 的可选特性。问题在于 MyBatis 不区分“可选”和“必须”,直接把closeOnCompletion当成每个 Statement 都应支持的能力来用。两个框架的默认行为撞到一起,流式查询就废了。

4.2 1.2.19 版本的实现:透传给底层 Statement

到了 1.2.19,Druid 改成了标准写法:

public void closeOnCompletion() throws SQLException { this.stmt.closeOnCompletion(); }

这里的stmt是 Druid 连接池内部持有的真实 JDBC Statement。Druid 用这种方式把“是否支持自动关闭”的决定权交还给了真正的数据库驱动。如果你的数据库驱动不支持closeOnCompletion,异常会从驱动层抛出,至少能明确下一步要看驱动文档,而不是在连接池里反复查。

4.3 升级依赖,pom 里的改动

用 Codex 确认差异之后,改动反而很小。把druid-spring-boot-starter的版本从 1.1.13 升到 1.2.19 即可:

<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.19</version> </dependency>

升级后建议做两类验证:一类是原有的selectListselectPage等普通查询,确认连接池行为没有回归;另一类是Cursor<T>流式查询,确认closeOnCompletion不再抛异常。如果项目里配置了 Druid 的拦截器或自定义 Filter,也一并跑一遍,因为连接池从旧版本跨到新版本时,部分内部状态可能有变化。

5. 验证流式查询恢复,顺带解决更多 Cursor 问题

5.1 升级后如何判断问题真的解决

升级完依赖,重新编译启动,直接调用之前出错的 Mapper 方法。如果日志里不再出现SQLFeatureNotSupportedException,并且你能正常用cursor.forEach遍历到全部记录,说明 Druid 已经把closeOnCompletion委托给了底层 Statement。

这里有一个容易忽视的点:遍历完 Cursor 后,要确保 Cursor 被关闭。1.2.19 虽然实现了自动关闭语义,但如果你写了break提前跳出循环,连接依然可能占住不放。建议代码里保留try (Cursor<BomDtl> cursor = mapper.queryBomDtl(...))这种写法,把兜底主动权掌握在自己手里。

验证通过后,回到 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_content= 控制台,看一下刚才 Codex 的请求记录,确认模型 ID、Token 数量和计费都能对上。这样既能确认 TaoToken 通道正常,也不会在月底发现一笔来源不明的消费。

5.2 用同一把 Key 继续排查其他流式查询异常

这次排障留下的不只是 pom 里一个版本号,还有一套可复用的动作:遇到 Cursor 流式查询问题,先看Caused by最下层是哪个类抛的,再决定是升级连接池还是换驱动。Codex 接入 TaoToken 后,这类问题可以直接丢给它做静态分析。你只需要把报错栈、框架版本和 Mapper 方法签名贴进去,它会试着给出排查路径。

需要注意的是:Codex 或任何 AI 工具都不应该被描述成“帮你连接生产库执行 SQL 诊断”。它更适合生成 SQL、解释报错、对比源码;真正在数据库客户端里跑那条 SQL,或者在生产环境执行诊断命令,必须由你本人在本地完成,再把结果贴回对话。这样既符合安全习惯,也能避免故障现场被误操作二次破坏。

如果接下来想把 Codex 长期当作 Druid、MyBatis-Plus 这类框架问题的辅助排查工具,可以在 模型对话 里先验证一下模型选择是否顺畅;要长时间写代码、频繁调用,再看 Coding Plan 的套餐是否更划算。新增或轮换 API Key 就回 控制台 API Keys 操作,三步之内能完成,不用重装 Codex,也不用改 base_url。

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

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

立即咨询