目录
- 准备工作
- 存储过程示例 (MySQL)
- Java 调用步骤
- 完整代码示例
- 示例1:调用无参存储过程
- 示例2:调用带 IN 参数的存储过程
- 示例3:调用带 OUT 参数的存储过程
- 示例4:调用带 INOUT 参数的存储过程
- 最佳实践与注意事项
准备工作
在开始之前,请确保你已经:

- Java 环境: JDK 已安装并配置好。
- MySQL 数据库: MySQL 服务正在运行。
- MySQL JDBC 驱动: 下载了对应你 MySQL 版本的 JDBC 驱动 (JAR 文件),并将其添加到你的 Java 项目的 classpath 中,如果你使用 Maven,可以直接在
pom.xml中添加依赖。<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> <!-- 请使用最新稳定版 --> </dependency>
存储过程示例 (MySQL)
为了演示,我们在 MySQL 数据库中创建几个不同类型的存储过程。
假设我们有一个数据库 test_db 和一张表 users:
CREATE DATABASE IF NOT EXISTS test_db;
USE test_db;
CREATE TABLE users (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50),
email VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入一些测试数据
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
我们创建几个存储过程:
-- 示例1: 无参,查询所有用户并返回结果集
DELIMITER //
CREATE PROCEDURE get_all_users()
BEGIN
SELECT id, name, email FROM users;
END //
DELIMITER ;
-- 示例2: 带 IN 参数,根据ID查询用户
DELIMITER //
CREATE PROCEDURE get_user_by_id(IN p_id INT)
BEGIN
SELECT id, name, email FROM users WHERE id = p_id;
END //
DELIMITER ;
-- 示例3: 带 OUT 参数,根据ID获取用户名
DELIMITER //
CREATE PROCEDURE get_user_name_by_id(IN p_id INT, OUT p_name VARCHAR(50))
BEGIN
SELECT name INTO p_name FROM users WHERE id = p_id;
END //
DELIMITER ;
-- 示例4: 带 INOUT 参数,增加用户年龄(这里我们用name_length代替)
DELIMITER //
CREATE PROCEDURE process_name_length(INOUT p_name_length INT)
BEGIN
-- 这里我们只是简单地将输入的长度乘以2作为返回
SET p_name_length = p_name_length * 2;
END //
DELIMITER ;
Java 调用步骤
在 Java 中调用存储过程的核心是 java.sql.CallableStatement,基本步骤如下:

