核心概念
在 Java 中调用存储过程,我们通常使用 JDBC(Java Database Connectivity),关键在于理解 Oracle 存储过程的几种不同类型以及对应的 JDBC 调用方式:

- 无参数过程:最简单的情况。
- 有输入参数的过程:Java 向过程传递数据。
- 有输出参数的过程:过程执行后返回数据到 Java。
- 有输入/输出参数的过程:参数既可以传入数据,也可以传出数据。
- 返回结果集的过程:过程返回一个类似
SELECT查询的结果集,这是最复杂的一种,需要使用 Oracle 特有的OracleCallableStatement和REF CURSOR。
准备工作:Oracle 驱动
确保你的项目中包含了 Oracle JDBC 驱动的 JAR 包,对于较新的 Oracle 数据库(如 11g, 12c, 19c),推荐使用 ojdbc8.jar 或 ojdbc11.jar。
如果你使用 Maven,可以在 pom.xml 中添加以下依赖:
<dependency>
<groupId>com.oracle.database.jdbc</groupId>
<artifactId>ojdbc8</artifactId>
<version>19.3.0.0</version> <!-- 请根据你的 Oracle 版本选择合适的版本 -->
</dependency>
无参数的过程
假设我们有一个存储过程,它只是执行一些操作,不返回任何值。
Oracle 存储过程示例
CREATE OR REPLACE PROCEDURE say_hello AS
BEGIN
DBMS_OUTPUT.PUT_LINE('Hello from Oracle Stored Procedure!');
END say_hello;
/
Java 代码示例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.SQLException;
public class CallSimpleProcedure {
// 数据库连接信息
private static final String DB_URL = "jdbc:oracle:thin:@your_host:your_port:your_service_name";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
// 使用 try-with-resources 确保 Connection 和 CallableStatement 被自动关闭
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall("{call say_hello()}")) {
System.out.println("Calling stored procedure...");
cstmt.execute(); // 执行存储过程
System.out.println("Procedure called successfully.");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
代码解释:

DriverManager.getConnection(...): 建立数据库连接。conn.prepareCall("{call say_hello()}"):prepareCall方法用于准备一个对存储过程或函数的调用。{call procedure_name()}是调用存储过程的语法格式。
cstmt.execute(): 执行存储过程。
有输入参数的过程
Java 向存储过程传递值。
Oracle 存储过程示例
CREATE OR REPLACE PROCEDURE greet_employee (p_emp_id IN NUMBER) AS
v_emp_name VARCHAR2(100);
BEGIN
SELECT first_name || ' ' || last_name INTO v_emp_name
FROM employees
WHERE employee_id = p_emp_id;
DBMS_OUTPUT.PUT_LINE('Hello, ' || v_emp_name || '!');
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Employee with ID ' || p_emp_id || ' not found.');
END greet_employee;
/
Java 代码示例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.SQLException;
public class CallProcedureWithInParam {
private static final String DB_URL = "jdbc:oracle:thin:@your_host:your_port:your_service_name";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
String sql = "{call greet_employee(?)}"; // ? 是一个占位符
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置输入参数
cstmt.setInt(1, 101); // 设置第一个 ? 为 101
System.out.println("Calling procedure with IN parameter...");
cstmt.execute();
System.out.println("Procedure called successfully.");
} catch (SQLException e) {
e.printStackTrace();
}
}
}
代码解释:
{call greet_employee(?)}: 是一个参数占位符。cstmt.setInt(1, 101): 为第一个(索引从1开始)参数设置int类型的值。setInt用于NUMBER或INT类型。setString用于VARCHAR2类型。setDate用于DATE类型,等等。
有输出参数的过程
存储过程执行后,将值返回给 Java。
Oracle 存储过程示例
CREATE OR REPLACE PROCEDURE get_employee_name (p_emp_id IN NUMBER, p_emp_name OUT VARCHAR2) AS
BEGIN
SELECT first_name || ' ' || last_name INTO p_emp_name
FROM employees
WHERE employee_id = p_emp_id;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_emp_name := 'Not Found';
END get_employee_name;
/
Java 代码示例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.SQLException;
public class CallProcedureWithOutParam {
private static final String DB_URL = "jdbc:oracle:thin:@your_host:your_port:your_service_name";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
// 注意 OUT 参数的语法是 ? OUT
String sql = "{call get_employee_name(?, ?)}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置 IN 参数
cstmt.setInt(1, 102);
// 注册 OUT 参数
// 必须在执行之前注册
cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);
System.out.println("Calling procedure with OUT parameter...");
cstmt.execute(); // 执行过程
// 获取 OUT 参数的值
String employeeName = cstmt.getString(2);
System.out.println("Employee Name: " + employeeName);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
代码解释:

