发现自己的语雀里面有之前学习的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.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 的世界:
- MySQL、Oracle、SQL Server 各家的通信协议、调用方式完全不同;
- Java 代码直接耦合某一家数据库的私有 API;
- 一旦换数据库,所有数据访问代码全部重写。
有了 JDBC:
- 我们面向统一的接口 编程(
Connection、Statement......); - 数据库厂商负责实现接口(写好驱动 jar 包);
- 换数据库 = 换个 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)之后,把这行删了程序照样能跑,为什么?
是两件事配合帮我们自动完成了:
-
驱动 jar 包里自带文件:
META-INF/services/java.sql.Driver,内容就是驱动类的全限定名; -
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();
原理分两步:
- 预编译 :
prepareStatement(sql)阶段,含?的 SQL 模板先发给数据库完成语法分析、生成执行计划 ------ 此时 SQL 的结构已经定死; - 参数单独传 :后续
setXxx()传的参数只作为纯数据 填入占位符:- 服务端预编译模式(URL 加
useServerPrepStmts=true):参数走二进制协议单独传输; - MySQL 驱动默认客户端模式:参数由驱动做严格转义后再发送;
- 无论哪种模式,参数永远不可能参与 SQL 语法解析。
- 服务端预编译模式(URL 加
(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 的区别),会非常丝滑。