JavaEE 环境与程序设计 · 交互课程
技术演进接力链(M1 → M9)
  1. 1WEB 基础
    HTTP / Browser / WebServer
    静态页面写死,人人看到一样
  2. 2Servlet
    手写 if-else 分发不可扩展,无生命周期/并发管理
  3. 3JSP:参数散字符串
    参数散成一堆 String,取值靠字段名硬记
  4. 3JSP:scriptlet 塞逻辑
    scriptlet 把逻辑塞进页面,代码与 HTML 糊在一起
  5. 3JavaBean:数据对象化
    对象全靠 new、层层硬依赖
  6. 4Spring IoC
    Web 路由仍手写 web.xml/if-else、手动取参绑定,展示/业务/数据访问没分开
  7. 5MVC · SpringMVC
    数据在库里,访问是重复样板 + 拼串有注入风险
  8. 6MySQL
    Java 连库要手写五步,连接与资源管理全手动
  9. 7JDBC
    SQL 与 Java 混写、结果集逐行手映射、没有动态 SQL
  10. 8MyBatis
    四份配置 + web.xml 装配繁琐,部署要外部 Tomcat
  11. 9SpringBoot
    链尾 · 不考核

JDBC 数据访问

承接上一章的死结
上一章(MySQL)的死结:Java 连库要手写五步、连接与资源管理全手动。本章讲 JDBC——Web 层与数据库之间的桥。

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 → 结果与关闭」逐步高亮每条消息, 看调用如何在各对象之间流动、运行期如何落到具体实现。

① 加载驱动② 建立连接③ 创建语句④ 执行 SQL⑤ 结果与关闭
第 1 / 12 步 · ① 加载驱动
Class.forName("com.mysql.jdbc.Driver")—— 触发驱动类的 static 块,把自身登记进 DriverManager
应用程序main()DriverManager静态工厂Driver接口 · com.mysql.jdbc.DriverConnection接口 · ConnectionImplStatement接口 · StatementImplResultSet接口 · ResultSetImplClass.forName

接口与具体实现(多态示例)

接口MySQL 驱动实现作用
Drivercom.mysql.jdbc.Driver连接数据库的入口,负责自注册
ConnectionConnectionImpl一条到数据库的连接会话
StatementStatementImpl执行 SQL、拿结果集的句柄
ResultSetResultSetImpl查询返回的行集,可逐行遍历

经典 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() 逐行取、手动映射成对象、手动关资源。痛点清单:

  1. 模板重复——五步样板每个 DAO 抄一遍;
  2. 手映射——rs.getXxx() 一行行拷进实体,字段多就累且易错;
  3. 无动态 SQL——条件组合多时拼字符串很痛苦。

这正是 M8 MyBatis 要解决的:用 XML/注解声明 SQL,自动把 ResultSet 映射成对象、自动管理资源、支持动态 SQL。MyBatis 底层仍是 JDBC——它只是把这五步的重复动作封装掉了。想看清底层,回看本节即可。

思考题

  1. JDBC 的四大接口(Driver/Connection/Statement/ResultSet)各自职责是什么?为什么说它们是「多态落点」?
  2. executeUpdate() 与 executeQuery() 分别用于什么语句?返回值各是什么?
  3. 字符串拼接 SQL 为什么会被注入?? 占位符 + setString 如何避免?
  4. 资源为什么要按 ResultSet → Statement → Connection 的顺序关闭?try-with-resources 有什么好处?
  5. 手写 JDBC 的三大痛点是什么?MyBatis 分别怎么解决?

学习小测验

学习小测验

Q1

JDBC 五步的正确顺序是( )

Q2

关于 JDBC 四大接口,正确的是( )

Q3

INSERT / UPDATE / DELETE 应使用哪个方法?返回值是( )

Q4

下面哪种写法能防 SQL 注入?

Q5

JDBC 资源正确的关闭顺序是( )

Q6

try (Connection conn = ...; PreparedStatement ps = ...) { try (ResultSet rs = ...) {...} } 的好处是( )

Q7

手写 JDBC 的三大痛点中,MyBatis 用「XML/注解声明 SQL + 自动映射对象」主要解决的是( )

本章留下的死结
SQL 与 Java 混写、结果集逐行手映射、没有动态 SQL(下一章 MyBatis)。