说说 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/

面试考察点

  1. 基础掌握度:面试官不仅仅是想知道你背不背得出 JDBC 的 6 个步骤,更是想考察你是否理解每个步骤背后的意义——为什么用 PreparedStatement 而不是 Statement,为什么一定要在 finally 中关闭资源。

  2. 安全意识:看你是否知道 SQL 注入的原理,以及 PreparedStatement 的预编译机制如何防止注入。

  3. 联系框架: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 连接,开销大
③ 创建 Statementconn.prepareStatement()PreparedStatement,不用 Statement
④ 执行 SQL.executeQuery() / .executeUpdate()查询用前者,增删改用后者
⑤ 处理结果ResultSet.next()游标模式,每次 next() 移动一行
⑥ 释放资源.close()必须finally 中逆序关闭,防止泄漏

三、Statement vs PreparedStatement

这是面试必问的对比点:

对比维度StatementPreparedStatement
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
③ 创建 StatementStatementHandler
④ 设置参数ParameterHandler + TypeHandler
⑤ 处理结果ResultSetHandler + TypeHandler
⑥ 释放资源SqlSession.close() 自动管理

MyBatis 本质上就是把 JDBC 这 6 个步骤封装起来,用 XML 或注解配置替代硬编码的 SQL,用反射和动态代理替代手动的参数设置和结果映射。

面试高频追问

  1. 追问一:为什么 PreparedStatement 能防止 SQL 注入?
    • 因为 PreparedStatement 使用 预编译 + 参数绑定机制。SQL 先发送给数据库编译(? 只是占位符),编译后的 SQL 结构已经固定。参数值通过 .setString() 等方法单独发送,数据库将参数值作为纯数据处理,不会解析为 SQL 关键字或运算符,因此无法改变 SQL 的语法结构。
  2. 追问二:JDBC 中的 execute()executeQuery()executeUpdate() 有什么区别?
    • executeQuery() 专门执行 SELECT,返回 ResultSetexecuteUpdate() 执行 INSERT/UPDATE/DELETE/DDL,返回受影响的行数;execute() 是通用方法,可以执行任意 SQL,返回 booleantrue 表示有 ResultSetfalse 表示没有或返回更新计数),需要再通过 getResultSet()getUpdateCount() 获取结果。
  3. 追问三:什么是数据库连接池?为什么需要?
    • JDBC 每次通过 DriverManager.getConnection() 创建连接时,底层要建立 TCP 连接、认证、分配会话资源,开销很大。连接池预先创建一批连接放在池中,用的时候借出,用完归还,避免了频繁创建和销毁连接的开销。常用的连接池有 HikariCP、Druid 等。

常见面试变体

  • 变体一:"StatementPreparedStatement 的区别?"
  • 变体二:"JDBC 如何防止 SQL 注入?"
  • 变体三:"MyBatis 是如何封装 JDBC 的?"

记忆口诀

加载驱动拿连接,预编译 SQL 防 " 注入 ",设参执行理结果,finally 里关资源。

总结

JDBC 执行步骤为 6 步:加载驱动 → 获取连接 → 创建 PreparedStatement → 设置参数并执行 → 处理结果集 → 释放资源。核心要点是必须使用 PreparedStatement 防止 SQL 注入,资源必须在 finally 中逆序关闭。MyBatis 本质上就是对这 6 个步骤的封装。