REF CURSOR 必须显式关闭,Connection.close() 不释放游标;需对 ResultSet 调用 close(),推荐 try-with-resources;OracleParameter 类型、方向、BindByName 三者缺一不可;CLOB 流读取须与 ResultSet 同生命周期。

REF CURSOR 必须显式关闭,Connection.close() 不起作用
Oracle 的 REF CURSOR 是服务器端资源,绑定在数据库会话上,和 JDBC 的 Connection 生命周期无关。哪怕用了 HikariCP 或 Druid 连接池,调用 connection.close() 只是把物理连接归还给池,底层 Oracle 会话依然存活,REF CURSOR 对应的游标句柄不会自动释放。
常见错误是以为“只要 Connection 关了,里面所有东西就清了”,结果每调一次存储过程就多挂一个游标,直到触发 ORA-01000: maximum open cursors exceeded。
- 必须对每个
REF CURSOR输出参数拿到的ResultSet显式调用close() - 推荐用
try (ResultSet rs = (ResultSet) cs.getObject(2)) { ... },确保异常时也释放 - 禁用语句级缓存(如 MyBatis 的
useStatementCache=true),它会让PreparedStatement复用,但游标仍由每次执行新开
Java 中调用带 REF CURSOR 的存储过程时,参数类型不能错
Oracle JDBC 驱动要求严格声明 REF CURSOR 类型,否则无法识别结构,轻则返回空结果,重则抛 ORA-06550 或 InvalidCastException。
关键配置项只有三个:OracleDbType.RefCursor、ParameterDirection.Output、BindByName = true。漏任一都会出问题。
-
OracleParameter的OracleDbType必须设为OracleDbType.RefCursor,不能用Types.OTHER或留空 - 方向必须是
ParameterDirection.Output(存储过程)或ParameterDirection.ReturnValue(极少见的函数返回) -
BindByName = true要打开,避免因参数顺序错位导致游标被绑定到非预期位置
流式读 CLOB 字段时,Reader 和 ResultSet 必须同生命周期关闭
从 REF CURSOR 返回的 ResultSet 若含 CLOB 字段,直接调 getClob().getSubString() 或 toString() 不仅会 OOM,还会让 LOB locator 和关联游标双重泄漏——因为 Oracle 驱动内部用游标维护 LOB 状态。
正确做法是用流,但流的生命期严格依附于当前行的 ResultSet。一旦调了 rs.next(),前一行的 Reader 就失效,再读会抛 SQLException。
- 用
rs.getCharacterStream("col_name")获取Reader - 必须包裹在
try-with-resources中,且该try必须嵌套在while (rs.next())循环内 - 缓冲区大小设
8192足够;避免用BufferedReader.readLine(),它内部缓冲不可控
存储过程中 RETURNING REF CURSOR 后,JDBC 端不能手动 CLOSE 游标变量
PL/SQL 里定义的 PROCEDURE p_list(p_out OUT SYS_REFCURSOR),在 Java 端拿到 ResultSet 后,ResultSet.close() 会通知 Oracle 释放游标;但如果在 PL/SQL 里对 p_out 手动执行 CLOSE p_out,JDBC 再调 rs.close() 就会报错,因为句柄已失效。
更隐蔽的问题是:某些框架(如旧版 Spring JDBC)会尝试在 finally 块里重复 close,若 PL/SQL 已关,就会触发 ORA-01001: invalid cursor。
- PL/SQL 中只做
OPEN p_out FOR ...,不要加CLOSE p_out - JDBC 端负责全部生命周期管理,包括异常路径下的
close() - 检查是否有多层包装(如自定义 DAO 拦截器),避免对同一
ResultSet多次 close
真正容易被忽略的是:REF CURSOR 泄漏往往不是单点 bug,而是多个环节松耦合造成的——比如 DAO 层关了 ResultSet,但 Service 层又把它传给另一个方法继续用;或者日志拦截器偷偷调了 rs.toString() 导致隐式加载并卡住流。查问题时得顺着数据流,从存储过程出口一直跟到最终 close 调用点。











