JDBC 详解:从原理到实战(含完整可运行 Demo)

发现自己的语雀里面有之前学习的JDBC的笔记,我喊AI整理了出来,哪里有错误,请大佬指点

一句话总结:JDBC 是 Java 官方定义的一套"接口规范",数据库厂商按规范提供"驱动 jar 包",我们面向接口编程,就能用同一套代码操作任何数据库。
本文目标:把 JDBC 的原理和实现 讲透、把六步模板 背牢、附带一套可直接跑的 Demo。忘了 JDBC 的时候,翻这一篇就够。


目录

[一、JDBC 是什么?为什么需要它](#一、JDBC 是什么?为什么需要它)

[1.1 定义](#1.1 定义)

[1.2 为什么需要 JDBC?](#1.2 为什么需要 JDBC?)

[1.3 版本小知识(帮记忆,容易考)](#1.3 版本小知识(帮记忆,容易考))

[二、JDBC 全景架构与一次调用的完整流程](#二、JDBC 全景架构与一次调用的完整流程)

[2.1 架构图(五层)](#2.1 架构图(五层))

[2.2 一次 JDBC 调用的完整流程(主干原理)](#2.2 一次 JDBC 调用的完整流程(主干原理))

三、核心原理逐个击破(源码级)

[3.1 注册驱动:Class.forName() 到底干了什么?](#3.1 注册驱动:Class.forName() 到底干了什么?)

[3.2 为什么 JDBC 4.0 之后可以不写 Class.forName?------ SPI 机制](#3.2 为什么 JDBC 4.0 之后可以不写 Class.forName?—— SPI 机制)

[3.3 getConnection 的原理](#3.3 getConnection 的原理)

[3.4 URL 的秘密](#3.4 URL 的秘密)

[3.5 Statement 三兄弟:executeQuery / executeUpdate / execute](#3.5 Statement 三兄弟:executeQuery / executeUpdate / execute)

[3.6 ResultSet 游标原理](#3.6 ResultSet 游标原理)

[3.7 PreparedStatement:预编译原理与 SQL 注入(重点!)](#3.7 PreparedStatement:预编译原理与 SQL 注入(重点!))

[(1)SQL 注入是怎么发生的?](#(1)SQL 注入是怎么发生的?)

[(2)PreparedStatement 为什么能防注入?](#(2)PreparedStatement 为什么能防注入?)

(3)三大好处

[3.8 JDBC 事务原理](#3.8 JDBC 事务原理)

[3.9 资源释放的原理](#3.9 资源释放的原理)

[四、JDBC 编程六步(标准模板 + 口诀)](#四、JDBC 编程六步(标准模板 + 口诀))

[五、实战 Demo](#五、实战 Demo)

[5.1 准备:导入驱动](#5.1 准备:导入驱动)

[5.2 建表 SQL(init.sql)](#5.2 建表 SQL(init.sql))

[5.3 Demo1:六步入门(JdbcQuickStart.java)](#5.3 Demo1:六步入门(JdbcQuickStart.java))

[5.4 Demo2:PreparedStatement 增删改查 + 防注入登录(JdbcCrudDemo.java)](#5.4 Demo2:PreparedStatement 增删改查 + 防注入登录(JdbcCrudDemo.java))

[5.5 Demo3:事务转账(JdbcTransactionDemo.java)](#5.5 Demo3:事务转账(JdbcTransactionDemo.java))

[5.6 运行方式](#5.6 运行方式)

[六、JDBC 的缺陷与技术演进](#六、JDBC 的缺陷与技术演进)

[七、面试高频 8 问](#七、面试高频 8 问)

八、一页速查(回忆专用,贴脑门版)

[8.1 五大核心对象](#8.1 五大核心对象)

[8.2 六步口诀](#8.2 六步口诀)

[8.3 最小可抄模板](#8.3 最小可抄模板)

[8.4 易错清单](#8.4 易错清单)



一、JDBC 是什么?为什么需要它

1.1 定义

JDBC(Java Database Connectivity) 是 Java 官方提供的一套操作关系型数据库的标准规范(接口集合) ,位于 JDK 的 java.sql、javax.sql 两个包中。

必须刻在脑子里的关键结论:

JDBC 只是规范(一堆接口),本身没有实现。 真正的实现叫"驱动(Driver)",由各数据库厂商按规范编写,以 jar 包形式提供。

概念 提供方 内容
JDBC API Java 官方(JDK 内置) java.sql.* 中的接口:Driver、DriverManager、Connection、Statement、PreparedStatement、ResultSet 等
JDBC 驱动 数据库厂商(jar 包) 上述接口的实现类,如 MySQL 的 mysql-connector-j-8.x.x.jar

1.2 为什么需要 JDBC?

想象没有 JDBC 的世界:

  1. MySQL、Oracle、SQL Server 各家的通信协议、调用方式完全不同;
  2. Java 代码直接耦合某一家数据库的私有 API;
  3. 一旦换数据库,所有数据访问代码全部重写。

有了 JDBC:

  1. 我们面向统一的接口 编程(Connection、Statement......);
  2. 数据库厂商负责实现接口(写好驱动 jar 包);
  3. 换数据库 = 换个 jar 包 + 改个 URL,Java 代码一行不用动。

这是"面向接口编程 + 解耦"思想最经典的落地。 类比:JDBC 就是 Java 与数据库之间的"USB 接口规范"------规范统一后,任何厂商的数据库都能"即插即用"。

1.3 版本小知识(帮记忆,容易考)

项目 MySQL 5.x MySQL 8.x
驱动类名 com.mysql.jdbc.Driver com.mysql.cj.jdbc.Driver(cj = Connector/Java)
Maven 坐标 mysql:mysql-connector-java com.mysql:mysql-connector-j
注意 ------ 早期版本 URL 需带 serverTimezone

二、JDBC 全景架构与一次调用的完整流程

2.1 架构图(五层)

复制代码
┌──────────────────────────────────────────────┐
│               Java 应用程序                    │
│           (我们写的 JDBC 六步代码)            │
└──────────────────────┬───────────────────────┘
                       │ ①面向接口调用
┌──────────────────────▼───────────────────────┐
│     JDBC API(java.sql 包中的接口)           │
│   DriverManager / Connection / Statement     │
│   PreparedStatement / ResultSet              │
└──────────────────────┬───────────────────────┘
                       │ ②找到接口的实现类
┌──────────────────────▼───────────────────────┐
│          DriverManager(驱动管理器)          │
│   内部维护一张"驱动注册表",按 URL 匹配驱动     │
└──────────────────────┬───────────────────────┘
                       │ ③委托驱动 connect(url, info)
┌──────────────────────▼───────────────────────┐
│     数据库驱动(厂商 jar 包里的实现类)         │
│    com.mysql.cj.jdbc.ConnectionImpl 等        │
└──────────────────────┬───────────────────────┘
                       │ ④TCP + 数据库私有协议
┌──────────────────────▼───────────────────────┐
│       数据库服务器(MySQL / Oracle ...)      │
└──────────────────────────────────────────────┘

分层记忆:我们的代码 → JDBC 接口 → DriverManager → 驱动实现 → 数据库服务器。

2.2 一次 JDBC 调用的完整流程(主干原理)

复制代码
1. 加载驱动类 → 触发其静态代码块 → 调用 DriverManager.registerDriver() 完成注册
2. DriverManager.getConnection(url, user, password)
   → 遍历"驱动注册表",依次调用每个驱动的 connect(url, info)
   → 驱动检查 URL 协议前缀(jdbc:mysql:):不是自己的返回 null,是自己的建立 TCP 连接
   → 成功后返回该厂商的 Connection 实现对象
3. conn.createStatement() / conn.prepareStatement(sql) 创建 SQL 执行器
4. 执行器把 SQL 发送到数据库服务器 → 数据库解析(词法/语法/语义)→ 执行 → 回传结果
5. 查询语句:结果封装成 ResultSet(游标)返回
   增删改语句:返回受影响行数 int
6. 释放资源(ResultSet → Statement → Connection,逆序)

把这段流程背下来,JDBC 原理就懂了 70%。


三、核心原理逐个击破(源码级)

3.1 注册驱动:Class.forName() 到底干了什么?

入门代码都有这一行:

复制代码
Class.forName("com.mysql.cj.jdbc.Driver");

它的作用是:把驱动类的字节码加载进 JVM 并触发类初始化。看 MySQL 驱动类的源码:

复制代码
// mysql-connector-j 源码(略有精简)
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // 关键:注册动作写在静态代码块里!
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

所以完整链条是:

复制代码
Class.forName("com.mysql.cj.jdbc.Driver")
→ 触发类加载,执行 static 静态代码块
→ new 一个驱动对象,注册进 DriverManager 的注册表
→ 后续 getConnection 才能找到它

"注册驱动"的本质:把驱动对象放入 DriverManager 内部的驱动集合(JDK 中是 CopyOnWriteArrayList<DriverInfo> registeredDrivers)。 Class.forName 只是触发手段,不是注册本身。

3.2 为什么 JDBC 4.0 之后可以不写 Class.forName?------ SPI 机制

JDK 6(JDBC 4.0)之后,把这行删了程序照样能跑,为什么?

是两件事配合帮我们自动完成了:

  1. 驱动 jar 包里自带文件:META-INF/services/java.sql.Driver,内容就是驱动类的全限定名;

  2. DriverManager 有静态代码块,类加载时执行 loadInitialDrivers(),内部用 SPI(ServiceLoader) 扫描 classpath 上所有 jar 的 META-INF/services/java.sql.Driver,把其中声明的驱动类自动加载注册。

    // JDK DriverManager 部分源码
    static {
    loadInitialDrivers(); // SPI 自动发现并注册驱动
    println("JDBC DriverManager initialized");
    }

结论:只要把 JDBC 4.0+ 的驱动 jar 放进 classpath,注册这步可以省略。 但实际开发建议写上:更直观,而且能在启动时立刻暴露"jar 没导入"的问题。

3.3 getConnection 的原理

复制代码
Connection conn = DriverManager.getConnection(url, user, password);

DriverManager 内部逻辑(简化):

复制代码
for (DriverInfo aDriver : registeredDrivers) {     // 遍历驱动注册表
    Connection con = aDriver.driver.connect(url, info);
    if (con != null) {
        return con;                                // 谁能处理这个 url 就用谁
    }
}
throw new SQLException("No suitable driver found for " + url);

MySQL 驱动的 connect() 会先检查 URL 前缀:

  • 不是 jdbc:mysql:// 开头 → 返回 null("不是我的活",让下一个驱动试);
  • 是 自己的协议 → 建立 TCP 连接、完成认证握手 → 返回 ConnectionImpl 实现对象。

由此也能秒懂经典报错:

复制代码
No suitable driver found for jdbc:mysql://...

→ 要么驱动 jar 没导入,要么 URL 写错了,总之没有驱动"认领"这个 URL。

3.4 URL 的秘密

复制代码
jdbc:mysql://localhost:3306/jdbc_demo?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8
 │      │        │        │        │            └─────────────── 连接参数(key=value,& 分隔)
 │      │        │        │        └─ 数据库名
 │      │        │        └─ 端口号(MySQL 默认 3306)
 │      │        └─ 主机地址
 │      └─ 子协议:标识用哪个驱动(驱动靠它"认领")
 └─ 协议(固定)

常用参数速记:

参数 作用
serverTimezone=Asia/Shanghai MySQL 8 早期版本必写,否则报时区错误;新版加上也无害
useSSL=false 关闭 SSL 及其警告
characterEncoding=utf8 防中文乱码
allowPublicKeyRetrieval=true MySQL 8 关闭 SSL 时可能需要(新认证插件)

3.5 Statement 三兄弟:executeQuery / executeUpdate / execute

方法 适合的 SQL 返回值
executeQuery() SELECT ResultSet(结果集)
executeUpdate() INSERT / UPDATE / DELETE(DDL 也行) int 受影响行数(DDL 返回 0)
execute() 任意 SQL boolean(true = 有结果集,再调 getResultSet() 取)

SQL 执行的真实流程:SQL 文本 → 驱动组装成数据库协议报文 → 发送到服务器 → 服务器解析(词法/语法/语义)→ 生成执行计划 → 执行 → 回传结果。

3.6 ResultSet 游标原理

复制代码
ResultSet rs = stmt.executeQuery("SELECT id, name, age FROM t_user");
while (rs.next()) {                        // ① 先 next(),② 再取值
    int id = rs.getInt("id");              // 按列名取值
    String name = rs.getString("name");
    int age = rs.getInt("age");            // 也可按列索引取,注意从 1 开始!
}

原理要点:

  • ResultSet 内部有一个游标(cursor) ,初始位置在第一行之前;
  • next():游标下移一行,返回是否还有数据;
  • 取值:getXxx(列名) 或 getXxx(列索引),列索引从 1 开始(不是 0);
  • 默认结果集为 TYPE_FORWARD_ONLY:只能从上往下走一遍,不能回头。

新手必踩的 3 个坑: ① 不调用 next() 直接 getXxx() → Before start of result set 异常; ② 列索引用了 0 → Column Index out of range; ③ 关闭 Connection 后还想用 rs → Operation not allowed after ResultSet closed。

3.7 PreparedStatement:预编译原理与 SQL 注入(重点!)

(1)SQL 注入是怎么发生的?

用 Statement 拼 SQL:

复制代码
String name = "任意名字";
String pwd = "' OR '1'='1";    // 恶意输入!
String sql = "SELECT * FROM t_user WHERE name='" + name + "' AND password='" + pwd + "'";
// 拼出来变成:
// SELECT * FROM t_user WHERE name='任意名字' AND password='' OR '1'='1'

由于 AND 优先级高于 OR,条件等价于:

复制代码
(name = '任意名字' AND password = '') OR '1'='1'    -- '1'='1' 恒为真!

于是任何数据都能被查出来 ------ 这就是 SQL 注入:用户输入参与了 SQL 语法结构的拼装。

(2)PreparedStatement 为什么能防注入?
复制代码
String sql = "SELECT * FROM t_user WHERE name=? AND password=?";
PreparedStatement ps = conn.prepareStatement(sql);   // 先把"骨架"交给数据库预编译
ps.setString(1, "任意名字");
ps.setString(2, "' OR '1'='1");   // 只会被当作纯数据,绝不会变成 SQL
ResultSet rs = ps.executeQuery();

原理分两步:

  1. 预编译 :prepareStatement(sql) 阶段,含 ? 的 SQL 模板先发给数据库完成语法分析、生成执行计划 ------ 此时 SQL 的结构已经定死;
  2. 参数单独传 :后续 setXxx() 传的参数只作为纯数据 填入占位符:
    • 服务端预编译模式(URL 加 useServerPrepStmts=true):参数走二进制协议单独传输;
    • MySQL 驱动默认客户端模式:参数由驱动做严格转义后再发送;
    • 无论哪种模式,参数永远不可能参与 SQL 语法解析。
(3)三大好处
好处 说明
防注入 参数永远是数据,不是 SQL
性能好 同一模板只编译一次,重复执行只传参;配合 addBatch() 还能批量
代码清晰 不再手写一堆 '+xxx+' 和单引号

记忆点:Statement 拼作文,PreparedStatement 填空题。

3.8 JDBC 事务原理

JDBC 默认 autoCommit = true:每条 SQL 都是一个独立事务,执行完立即提交。

想让多条 SQL "同生共死":

复制代码
conn.setAutoCommit(false);   // ① 关自动提交 = 开启事务
try {
    // ...多条 DML...
    conn.commit();           // ② 全部成功 → 提交
} catch (Exception e) {
    conn.rollback();         // ③ 任何一步失败 → 回滚
}
// ④ 结束后恢复
conn.setAutoCommit(true);

补充两件小事:

  • 回滚一半时想保留部分操作 → Savepoint:Savepoint sp = conn.setSavepoint(); + conn.rollback(sp);
  • 隔离级别 → conn.setTransactionIsolation(...):JDBC 定义了 4 级(TRANSACTION_READ_UNCOMMITTED / READ_COMMITTED / REPEATABLE_READ / SERIALIZABLE),MySQL InnoDB 默认 REPEATABLE READ。

3.9 资源释放的原理

Connection、Statement、ResultSet 为什么必须关、按什么顺序关?

  • Connection 最贵 :底层是 TCP 长连接 + 认证会话,且数据库端连接数有限(MySQL 默认 max_connections = 151)。用完不还,很快就会 Too many connections;
  • 关闭顺序 = 创建顺序的逆序 :rs → stmt → conn;
  • 连带关系:关闭 Statement 会自动关其 ResultSet,关闭 Connection 会自动关其 Statement ------ 但规范不保证,习惯上还是显式全关;
  • 必须放 finally(或用 try-with-resources),否则一旦抛异常资源就漏了。

四、JDBC 编程六步(标准模板 + 口诀)

复制代码
// ① 注册驱动(JDBC 4.0+ 可省略,写上更好理解)
Class.forName("com.mysql.cj.jdbc.Driver");

// ② 获取连接
Connection conn = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/jdbc_demo?useSSL=false&serverTimezone=Asia/Shanghai",
        "root", "123456");

// ③ 编写 SQL 并创建执行对象(预编译)
String sql = "SELECT id, name, age FROM t_user WHERE age > ?";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setInt(1, 20);

// ④ 执行 SQL
ResultSet rs = ps.executeQuery();   // 查询;增删改用 executeUpdate()

// ⑤ 处理结果
while (rs.next()) {
    System.out.println(rs.getInt("id") + " | " + rs.getString("name"));
}

// ⑥ 释放资源(逆序)
rs.close();
ps.close();
conn.close();

口诀:一注册、二连接、三语句、四执行、五结果、六释放。

现代写法(try-with-resources,自动按逆序关闭,实际项目推荐):

复制代码
String url = "jdbc:mysql://localhost:3306/jdbc_demo?useSSL=false&serverTimezone=Asia/Shanghai";
String sql = "SELECT id, name FROM t_user";
try (Connection conn = DriverManager.getConnection(url, "root", "123456");
     PreparedStatement ps = conn.prepareStatement(sql);
     ResultSet rs = ps.executeQuery()) {
    while (rs.next()) {
        System.out.println(rs.getInt("id") + " | " + rs.getString("name"));
    }
}   // 大括号结束自动调用 close()

五、实战 Demo

5.1 准备:导入驱动

Maven 项目:

复制代码
<dependency>
    <groupId>com.mysql</groupId>
    <artifactId>mysql-connector-j</artifactId>
    <version>8.0.33</version>
</dependency>

普通项目:把 mysql-connector-j-8.x.x.jar 加入 classpath(IDEA:File → Project Structure → Libraries → 加号导入)。

5.2 建表 SQL(init.sql)

复制代码
-- JDBC Demo 初始化脚本
CREATE DATABASE IF NOT EXISTS jdbc_demo DEFAULT CHARACTER SET utf8mb4;
USE jdbc_demo;

-- 用户表(Demo1、Demo2 使用)
DROP TABLE IF EXISTS t_user;
CREATE TABLE t_user (
    id       INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键',
    name     VARCHAR(50) NOT NULL COMMENT '姓名',
    password VARCHAR(50) NOT NULL COMMENT '密码(仅演示,明文)',
    age      INT COMMENT '年龄'
);
INSERT INTO t_user (name, password, age) VALUES
('张三', '123456', 23),
('李四', '123456', 25),
('王五', '123456', 28);

-- 账户表(Demo3 事务转账使用)
DROP TABLE IF EXISTS t_account;
CREATE TABLE t_account (
    id    INT PRIMARY KEY AUTO_INCREMENT,
    name  VARCHAR(50) NOT NULL,
    money DOUBLE NOT NULL
);
INSERT INTO t_account (name, money) VALUES
('张三', 1000),
('李四', 1000);

5.3 Demo1:六步入门(JdbcQuickStart.java)

对应第四章的六步模板,每一步的注释就是原理:

复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.Statement;

public class JdbcQuickStart {
    public static void main(String[] args) throws Exception {
        // ① 注册驱动:触发驱动类静态代码块,把自己注册进 DriverManager
        //    JDBC 4.0+ 靠 SPI 可自动完成,写出来是为了看清原理
        Class.forName("com.mysql.cj.jdbc.Driver");

        // ② 获取连接:DriverManager 按协议前缀 jdbc:mysql: 找到 MySQL 驱动,
        //    由驱动建立 TCP 连接,返回 Connection 实现对象
        String url = "jdbc:mysql://localhost:3306/jdbc_demo"
                + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8";
        Connection conn = DriverManager.getConnection(url, "root", "123456");

        // ③ 创建 Statement:SQL 执行器
        Statement stmt = conn.createStatement();

        // ④ 执行查询 SQL,返回结果集
        ResultSet rs = stmt.executeQuery("SELECT id, name, age FROM t_user");

        // ⑤ 处理结果:游标初始在第一行之前,next() 下移一行
        while (rs.next()) {
            int id = rs.getInt("id");            // 按列名取值(索引则从 1 开始)
            String name = rs.getString("name");
            int age = rs.getInt("age");
            System.out.println(id + "\t" + name + "\t" + age);
        }

        // ⑥ 释放资源:与创建顺序相反;Connection 底层是 TCP,最贵,必须还
        rs.close();
        stmt.close();
        conn.close();
    }
}

5.4 Demo2:PreparedStatement 增删改查 + 防注入登录(JdbcCrudDemo.java)

复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * PreparedStatement 标准写法:
 * 1. 增删改:executeUpdate() 返回受影响行数
 * 2. 查询:executeQuery() 返回 ResultSet
 * 3. 预编译 + ? 占位符,天然防 SQL 注入
 */
public class JdbcCrudDemo {

    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo"
            + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8";
    private static final String USER = "root";        // TODO 改成你的用户名
    private static final String PASSWORD = "123456";  // TODO 改成你的密码

    public static void main(String[] args) {
        insert();
        update();
        delete();
        queryAll();
        login("张三", "123456");
        login("张三", "' OR '1'='1");   // 注入攻击串 → 登录失败,预编译防御生效
    }

    /** 新增 */
    private static void insert() {
        String sql = "INSERT INTO t_user(name, password, age) VALUES(?, ?, ?)";
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            ps = conn.prepareStatement(sql);
            ps.setString(1, "赵六");
            ps.setString(2, "123456");
            ps.setInt(3, 30);
            int rows = ps.executeUpdate();
            System.out.println("[insert] 影响行数:" + rows);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(null, ps, conn);
        }
    }

    /** 修改 */
    private static void update() {
        String sql = "UPDATE t_user SET age = ? WHERE name = ?";
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            ps = conn.prepareStatement(sql);
            ps.setInt(1, 26);
            ps.setString(2, "张三");
            int rows = ps.executeUpdate();
            System.out.println("[update] 影响行数:" + rows);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(null, ps, conn);
        }
    }

    /** 删除 */
    private static void delete() {
        String sql = "DELETE FROM t_user WHERE name = ?";
        Connection conn = null;
        PreparedStatement ps = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            ps = conn.prepareStatement(sql);
            ps.setString(1, "赵六");
            int rows = ps.executeUpdate();
            System.out.println("[delete] 影响行数:" + rows);
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(null, ps, conn);
        }
    }

    /** 查询 */
    private static void queryAll() {
        String sql = "SELECT id, name, age FROM t_user";
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            ps = conn.prepareStatement(sql);
            rs = ps.executeQuery();
            System.out.println("[query] 查询结果:");
            while (rs.next()) {
                System.out.println("  " + rs.getInt("id") + " | "
                        + rs.getString("name") + " | " + rs.getInt("age"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rs, ps, conn);
        }
    }

    /** 登录:预编译防注入演示 */
    private static void login(String name, String pwd) {
        String sql = "SELECT id, name, age FROM t_user WHERE name = ? AND password = ?";
        Connection conn = null;
        PreparedStatement ps = null;
        ResultSet rs = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            ps = conn.prepareStatement(sql);
            ps.setString(1, name);
            ps.setString(2, pwd);
            rs = ps.executeQuery();
            if (rs.next()) {
                System.out.println("[login] 登录成功:" + rs.getString("name"));
            } else {
                System.out.println("[login] 登录失败(密码错误或注入串无效)");
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            close(rs, ps, conn);
        }
    }

    /** 统一释放资源:rs → ps → conn,与创建顺序相反 */
    private static void close(ResultSet rs, PreparedStatement ps, Connection conn) {
        try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); }
        try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); }
        try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); }
    }
}

5.5 Demo3:事务转账(JdbcTransactionDemo.java)

复制代码
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

/**
 * JDBC 事务 ------ 转账:A 扣钱 + B 加钱,必须同时成功,否则整体回滚
 * 核心 API:setAutoCommit(false) 开启事务 → commit() 提交 → rollback() 回滚
 */
public class JdbcTransactionDemo {

    private static final String URL = "jdbc:mysql://localhost:3306/jdbc_demo"
            + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8";
    private static final String USER = "root";        // TODO 改成你的用户名
    private static final String PASSWORD = "123456";  // TODO 改成你的密码

    public static void main(String[] args) {
        System.out.println("转账前:");
        queryMoney();

        transfer(1, 2, 100.0);   // 1 号账户向 2 号账户转 100 元

        System.out.println("转账后:");
        queryMoney();
    }

    private static void transfer(int fromId, int toId, double money) {
        Connection conn = null;
        PreparedStatement outPs = null;
        PreparedStatement inPs = null;
        try {
            conn = DriverManager.getConnection(URL, USER, PASSWORD);
            conn.setAutoCommit(false);   // ① 开启事务:下面多条 SQL 同生共死

            String outSql = "UPDATE t_account SET money = money - ? WHERE id = ?";
            outPs = conn.prepareStatement(outSql);
            outPs.setDouble(1, money);
            outPs.setInt(2, fromId);
            outPs.executeUpdate();

            // 模拟中途出错:解开下一行注释,可观察"扣款已执行但未加钱"被整体回滚
            // int x = 1 / 0;

            String inSql = "UPDATE t_account SET money = money + ? WHERE id = ?";
            inPs = conn.prepareStatement(inSql);
            inPs.setDouble(1, money);
            inPs.setInt(2, toId);
            inPs.executeUpdate();

            conn.commit();               // ② 全部成功 → 提交
            System.out.println("[transfer] 转账成功,已提交事务");
        } catch (Exception e) {
            e.printStackTrace();
            if (conn != null) {
                try {
                    conn.rollback();     // ③ 任何一步失败 → 全部回滚
                    System.out.println("[transfer] 转账失败,已回滚");
                } catch (SQLException ex) {
                    ex.printStackTrace();
                }
            }
        } finally {
            close(inPs);
            close(outPs);
            close(conn);
        }
    }

    /** try-with-resources 现代写法:大括号结束自动关闭(逆序) */
    private static void queryMoney() {
        String sql = "SELECT id, name, money FROM t_account";
        try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD);
             PreparedStatement ps = conn.prepareStatement(sql);
             ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                System.out.println("  " + rs.getInt("id") + " | "
                        + rs.getString("name") + " | " + rs.getDouble("money"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }

    private static void close(AutoCloseable r) {
        if (r != null) {
            try { r.close(); } catch (Exception e) { e.printStackTrace(); }
        }
    }
}

5.6 运行方式

复制代码
# 1. 执行 init.sql 初始化数据库
# 2. 修改 Java 文件中的 USER / PASSWORD
# 3. 编译运行(Windows 注意分号,Linux/Mac 换成冒号)
javac -encoding UTF-8 JdbcQuickStart.java
java -cp ".;mysql-connector-j-8.0.33.jar" JdbcQuickStart

预期效果:

复制代码
1	张三	23
2	李四	25
3	王五	28

六、JDBC 的缺陷与技术演进

JDBC 是"能用",但直接在项目里裸写有三大痛点,也正是它们催生了后面的技术:

痛点 解决方案 现状
频繁创建/销毁 TCP 连接,太贵 连接池:C3P0 → Druid → HikariCP(Spring Boot 2+ 默认) 标配
六步模板代码重复、样板臃肿 DbUtils / Spring JdbcTemplate 基本退出舞台
结果集手动映射太麻烦 ORM :Hibernate / MyBatis 主流

记忆主线:JDBC(地基)→ 连接池(解决性能)→ MyBatis(解放生产力)。 框架再怎么封装,底层跑的还是 JDBC ------ 这也是必须先把 JDBC 学透的原因。


七、面试高频 8 问

Q1:说说 JDBC 的执行流程? 六步:注册驱动 → 获取连接 → 创建语句对象 → 执行 SQL → 处理结果 → 释放资源;再补一句驱动注册的 SPI 原理,直接加分。

Q2:Statement 和 PreparedStatement 的区别? ① 预编译 vs 每次发送完整 SQL;② 防注入 vs 可被注入;③ 高频重复执行时性能更好;④ ? 占位符代码更清晰。

Q3:Class.forName 的作用?为什么现在可以省略? 加载驱动类并触发静态代码块完成注册;JDBC 4.0+ 驱动 jar 的 META-INF/services/java.sql.Driver + DriverManager 静态块的 SPI 扫描会自动注册。

Q4:SQL 注入的原理与防范? 用户输入被拼进 SQL 文本、改变了语法结构(' OR '1'='1);防范:预编译 + 参数占位符,参数永远只作纯数据传输。

Q5:execute / executeQuery / executeUpdate 的区别? 分别对应任意 SQL / 查询返回 ResultSet / 增删改返回受影响行数 int。

Q6:JDBC 中如何实现事务? setAutoCommit(false) 开启 → commit() 提交 / rollback() 回滚;可用 Savepoint 部分回滚;隔离级别由 setTransactionIsolation() 控制。

Q7:为什么需要连接池? Connection 建立成本高(TCP 三次握手 + 认证)且数据库连接数有限;池化思想 = 预先建好一批连接,借出→归还,避免重复创建销毁。

Q8:JDBC 事务隔离级别有哪几种? 读未提交、读已提交、可重复读、串行化 4 种(JDBC 常量 TRANSACTION_xxx),MySQL InnoDB 默认可重复读。


八、一页速查(回忆专用,贴脑门版)

8.1 五大核心对象

接口/类 一句话职责
DriverManager 管理驱动注册表,负责"按 URL 认领驱动"并发起连接
Connection 一条数据库会话(底层 TCP),事务由它控制
Statement SQL 执行器(拼接式,有注入风险)
PreparedStatement 预编译执行器(? 占位符,防注入,推荐)
ResultSet 查询结果集,游标遍历,列索引从 1 开始

8.2 六步口诀

一注册、二连接、三语句、四执行、五结果、六释放(逆序关:rs → stmt → conn)

8.3 最小可抄模板

复制代码
Class.forName("com.mysql.cj.jdbc.Driver");
try (Connection conn = DriverManager.getConnection(
        "jdbc:mysql://localhost:3306/库名?useSSL=false&serverTimezone=Asia/Shanghai",
        "root", "123456");
     PreparedStatement ps = conn.prepareStatement("SELECT * FROM t_user WHERE id = ?")) {
    ps.setInt(1, 1);
    try (ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("name"));
        }
    }
}

8.4 易错清单

  • MySQL 8 驱动类名是 com.mysql.cj.jdbc.Driver(多个 .cj)
  • URL 协议前缀 jdbc:mysql: 少个冒号 → No suitable driver
  • 列索引从 1 开始
  • 取值前必须先 rs.next()
  • 关闭顺序逆序,且放 finally / try-with-resources
  • 增删改用 executeUpdate(),别对 INSERT 用 executeQuery()
  • 事务记得 setAutoCommit(false),忘了等于没开事务

总结一句话:JDBC = 接口规范 + 厂商驱动,六步模板 + 预编译占位 + 事务三连,是后面所有持久层框架的地基。

学完这篇,再去看 Druid 连接池和 MyBatis 的 $ 与 # 区别(就是 Statement 与 PreparedStatement 的区别),会非常丝滑。

相关推荐
程序猿乐锅1 小时前
一文讲透缓存穿透、击穿和雪崩
java·redis·spring·缓存·mybatis
xcl09251 小时前
西安同城货运系统源码开发实战 核心功能实现与部署全指南
java·spring boot
pippocao2 小时前
王者荣耀日志组件BqLog为什么这么快之1——高性能实时压缩日志
java
AC赳赳老秦2 小时前
公开 CSV 数据集批量处理实战:用 OpenClaw 高效完成下载、清洗与标准化分析样本生成
java·开发语言·汇编·c++·python·deepseek·openclaw
ym hyd 1112 小时前
慢性病精细化管理平台源码 Java+SpringBoot+Vue3 前后分离
java·vue.js·spring boot·毕设
程序猿乐锅2 小时前
【黑马点评 | 第七篇】Redis 分布式锁的两种实现
java·数据库·redis·分布式·spring·缓存
SL_staff2 小时前
JVS私有化交付为何敢承诺100%源码开放与无兜底风险?
java·低代码·全栈
需要8262 小时前
MySQL MVCC 与事务隔离级别:从一条 update 看版本链
java·数据库·spring boot·mysql·spring cloud
Memory_荒年2 小时前
订单超时未支付?从“定时扫库”一步步到最终形态
java·后端