cstmt.registerOutParameter(2, java.sql.Types.VARCHAR):- 这是最关键的一步,在执行
execute()之前,你必须告诉 JDBC 驱动哪个参数是输出参数,以及它的 SQL 类型。 java.sql.Types.VARCHAR对应 Oracle 的VARCHAR2。
- 这是最关键的一步,在执行
cstmt.getString(2): 在执行之后,通过索引获取输出参数的值。
有输入/输出参数的过程
参数既可以传入,也可以传出。
Oracle 存储过程示例
CREATE OR REPLACE PROCEDURE get_employee_info (
p_emp_id IN NUMBER,
p_emp_name OUT VARCHAR2,
p_salary IN OUT NUMBER
) AS
BEGIN
SELECT first_name || ' ' || last_name, salary
INTO p_emp_name, p_salary
FROM employees
WHERE employee_id = p_emp_id;
-- 给涨10%的工资(仅作为演示)
p_salary := p_salary * 1.10;
EXCEPTION
WHEN NO_DATA_FOUND THEN
p_emp_name := 'Not Found';
p_salary := 0;
END get_employee_info;
/
Java 代码示例
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.SQLException;
public class CallProcedureWithInOutParam {
private static final String DB_URL = "jdbc:oracle:thin:@your_host:your_port:your_service_name";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
// 注意 IN OUT 参数的语法是 ? INOUT
String sql = "{call get_employee_info(?, ?, ?)}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
int empId = 103;
double initialSalary = 5000.00;
// 设置 IN 参数
cstmt.setInt(1, empId);
// 设置 IN OUT 参数的初始值
cstmt.setDouble(3, initialSalary);
// 注册 OUT 参数 (第二个和第三个都是)
cstmt.registerOutParameter(2, java.sql.Types.VARCHAR);
cstmt.registerOutParameter(3, java.sql.Types.DOUBLE);
System.out.println("Calling procedure with IN OUT parameter...");
cstmt.execute();
// 获取 OUT 和 IN OUT 参数的返回值
String empName = cstmt.getString(2);
double finalSalary = cstmt.getDouble(3);
System.out.println("Employee ID: " + empId);
System.out.println("Employee Name: " + empName);
System.out.println("Initial Salary: " + initialSalary);
System.out.println("Final Salary (after 10% raise): " + finalSalary);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
代码解释:
- 对于
IN OUT参数,你需要同时使用setXxx()来设置初始值,并用registerOutParameter()来注册它用于接收返回值。 - 执行后,用
getXxx()获取其最终值。
返回结果集的过程(使用 REF CURSOR)
这是处理存储过程返回多行数据的最佳方式,Oracle 使用 REF CURSOR 来实现。
Oracle 存储过程示例
CREATE OR REPLACE PACKAGE types AS
-- 定义一个 REF CURSOR 类型
TYPE emp_cursor IS REF CURSOR;
END types;
/
CREATE OR REPLACE PROCEDURE get_all_employees (p_cursor OUT types.emp_cursor) AS
BEGIN
-- 打开游标,返回 employees 表的所有数据
OPEN p_cursor FOR
SELECT employee_id, first_name, last_name, email, hire_date
FROM employees
ORDER BY employee_id;
END get_all_employees;
/
Java 代码示例
这里需要使用 Oracle 特有的 OracleCallableStatement。
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import oracle.jdbc.OracleCallableStatement; // Oracle 特有的接口
public class CallProcedureWithResultSet {
private static final String DB_URL = "jdbc:oracle:thin:@your_host:your_port:your_service_name";
private static final String USER = "your_username";
private static final String PASS = "your_password";
public static void main(String[] args) {
// 注意语法: {call procedure_name(?, ?)}
// 第一个 ? 是 IN 参数(这里没有),第二个 ? 是 OUT 的 REF CURSOR
// Oracle 的 REF CURSOR 用 OracleTypes.CURSOR 表示
String sql = "{call get_all_employees(?)}";
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
// 必须使用 OracleCallableStatement 来处理 REF CURSOR
OracleCallableStatement ocstmt = (OracleCallableStatement) conn.prepareCall(sql)) {
// 注册 OUT 参数,类型为 OracleTypes.CURSOR
ocstmt.registerOutParameter(1, oracle.jdbc.OracleTypes.CURSOR);
System.out.println("Calling procedure that returns a result set...");
ocstmt.execute();
// 从 OUT 参数中获取 ResultSet
// 注意:对于 REF CURSOR,getCursor() �的是一个 ResultSet
ResultSet rs = ocstmt.getCursor(1);
System.out.println("Employee List:");
System.out.println("-------------------------------------------");
while (rs.next()) {
int id = rs.getInt("EMPLOYEE_ID");
String firstName = rs.getString("FIRST_NAME");
String lastName = rs.getString("LAST_NAME");
String email = rs.getString("EMAIL");
System.out.printf("ID: %d, Name: %s %s, Email: %s%n", id, firstName, lastName, email);
}
System.out.println("-------------------------------------------");
rs.close(); // 关闭 ResultSet
} catch (SQLException e) {
e.printStackTrace();
}
}
}
代码解释:
OracleCallableStatement: Oracle 扩展了CallableStatement,增加了对REF CURSOR的支持。ocstmt.registerOutParameter(1, oracle.jdbc.OracleTypes.CURSOR): 注册输出参数的类型为OracleTypes.CURSOR。ocstmt.getCursor(1): 执行后,调用getCursor()方法从输出参数中获取ResultSet对象。- 后续代码就和处理普通
SELECT查询的ResultSet一样了。
最佳实践和注意事项
- 使用
try-with-resources: 始终将Connection,Statement,CallableStatement,ResultSet等资源放在try-with-resources语句块中,以确保它们在使用后被正确关闭,防止资源泄漏。 - 使用连接池: 在生产环境中,绝对不要每次调用都创建和销毁连接,使用如 HikariCP, DBCP, C3P0 等连接池来管理数据库连接,能极大地提升性能和稳定性。
- 处理异常: 捕获
SQLException并妥善处理,记录日志或向用户展示友好的错误信息。 - 防止 SQL 注入: 当使用
PreparedStatement(包括CallableStatement)时,参数值会被正确转义,从而有效防止 SQL 注入攻击。永远不要用字符串拼接的方式来构建 SQL 调用语句。 - 明确参数类型: 在注册
OUT参数时,务必提供正确的java.sql.Types或OracleTypes,否则会导致类型转换错误。 - *PL/SQL Developer 或 SQLPlus 调试* 在 Java 中调用前,先在 PL/SQL Developer 或 SQLPlus 中测试你的存储过程,确保其逻辑正确,可以使用
DBMS_OUTPUT.PUT_LINE来打印调试信息。
