
1. Java 调用 Oracle 存储过程返回 Cursor 到底难在哪如果你写过 MySQL 的存储过程调用再切到 Oracle大概率会在SYS_REFCURSOR这个类型上卡一下。MySQL 里存储过程直接SELECT就能把结果集吐回来Java 端拿ResultSet就行但 Oracle 不一样它把「游标」当成一种显式的 OUT 参数你必须先在数据库里声明一个REF CURSOR类型再在 Java 端用CallableStatement注册这个 OUT 参数最后从getObject()里把它还原成ResultSet。少任何一步要么报类型不匹配要么拿到的永远是null。这篇就聚焦一个典型场景Java 通过 JDBC 调用 Oracle 存储过程接收SYS_REFCURSOR返回值把结果集遍历出来。我会把存储过程定义、连接配置、CallableStatement注册 OUT 参数、类型映射、结果集遍历、以及验证查询条数的执行动作全部给全代码可以直接复制改改就用。适合谁看正在做 Oracle 老系统对接、报表数据抽取、或者被ORA-03115: unsupported network datatype or representation折磨过的后端同学。核心检索词先摆出来Java 调用 Oracle 存储过程返回 Cursor本质是 JDBC 对OracleTypes.CURSOR的注册与还原。搞懂这一条后面全是细节。先说清楚一个容易混的点。很多人把SYS_REFCURSOR和普通游标搞混。普通游标CURSOR是 PL/SQL 里FOR ... LOOP用的不能直接跨会话传而REF CURSOR是一个指向结果集的指针可以当参数传进传出。Oracle 从 9i 开始内置了SYS_REFCURSOR这个弱类型引用游标你不需要自己TYPE ... IS REF CURSOR也能用。但如果你要兼容老代码或者想约束返回的列结构自己定义强类型游标也完全可以。下面两种写法我都会给。还有一个坑是驱动版本。ojdbc8及以上对OracleTypes.CURSOR的支持最稳ojdbc14那种老驱动在 JDK 8 上跑起来经常出幺蛾子。如果你用的是 Maven直接上com.oracle.database.jdbc:ojdbc8或者ojdbc11别去网上随便下个 jar 丢 lib 里版本对不上报错能查到你怀疑人生。2. 前置准备TaoToken 与 Oracle 环境怎么配在写 Java 代码之前先把两件事准备好一个是数据库侧的存储过程和包一个是连接配置。这里顺便说下如果你平时还要调大模型来辅助写 SQL 或者生成 JDBC 样板代码可以用 TaoToken 这类聚合入口统一管理 API Key省得每个平台单独配。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 地址是 https://taotoken.net/api 注册后在控制台拿 Key 就行。不过这篇的重点还是 OracleTaoToken 只是顺带提一句别跑偏。回到数据库。先建包和存储过程。我用一个查询用户表的例子输入一个编号返回一个游标。注意SYS_REFCURSOR的用法-- 如果要用自定义强类型游标先建包 create or replace package pack1 is type test_cursor is ref cursor; end pack1; / -- 方式一用 SYS_REFCURSOR推荐简单 create or replace procedure pro1( test_uNo in number, test_res out sys_refcursor ) is begin open test_res for select user_id, user_name from users where user_id test_uNo; end pro1; / -- 方式二用自定义游标类型 create or replace procedure pro2( test_uNo in number, test_res out pack1.test_cursor ) is begin open test_res for select user_id, user_name from users where user_id test_uNo; end pro2; /建完之后在 SQL 客户端里先手动验证一下存储过程本身没问题-- 在 SQL*Plus 或 PL/SQL Developer 里执行 var rc refcursor; exec pro1(95, :rc); print rc;如果这一步能看到结果集说明数据库侧 OK。看不到就先去查存储过程别急着写 Java。接下来是连接配置。我习惯用db.properties放在src/main/resources下内容大概这样connection.driver_classoracle.jdbc.OracleDriver connection.urljdbc:oracle:thin://127.0.0.1:1521/ORCLPDB1 connection.usernamescott connection.passwordtiger注意 URL 的写法。jdbc:oracle:thin://host:port/service_name是服务名方式jdbc:oracle:thin:host:port:SID是 SID 方式两者别写混。现在新库基本都是服务名用双斜杠那种。端口默认 1521服务名去tnsnames.ora或者问 DBA 要。Maven 依赖也贴一下省得你到处找dependency groupIdcom.oracle.database.jdbc/groupId artifactIdojdbc8/artifactId version21.9.0.0/version /dependency驱动类名是oracle.jdbc.OracleDriverJDBC 4.0 之后其实可以省略Class.forName但为了兼容老容器我还是习惯显式注册一次。这里有个细节Class.forName只需要执行一次放在静态块里最合适别每次 new 连接都注册一遍浪费。3. 可复制配置CallableStatement 注册 OUT 参数全流程这一节是核心我把工具类和调用代码拆开写你可以直接抄。先看工具类重点在callProcedureReturnCursor这个方法它负责拼{call pro1(?,?)}、设置 IN 参数、注册 OUT 参数、执行。import java.io.InputStream; import java.sql.*; import java.util.Properties; public class OracleHelper { private static String DRIVER; private static String USERNAME; private static String PASSWORD; private static String URL; private Connection connection null; private CallableStatement clstatement null; static { Properties props new Properties(); try (InputStream fis OracleHelper.class.getClassLoader() .getResourceAsStream(db.properties)) { props.load(fis); DRIVER props.getProperty(connection.driver_class); USERNAME props.getProperty(connection.username); PASSWORD props.getProperty(connection.password); URL props.getProperty(connection.url); Class.forName(DRIVER); } catch (Exception e) { throw new RuntimeException(读取数据库配置失败, e); } } public OracleHelper() { try { connection DriverManager.getConnection(URL, USERNAME, PASSWORD); } catch (SQLException e) { throw new RuntimeException(获取数据库连接失败, e); } } /** * 执行返回游标的存储过程 * param sql 形如 {call pro1(?,?)} * param inParams IN 参数数组 * param outTypes OUT 参数类型数组游标用 OracleTypes.CURSOR */ public CallableStatement callProcedureReturnCursor( String sql, Object[] inParams, int[] outTypes) { try { clstatement connection.prepareCall(sql); if (inParams ! null) { for (int i 0; i inParams.length; i) { clstatement.setObject(i 1, inParams[i]); } } if (outTypes ! null) { for (int i 0; i outTypes.length; i) { // OUT 参数位置 IN 参数个数 1 i clstatement.registerOutParameter( inParams.length 1 i, outTypes[i]); } } clstatement.execute(); } catch (SQLException e) { throw new RuntimeException(执行存储过程失败: e.getMessage(), e); } return clstatement; } public void close(ResultSet rs, Statement st, Connection conn) { if (rs ! null) try { rs.close(); } catch (SQLException ignored) {} if (st ! null) try { st.close(); } catch (SQLException ignored) {} if (conn ! null) try { conn.close(); } catch (SQLException ignored) {} } public Connection getConnection() { return connection; } public CallableStatement getClstatement() { return clstatement; } }几个关键点必须说透。第一registerOutParameter的位置索引是「IN 参数个数 1 i」因为 JDBC 的参数位置是从 1 开始连续编号的IN 占前面OUT 接后面。如果你有 2 个 IN、1 个 OUT那 OUT 就是第 3 位。第二游标的类型常量是oracle.jdbc.OracleTypes.CURSOR值是-10别写成Types.OTHER虽然某些驱动能兼容但 Oracle 官方推荐用OracleTypes.CURSOR。第三execute()之后不要急着关CallableStatement因为结果集还挂在它上面提前关会导致ResultSet失效。调用代码长这样public class TestCall { public static void main(String[] args) { OracleHelper helper new OracleHelper(); ResultSet rs null; try { String sql {call pro1(?,?)}; Object[] inParams {95}; int[] outTypes {oracle.jdbc.OracleTypes.CURSOR}; CallableStatement cs helper.callProcedureReturnCursor( sql, inParams, outTypes); // 注意OUT 参数从第 2 位取因为第 1 位是 IN rs (ResultSet) cs.getObject(2); int count 0; while (rs.next()) { int userId rs.getInt(user_id); String userName rs.getString(user_name); System.out.println(userId - userName); count; } System.out.println(共返回 count 条记录); } catch (Exception e) { e.printStackTrace(); } finally { helper.close(rs, helper.getClstatement(), helper.getConnection()); } } }这里cs.getObject(2)的 2 就是 OUT 参数的位置。如果你有多个 OUT按注册顺序依次getObject(2)、getObject(3)。取出来强转成ResultSet就能遍历了。列名建议用rs.getInt(user_id)这种按名取比按索引getInt(1)稳因为存储过程里select *的列顺序可能变。4. 验证请求跑通并确认结果条数代码写完直接跑main方法。如果数据库里有user_id 95的数据控制台会一行行打印出来最后输出「共返回 N 条记录」。这个 N 就是验证的关键——它必须和你直接在 SQL 客户端执行select count(*) from users where user_id 95的结果一致。我实测下来最容易出问题的不是代码逻辑而是环境。比如你本地连的是测试库但存储过程建在了另一个 schema 下调用时就得写成{call schema_name.pro1(?,?)}否则报ORA-06550: PLS-00201: identifier PRO1 must be declared。再比如权限当前用户如果没有执行该存储过程的权限会报ORA-01031: insufficient privileges得让 DBA 授权grant execute on pro1 to your_user。验证的时候我建议分三步走。第一步在 SQL 客户端手动exec一次确认存储过程本身返回的数据对。第二步Java 里先只打印rs是否为 null确认 OUT 参数取到了。第三步再遍历打印。这样出问题能快速定位是数据库侧还是 Java 侧。还有一个细节SYS_REFCURSOR返回的结果集在ResultSet关闭之前CallableStatement和Connection都不能关。我见过有人在while循环里就把cs.close()调了结果rs.next()直接抛SQLException: Closed Connection。顺序永远是先关ResultSet再关CallableStatement最后关Connection也就是finally里那个close方法的顺序。如果你要统计条数除了在 Java 里count也可以在存储过程里多返回一个 OUT 参数专门放count(*)这样一次调用拿两个结果。不过大多数场景下Java 端遍历时计数就够了没必要改存储过程。5. 常见报错排查401、local proxy failed、reading choices 对照这一节我把调用 Oracle 存储过程时高频出现的报错列出来对照着查。ORA-03115: unsupported network datatype or representation这个基本就是 OUT 参数类型注册错了。你可能用了Types.OTHER或者Types.REF_CURSOR但 Oracle 驱动只认OracleTypes.CURSOR。改成oracle.jdbc.OracleTypes.CURSOR就好。另外确认驱动是ojdbc8及以上。ORA-01000: maximum open cursors exceeded游标没关。每次调用存储过程都会打开一个游标如果ResultSet没关游标数会一直涨超过open_cursors参数就报这个。检查finally里有没有关ResultSet。临时可以调大open_cursors但根治还是靠正确关闭。java.sql.SQLException: 无效的列索引getObject(2)里的 2 越界了。数一下你的 IN 和 OUT 参数总数OUT 的位置不能超过总参数数。比如只有 1 个 IN、1 个 OUT那 OUT 就是第 2 位getObject(2)对如果写成getObject(3)就报这个。local proxy failed 类连接错误这个通常不是 Oracle 本身的错而是你本地网络层或者某些客户端工具在转发时出的问题。如果你在用某个本地代理工具连数据库先把它关掉直连试试。数据库连接走的是 TCP 1521不需要任何额外转发。这类报错和存储过程代码无关别去改 Java。reading choices 相关报错如果你在调用链里混用了某些 AI 辅助工具生成的代码可能会看到类似reading choices的字段解析错误。这多半是返回结构和你预期的不一致比如把ResultSet当成了 JSON 去解析。回到 JDBC 本身getObject拿到的就是ResultSet别做多余转换。ORA-06550 / PLS-00201存储过程名或包名写错或者没加 schema 前缀。确认{call pro1(?,?)}里的名字和数据库里一致大小写不敏感但拼写要对。驱动类找不到 ClassNotFoundException: oracle.jdbc.OracleDriverMaven 依赖没下下来或者 jar 没进 classpath。检查pom.xml里ojdbc8的 scope别写成provided又没在容器里提供。排查顺序建议先看报错码ORA- 开头去查数据库侧java.sql.开头查 Java 侧连接类错误查网络和配置。别一上来就改代码。6. 长期编码与 Agent 场景下的接入建议如果你只是偶尔调一次 Oracle 存储过程上面这套代码够用了。但如果你在做长期的数据同步、报表平台或者用 Agent 自动生成 JDBC 代码那有几个工程化建议。第一把OracleHelper改成连接池版本。DriverManager.getConnection每次新建连接开销大生产环境用 HikariCP 或者 Druid配置里把connectionTestQuery设成select 1 from dual。连接池的db.properties和上面一样只是 URL 换成池的配置。第二存储过程调用建议封装成 DAO 方法别在业务代码里裸写{call ...}。比如UserDao.findUsersAfterId(int id)内部调pro1返回ListUser。这样存储过程改名或者换库时只改一处。第三如果你用 Cline、CC Switch 这类工具做 MCP 接入或者用 Codex 的auth.json配模型记得把 Base URL、Key、Model ID 三件套写全。比如接 TaoToken 的话Base URL 用 https://taotoken.net/api Key 在控制台生成Model ID 按你选的模型填。Coding Plan 适合长期写代码的场景模型对话适合临时验证 SQL 逻辑接入文档里有完整的参数说明。这些和 Oracle 调用是两条线别混在一起配。第四结果集遍历时如果数据量大别一次性全读进内存。用rs.setFetchSize(500)控制每次从数据库拉取的条数配合流式处理。Oracle 驱动默认 fetch size 是 10调大能明显减少网络往返。最后说个真实经验Oracle 的SYS_REFCURSOR在跨版本迁移时偶尔会有兼容问题比如从 11g 迁到 19c某些老驱动注册 OUT 参数会失败。遇到这种情况先升级ojdbc到和数据库版本匹配的版本再检查存储过程里的游标类型是不是用了废弃语法。大部分时候换个驱动就解决了。代码跑通之后你可以把pro1换成任何返回游标的存储过程只要改{call ...}里的名字和参数个数工具类不用动。这就是把这套配置抽出来的好处。