- 1WEB 基础HTTP / Browser / WebServer静态页面写死,人人看到一样
- 2Servlet手写 if-else 分发不可扩展,无生命周期/并发管理
- 3JSP:参数散字符串参数散成一堆 String,取值靠字段名硬记
- 3JSP:scriptlet 塞逻辑scriptlet 把逻辑塞进页面,代码与 HTML 糊在一起
- 3JavaBean:数据对象化对象全靠 new、层层硬依赖
- 4Spring IoCWeb 路由仍手写 web.xml/if-else、手动取参绑定,展示/业务/数据访问没分开
- 5MVC · SpringMVC数据在库里,访问是重复样板 + 拼串有注入风险
- 6MySQLJava 连库要手写五步,连接与资源管理全手动
- 7JDBCSQL 与 Java 混写、结果集逐行手映射、没有动态 SQL
- 8MyBatis四份配置 + web.xml 装配繁琐,部署要外部 Tomcat
- 9SpringBoot链尾 · 不考核
JDBC 数据访问
JDBC 数据访问
目标:理解 JDBC(Java Database Connectivity)是 Java 访问关系数据库的标准 API;掌握五步时序(加载驱动 → Connection → Statement/PreparedStatement → 执行 → ResultSet → 逐级关闭)、四大接口与多态落点、
?占位符防 SQL 注入、资源关闭顺序。本模块承接 M6「数据在数据库里」:MySQL 存好了数据,Java 程序怎么连上它、发 SQL、取结果? 答案就是 JDBC——它是 Web 层与数据库之间的桥,也是 M8 MyBatis 的底层(MyBatis 只是把 JDBC 的重复动作封装掉)。
一、为什么需要 JDBC:Java 连库要手写五步
M6 讲数据存进 MySQL。现在 Java 要读它,得手工完成一串固定动作:加载驱动 → 拿连接 → 造语句 → 执行 → 取结果 → 逐级关闭。这套「五步」每个 DAO 都要写一遍,模板重复、易漏关闭——这正是后面 M8 MyBatis 要解决的痛点(先记住)。
JDBC 是什么:一套标准接口(
java.sql.*/javax.sql.*)。Java 只面向这些接口编程,具体实现由数据库厂商提供(MySQL 的驱动 jar)——这是多态的典型落点。
二、四大接口与多态落点
JDBC 核心是四个接口,声明在 Java、实现在厂商驱动:
| 接口 | 职责 | 谁来实现 |
|---|---|---|
Driver / DriverManager | 加载驱动、建立连接 | MySQL 驱动(com.mysql.cj.jdbc.Driver) |
Connection | 一次数据库连接(会话) | 厂商驱动 |
Statement / PreparedStatement | 承载一条 SQL 并执行 | 厂商驱动 |
ResultSet | 查询结果集,逐行读取 | 厂商驱动 |
// 面向接口编程:变量类型是接口,实际对象是 MySQL 驱动给的实现类(多态)
Connection conn = DriverManager.getConnection(url, user, pwd); // 返回的是 MySQL 的实现
Statement st = conn.createStatement(); // 同上
ResultSet rs = st.executeQuery(sql); // 同上
多态落点:
DriverManager→Connection→Statement→ResultSet一路都是「接口类型变量 + 厂商实现对象」。换数据库(MySQL→Oracle)只需换驱动与 url,业务代码几乎不动——这就是面向接口的好处。下面时序图把这五步拆开看:
交互实验:JDBC 五步时序图(JDBCSequenceDiagram)
接口 · 多态JDBC 里 Driver / Connection / Statement / ResultSet 都是接口,MySQL 驱动提供具体实现。 下面按「加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 结果与关闭」逐步高亮每条消息, 看调用如何在各对象之间流动、运行期如何落到具体实现。
接口与具体实现(多态示例)
| 接口 | MySQL 驱动实现 | 作用 |
|---|---|---|
| Driver | com.mysql.jdbc.Driver | 连接数据库的入口,负责自注册 |
| Connection | ConnectionImpl | 一条到数据库的连接会话 |
| Statement | StatementImpl | 执行 SQL、拿结果集的句柄 |
| ResultSet | ResultSetImpl | 查询返回的行集,可逐行遍历 |
经典 JDBC 五步代码
// ① 加载驱动:触发其 static 块完成自注册
Class.forName("com.mysql.jdbc.Driver");
// ② 建立连接:DriverManager 按 URL 选中具体驱动实现(多态)
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mytest", "root", "");
// ③ 创建语句
Statement stmt = conn.createStatement();
// ④ 执行 SQL
stmt.executeUpdate("insert into guestbook(username,title) values('tom','from tom')");
ResultSet rs = stmt.executeQuery("select * from guestbook");
// ⑤ 处理结果 + 关闭(顺序与创建相反)
while (rs.next()) {
System.out.println(rs.getInt("id") + " " + rs.getString("username"));
}
rs.close();
stmt.close();
conn.close();JDBC / 多态要点
- JDBC 的核心是接口:Driver / Connection / Statement / ResultSet 都是接口,厂商提供具体实现——编译期依赖接口、运行期由 DriverManager 按 URL 选中实现,正是模块2 学的多态落到真实 API;
- 五步时序:加载驱动 → 建立连接 → 创建语句 → 执行 SQL → 处理结果并关闭;资源释放顺序与创建相反(先 rs/stmt,再 conn);
- Class.forName 只负责触发驱动类的 static 块完成自注册;JDBC 4.0 起可由 SPI 自动发现而省略,但显式写出更稳妥、便于定位问题;
- 拼接 SQL 字符串有 SQL 注入风险:应使用 PreparedStatement 预编译 + 占位符 ?,把数据与命令分离(见下一节)。
三、五步时序:连接 → 语句 → 执行 → 结果 → 关闭
以 GuestBookJDBC 的 WordDao 为例,一次「查」的完整链路:
// ① 加载驱动(新版本 JDBC4 可省,写出来更直观)
Class.forName("com.mysql.cj.jdbc.Driver");
// ② 建立连接
Connection conn = DriverManager.getConnection(url, user, pwd);
// ③ 造语句
Statement st = conn.createStatement();
// ④ 执行查询
ResultSet rs = st.executeQuery("SELECT * FROM guestbook");
// ⑤ 逐行读取
while (rs.next()) {
int id = rs.getInt("id");
String title = rs.getString("title");
}
// ⑥ 逐级关闭(见第五节)
executeUpdate vs executeQuery:
INSERT/UPDATE/DELETE用executeUpdate(),返回受影响行数;SELECT用executeQuery(),返回一个 ResultSet。M6 的 CRUDVisualizer 里「增删改」和「查」走的就是这两条路。
四、? 占位符防 SQL 注入:拼接串 vs PreparedStatement
字符串拼接有致命安全隐患——SQL 注入:
// ❌ 危险:把用户输入直接拼进 SQL
String sql = "SELECT * FROM guestbook WHERE title='" + userInput + "'";
// 若 userInput = "x' OR '1'='1",整张表被拖出来
PreparedStatement 用 ? 占位符,参数与 SQL 结构分离,数据库按「值」而非「代码」处理参数,天然防注入:
// ✅ 安全:? 占位 + setString 绑定
PreparedStatement ps = conn.prepareStatement("SELECT * FROM guestbook WHERE title=?");
ps.setString(1, userInput); // 用户输入只当「值」,不可能改变 SQL 结构
ResultSet rs = ps.executeQuery();
交互实验:下面组件把「拼接串 vs 占位符」并排对照,输入恶意串看哪条被注入、哪条安全——直观理解为什么必须用
?:
交互实验:预编译语句实验台(PreparedStatementLab)
SQL 模板 SELECT * FROM users WHERE name = ?—— 填一个 name,看绑定版如何把它当数据;再试注入样例,对比拼接版怎样被改变结构。
字符串拼接(危险)
SELECT * FROM users WHERE name = '张三'
正常值下看不出问题——但攻击者输入带引号的值时,结构就被改了。
参数绑定 setString(安全)
SELECT * FROM users WHERE name = ?
→ 参数[0] = "张三"(数据,不是代码)✓ SQL 结构永远不变:值只是被当作数据字面量传进去——admin' -- 也匹配不到任何行,注入失效。
预编译的三件好处:防注入(结构与数据分离)+ 可复用(同一条 SQL 反复 setParameter 执行)+ 数据库优化(预编译计划缓存)。
五、资源关闭顺序:ResultSet → Statement → Connection
JDBC 资源是有限且昂贵的(连接尤其贵),用完必须关,且要由内向外逐级关:
// 传统写法:逆序关闭,try/finally 保证一定执行
try {
// ... 使用 rs / st / conn
} finally {
if (rs != null) rs.close(); // 先关结果集
if (st != null) st.close(); // 再关语句
if (conn != null) conn.close(); // 最后关连接
}
// 推荐:try-with-resources(Java 7+),自动按声明逆序关闭,省去判空
try (Connection conn = DriverManager.getConnection(url, user, pwd);
PreparedStatement ps = conn.prepareStatement(sql)) {
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) { /* ... */ }
}
}
为什么逆序:ResultSet 依赖 Statement、Statement 依赖 Connection——先关「外层」会让「内层」失效。try-with-resources 自动按声明的逆序关闭,最省心也不易漏。
六、JDBC 痛点清单 → M8 MyBatis 预告
手写 JDBC 每处都要重复:拼 SQL、绑参数、rs.next() 逐行取、手动映射成对象、手动关资源。痛点清单:
- 模板重复——五步样板每个 DAO 抄一遍;
- 手映射——
rs.getXxx()一行行拷进实体,字段多就累且易错; - 无动态 SQL——条件组合多时拼字符串很痛苦。
这正是 M8 MyBatis 要解决的:用 XML/注解声明 SQL,自动把 ResultSet 映射成对象、自动管理资源、支持动态 SQL。MyBatis 底层仍是 JDBC——它只是把这五步的重复动作封装掉了。想看清底层,回看本节即可。
思考题
- JDBC 的四大接口(Driver/Connection/Statement/ResultSet)各自职责是什么?为什么说它们是「多态落点」?
executeUpdate()与executeQuery()分别用于什么语句?返回值各是什么?- 字符串拼接 SQL 为什么会被注入?
?占位符 +setString如何避免? - 资源为什么要按 ResultSet → Statement → Connection 的顺序关闭?try-with-resources 有什么好处?
- 手写 JDBC 的三大痛点是什么?MyBatis 分别怎么解决?
学习小测验
学习小测验
JDBC 五步的正确顺序是( )
关于 JDBC 四大接口,正确的是( )
INSERT / UPDATE / DELETE 应使用哪个方法?返回值是( )
下面哪种写法能防 SQL 注入?
JDBC 资源正确的关闭顺序是( )
try (Connection conn = ...; PreparedStatement ps = ...) { try (ResultSet rs = ...) {...} } 的好处是( )
手写 JDBC 的三大痛点中,MyBatis 用「XML/注解声明 SQL + 自动映射对象」主要解决的是( )