- 加载并注册 JDBC 驱动 (对于较新版本的驱动,这一步通常是可选的)。
- 建立数据库连接 (
Connection)。 - 创建
CallableStatement对象:使用Connection.prepareCall()方法,并传入调用存储过程的 SQL 字符串。- 调用语法:
{call procedure_name(?, ?, ...)} - 代表参数占位符。
- 对于 OUT 和 INOUT 参数,需要在 前面加上
OUT或INOUT关键字,{call procedure_name(?, OUT ?)}
- 调用语法:
- 设置参数:根据参数类型,使用
CallableStatement的相应方法设置参数值。- IN 参数:
setXxx(index, value) - OUT 参数:
registerOutParameter(index, sqlType) - INOUT 参数: 先
setXxx()再registerOutParameter()
- IN 参数:
- 执行存储过程:使用
execute()方法。 - 处理结果:
- 如果存储过程返回结果集 (ResultSet),使用
statement.getResultSet()获取。 - 如果存储过程返回 OUT 参数,使用
getXxx(index)获取。
- 如果存储过程返回结果集 (ResultSet),使用
- 关闭资源:按照
ResultSet->CallableStatement->Connection的顺序关闭。
完整代码示例
这里我们使用 try-with-resources 语句来自动管理 Connection, CallableStatement, 和 ResultSet,这是一种推荐的最佳实践。
示例1:调用无参存储过程 (get_all_users)
这个存储过程返回一个结果集。
import java.sql.*;
public class CallStoredProcedureExample {
// 数据库连接信息 (请根据你的实际情况修改)
private static final String DB_URL = "jdbc:mysql://localhost:3306/test_db?useSSL=false&serverTimezone=UTC";
private static final String USER = "root";
private static final String PASS = "your_password";
public static void main(String[] args) {
// try-with-resources 确保 Connection, CallableStatement, ResultSet 自动关闭
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall("{call get_all_users()}")) {
System.out.println("正在调用 get_all_users 存储过程...");
// 执行存储过程
cstmt.execute();
// 获取结果集
ResultSet rs = cstmt.getResultSet();
// 处理结果集
System.out.println("ID\tName\tEmail");
System.out.println("---------------------------");
while (rs.next()) {
// 通过列名获取数据,更健壮
int id = rs.getInt("id");
String name = rs.getString("name");
String email = rs.getString("email");
System.out.println(id + "\t" + name + "\t" + email);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
示例2:调用带 IN 参数的存储过程 (get_user_by_id)
这个存储过程接收一个输入,并返回一个结果集。
// ... (DB_URL, USER, PASS 同上)
public static void callProcedureWithInParam(int userId) {
String sql = "{call get_user_by_id(?)}"; // ? 是 IN 参数占位符
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置 IN 参数 (参数索引从1开始)
cstmt.setInt(1, userId);
System.out.println("正在调用 get_user_by_id 存储过程,ID: " + userId);
// 执行并获取结果
ResultSet rs = cstmt.executeQuery(); // executeQuery() 适用于只返回结果集的情况
System.out.println("ID\tName\tEmail");
System.out.println("---------------------------");
if (rs.next()) {
System.out.println(rs.getInt("id") + "\t" + rs.getString("name") + "\t" + rs.getString("email"));
} else {
System.out.println("未找到ID为 " + userId + " 的用户。");
}
} catch (SQLException e) {
e.printStackTrace();
}
}
示例3:调用带 OUT 参数的存储过程 (get_user_name_by_id)
这个存储过程接收一个 IN 参数,并通过一个 OUT 参数返回值。
// ... (DB_URL, USER, PASS 同上)
public static void callProcedureWithOutParam(int userId) {
String sql = "{call get_user_name_by_id(?, ?)}"; // 第一个 ? 是 IN,第二个 ? 是 OUT
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置 IN 参数
cstmt.setInt(1, userId);
// 注册 OUT 参数 (参数索引从1开始,类型为 VARCHAR)
cstmt.registerOutParameter(2, Types.VARCHAR);
System.out.println("正在调用 get_user_name_by_id 存储过程,ID: " + userId);
// 执行存储过程
cstmt.execute();
// 获取 OUT 参数的值
String userName = cstmt.getString(2); // 通过索引获取
System.out.println("用户ID " + userId + " 的名字是: " + (userName != null ? userName : "未找到"));
} catch (SQLException e) {
e.printStackTrace();
}
}
示例4:调用带 INOUT 参数的存储过程 (process_name_length)
这个存储过程接收一个值,处理它,然后返回一个新值。
// ... (DB_URL, USER, PASS 同上)
public static void callProcedureWithInOutParam() {
String initialName = "Charlie";
int initialLength = initialName.length();
String sql = "{call process_name_length(?)}"; // 这个 ? 是 INOUT 参数
try (Connection conn = DriverManager.getConnection(DB_URL, USER, PASS);
CallableStatement cstmt = conn.prepareCall(sql)) {
// 设置 INOUT 参数的初始值
cstmt.setInt(1, initialLength);
// 注册 INOUT 参数 (类型为 INTEGER)
cstmt.registerOutParameter(1, Types.INTEGER);
System.out.println("正在调用 process_name_length 存储过程,初始长度: " + initialLength);
// 执行存储过程
cstmt.execute();
// 获取 INOUT 参数处理后的值
int processedLength = cstmt.getInt(1);
System.out.println("处理后的长度: " + processedLength);
} catch (SQLException e) {
e.printStackTrace();
}
}
最佳实践与注意事项
-
使用
try-with-resources:始终确保数据库资源(Connection,Statement,ResultSet)被正确关闭,以避免内存泄漏和连接耗尽。try-with-resources是最简单、最安全的方式。 -
防止 SQL 注入:
CallableStatement和PreparedStatement一样,使用参数化查询( 占位符)来传递参数,这可以有效地防止 SQL 注入攻击。永远不要使用字符串拼接来构建 SQL 调用语句。 -
区分
execute()和executeQuery():execute():可以执行任何 SQL 语句,如果第一个结果是ResultSet对象,则返回true;如果第一个结果是更新计数或没有结果,则返回false,适用于存储过程可能返回多种结果的情况。executeQuery():只能执行返回单个ResultSet对象的 SQL 语句(如SELECT或某些存储过程),如果用它执行不返回结果集的 SQL,会抛出异常,对于只返回结果集的存储过程,使用executeQuery()更简洁。
-
处理多个结果集:有些存储过程可能返回多个结果集,可以通过
boolean hasResultSet = cstmt.execute();循环调用cstmt.getResultSet()和cstmt.getUpdateCount()来处理所有结果。 -
明确指定 OUT 参数类型:在注册 OUT 参数时(
registerOutParameter),必须明确指定 SQL 类型(如Types.INTEGER,Types.VARCHAR),如果类型不匹配,可能会导致运行时错误或数据转换问题。 -
异常处理:妥善处理
SQLException,记录详细的错误信息,以便于调试。 -
连接管理:在实际项目中,不要在每次请求时都创建和销毁数据库连接,应该使用数据库连接池(如 HikariCP, Druid)来管理连接,以提高性能和资源利用率。
