说说 JDBC 的执行步骤?
一则或许对你有用的小广告
欢迎 加入小哈的星球 ,你将获得: 专属的项目实战(已更新的所有项目都能学习) / 1v1 提问 / Java 学习路线 / 学习打卡 / 每月赠书 / 社群讨论
- 新开坑项目: 《Spring AI 项目实战(问答机器人、RAG 增强检索、联网搜索)》 正在持续爆肝中,基于
Spring AI + Spring Boot3.x + JDK 21..., 点击查看; - 《从零手撸:仿小红书(微服务架构)》 已完结,基于
Spring Cloud Alibaba + Spring Boot3.x + JDK 17..., 点击查看项目介绍; 演示链接: http://116.62.199.48:7070/; - 《从零手撸:前后端分离博客项目(全栈开发)》 2 期已完结,演示链接: http://116.62.199.48/
面试考察点
-
基础掌握度:面试官不仅仅是想知道你背不背得出 JDBC 的 6 个步骤,更是想考察你是否理解每个步骤背后的意义——为什么用
PreparedStatement而不是Statement,为什么一定要在finally中关闭资源。 -
安全意识:看你是否知道 SQL 注入的原理,以及
PreparedStatement的预编译机制如何防止注入。 -
联系框架:MyBatis 本质上就是对 JDBC 的封装,理解 JDBC 是理解 MyBatis 工作原理的基础。
核心答案
JDBC 操作数据库的标准步骤有 6 步:
深度解析
一、完整代码示例
// JDBC 查询完整示例
public class JdbcDemo {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
// ① 加载驱动(JDBC 4.0+ 可省略,SPI 自动加载)
Class.forName("com.mysql.cj.jdbc.Driver");
// ② 获取连接
String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false";
conn = DriverManager.getConnection(url, "root", "123456");
// ③ 创建 PreparedStatement(预编译 SQL)
String sql = "SELECT id, username, email FROM t_user WHERE id = ?";
ps = conn.prepareStatement(sql);
// ④ 设置参数并执行
ps.setInt(1, 1); // 第一个 ? 占位符赋值为 1
rs = ps.executeQuery(); // 执行查询
// ⑤ 处理结果集
while (rs.next()) {
Long id = rs.getLong("id");
String username = rs.getString("username");
String email = rs.getString("email");
System.out.println(id + " | " + username + " | " + email);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
// ⑥ 释放资源(逆序关闭)
if (rs != null) try { rs.close(); } catch (SQLException e) {}
if (ps != null) try { ps.close(); } catch (SQLException e) {}
if (conn != null) try { conn.close(); } catch (SQLException e) {}
}
}
}
二、各步骤关键细节
| 步骤 | 关键类/方法 | 要点 |
|---|---|---|
| ① 加载驱动 | Class.forName() | JDBC 4.0+ 通过 SPI 自动加载,可省略 |
| ② 获取连接 | DriverManager.getConnection() | 底层通过驱动创建 TCP 连接,开销大 |
| ③ 创建 Statement | conn.prepareStatement() | 用 PreparedStatement,不用 Statement |
| ④ 执行 SQL | .executeQuery() / .executeUpdate() | 查询用前者,增删改用后者 |
| ⑤ 处理结果 | ResultSet.next() | 游标模式,每次 next() 移动一行 |
| ⑥ 释放资源 | .close() | 必须在 finally 中逆序关闭,防止泄漏 |
三、Statement vs PreparedStatement
这是面试必问的对比点:
| 对比维度 | Statement | PreparedStatement |
|---|---|---|
| SQL 注入 | ❌ 有风险(字符串拼接) | ✅ 安全(参数化查询) |
| 预编译 | 不支持 | 支持,数据库缓存执行计划 |
| 性能 | 重复 SQL 每次都要编译 | 相同 SQL 可复用执行计划 |
| 参数设置 | 手动拼接字符串 | .setXxx() 方法绑定参数 |
| 推荐度 | ❌ 不推荐 | ✅ 必须使用 |
SQL 注入示例:
// ❌ Statement —— 有 SQL 注入风险
String name = "admin' OR '1'='1";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(
"SELECT * FROM t_user WHERE username = '" + name + "'"
);
// 实际执行:SELECT * FROM t_user WHERE username = 'admin' OR '1'='1'
// 结果:查出了全部数据!
// ✅ PreparedStatement —— 安全
String name = "admin' OR '1'='1";
PreparedStatement ps = conn.prepareStatement(
"SELECT * FROM t_user WHERE username = ?"
);
ps.setString(1, name);
ResultSet rs = ps.executeQuery();
// 参数值作为纯数据处理,不会改变 SQL 结构
// 结果:查不到数据(没有用户名叫 "admin' OR '1'='1" 的用户)
四、资源释放的正确姿势
JDK 7+ 推荐使用 try-with-resources 自动关闭资源:
// try-with-resources:自动关闭(推荐)
try (
Connection conn = DriverManager.getConnection(url, "root", "123456");
PreparedStatement ps = conn.prepareStatement(sql);
ResultSet rs = ps.executeQuery()
) {
// 只要实现了 AutoCloseable 接口,try 结束自动调用 close()
while (rs.next()) {
// 处理结果
}
} catch (SQLException e) {
e.printStackTrace();
}
// 无需手动 close(),Java 自动按 rs → ps → conn 的顺序关闭
关闭顺序必须是 ResultSet → PreparedStatement → Connection(先开后关),因为后者依赖前者。
五、MyBatis 对 JDBC 的封装映射
理解了 JDBC 的 6 个步骤,再看 MyBatis 就非常清晰了:
| JDBC 步骤 | MyBatis 对应组件 |
|---|---|
| ① 加载驱动 | SqlSessionFactoryBuilder 读取配置 |
| ② 获取连接 | SqlSession(内部持有 Connection) |
| ③ 创建 Statement | StatementHandler |
| ④ 设置参数 | ParameterHandler + TypeHandler |
| ⑤ 处理结果 | ResultSetHandler + TypeHandler |
| ⑥ 释放资源 | SqlSession.close() 自动管理 |
MyBatis 本质上就是把 JDBC 这 6 个步骤封装起来,用 XML 或注解配置替代硬编码的 SQL,用反射和动态代理替代手动的参数设置和结果映射。
面试高频追问
- 追问一:为什么
PreparedStatement能防止 SQL 注入?- 因为
PreparedStatement使用 预编译 + 参数绑定机制。SQL 先发送给数据库编译(?只是占位符),编译后的 SQL 结构已经固定。参数值通过.setString()等方法单独发送,数据库将参数值作为纯数据处理,不会解析为 SQL 关键字或运算符,因此无法改变 SQL 的语法结构。
- 因为
- 追问二:JDBC 中的
execute()、executeQuery()、executeUpdate()有什么区别?executeQuery()专门执行SELECT,返回ResultSet;executeUpdate()执行INSERT/UPDATE/DELETE/DDL,返回受影响的行数;execute()是通用方法,可以执行任意 SQL,返回boolean(true表示有ResultSet,false表示没有或返回更新计数),需要再通过getResultSet()或getUpdateCount()获取结果。
- 追问三:什么是数据库连接池?为什么需要?
- JDBC 每次通过
DriverManager.getConnection()创建连接时,底层要建立 TCP 连接、认证、分配会话资源,开销很大。连接池预先创建一批连接放在池中,用的时候借出,用完归还,避免了频繁创建和销毁连接的开销。常用的连接池有 HikariCP、Druid 等。
- JDBC 每次通过
常见面试变体
- 变体一:"
Statement和PreparedStatement的区别?" - 变体二:"JDBC 如何防止 SQL 注入?"
- 变体三:"MyBatis 是如何封装 JDBC 的?"
记忆口诀
加载驱动拿连接,预编译 SQL 防 " 注入 ",设参执行理结果,finally 里关资源。
总结
JDBC 执行步骤为 6 步:加载驱动 → 获取连接 → 创建 PreparedStatement → 设置参数并执行 → 处理结果集 → 释放资源。核心要点是必须使用 PreparedStatement 防止 SQL 注入,资源必须在 finally 中逆序关闭。MyBatis 本质上就是对这 6 个步骤的封装。