简介:本资源是吉林大学《数据库应用程序开发》课程的完整实验报告,面向计算机专业本科生及Java数据库开发初学者,聚焦JDBC核心实践能力培养。报告覆盖8个递进式实验任务:从JDBC连接配置、SELECT查询与ResultSet遍历,到PreparedStatement参数化更新、事务控制(setAutoCommit)、GUI界面集成(JOptionPane)、多模式数据插入(单行/多行/子查询)、空值统一显示、结构化异常处理(约束/类型/溢出),直至结果集动态编辑,完整呈现数据库应用开发全流程排错思路与实操细节。资源为1个1.76MB的Word文档(.doc),内容含详细调试过程、典型错误截图分析、SQL权限配置、DB2驱动加载、事务提交修复等关键知识点,结构清晰、步骤翔实。目前已有900人学习下载,是掌握Java+DB2数据库交互、夯实JDBC编程基础的优质教学实践材料。
1. 这不是一份交差的实验报告,而是一套可直接复用的 JDBC + DB2 + Swing 实战模板
你有没有试过:写完一段 JDBC 连接代码,跑起来报DB2ADMIN.TEMPL is an undefined name,查了半小时才发现是 schema 名没写全;或者executeUpdate()返回 1,但数据库里死活查不到新数据,最后发现setAutoCommit(false)后忘了commit();又或者用户输个 20,程序直接java.lang.ArithmeticException: Overflow崩掉,连错误提示都卡在控制台里——而你的 GUI 界面还傻乎乎弹着“操作成功”。这不是玄学,是每个刚接触企业级数据库 Java 开发的人必踩的坑。这份来自吉林大学《数据库应用程序开发》实验的完整源码包,恰恰把这八类高频翻车场景全打穿了:从最基础的DriverManager.getConnection配置、PreparedStatement参数绑定,到事务开关时机、空值统一渲染、SQL 异常分级捕获(约束冲突/类型不匹配/溢出/非法符号),再到 Swing 表单与 ResultSet 的双向绑定改造。它不讲 JDBC 接口定义,只给你能粘贴进自己项目、改两行就能跑通的.java文件;它不画 UML 图,但每个任务的代码结构都暗合 DAO 分层雏形;它甚至把JOptionPane的调用封装成一行showErrorDialog("违反主键约束")——你拿到手,就是一套带血泪经验的数据库交互脚手架。
2. 环境准备与工程结构:DB2 驱动、JDBC URL、Schema 命名三件套必须对齐
2.1 DB2 数据库环境初始化:SAMPLE 数据库与 JLU Schema 是前提
实验所有任务均依赖本地已安装的 IBM DB2 Express-C 或类似版本(实验文档未指定具体版本,但根据COM.ibm.db2.jdbc.app.DB2Driver类名可判定为 DB2 8.x–10.x 时代经典驱动)。关键前提是:
- 数据库实例中必须存在名为
SAMPLE的示例数据库(DB2 自带,可通过db2sampl命令创建); SAMPLE库内需存在JLU模式(schema),且该模式下有staff、employee、TEMPL等表(注意:TEMPL是实验自建表,非 DB2 默认表);- 用户
db2admin必须被显式授予对JLU下所有表的SELECT,INSERT,UPDATE,DELETE权限。
提示:权限问题在任务 1 中首次暴露。正确授予权限的 SQL 语句为:
CONNECT TO SAMPLE USER db2admin USING db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE JLU.staff TO USER db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE JLU.employee TO USER db2admin; GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE JLU.TEMPL TO USER db2admin;
2.2 Java 工程依赖配置:DB2 JDBC 驱动 jar 包路径必须硬编码进 classpath
实验使用的是 DB2 旧版 JDBC 驱动(db2java.zip),其核心类为COM.ibm.db2.jdbc.app.DB2Driver。该驱动不兼容新版ibm-db2-jcc(即com.ibm.db2.jcc.DB2Driver),若强行替换会导致ClassNotFoundException。因此,构建工程时必须:
- 将
db2java.zip(或解压后的db2java.jar)放入项目lib/目录; - 在编译和运行命令中显式添加
-cp参数,确保驱动在 classpath 顶端。
例如,编译并运行labstaff.java的完整命令链为:
# 编译(假设当前目录含 labstaff.java 和 lib/db2java.zip) javac -cp ".:lib/db2java.zip" labstaff.java # 运行(注意:JDBC URL 中的 "JDBC:db2:sample" 是 DB2 专用格式,冒号不能少) java -cp ".:lib/db2java.zip" labstaff注意:Linux/macOS 用
:分隔 classpath,Windows 用;。若漏掉db2java.zip,会报java.lang.ClassNotFoundException: COM.ibm.db2.jdbc.app.DB2Driver——这是任务 3 中明确要求Class.forName(...)的根本原因:驱动类必须由 JVM 显式加载注册。
2.3 JDBC URL 与 Schema 命名规范:JLU.TEMPL 不是笔误,而是 DB2 的强制约定
任务 1 中的错误DB2ADMIN.TEMPL is an undefined name,表面看是权限问题,实则是 DB2 对对象引用的严格解析规则所致。DB2 要求:
- 所有表、视图等数据库对象,必须以 schema 名作为前缀(除非当前连接用户默认 schema 就是该对象所属 schema);
TEMPL是表名,JLU是 schema 名,二者组合JLU.TEMPL才构成 DB2 可识别的完全限定名(Fully Qualified Name);- 若代码中写
TEMPL,DB2 会默认查找DB2ADMIN.TEMPL(因当前用户是db2admin),而该用户下无此表,故报错。
因此,所有 SQL 语句中的表名都必须补全 schema:
// ✅ 正确:显式指定 JLU schema String sql = "SELECT * FROM JLU.staff WHERE ID = ?"; String insertSql = "INSERT INTO JLU.TEMPL (ID, NAME) VALUES (?, ?)"; // ❌ 错误:DB2 无法解析裸表名 String wrongSql = "SELECT * FROM staff"; // → 解析为 DB2ADMIN.staff2.4 工程目录结构:8 个任务对应 8 个独立 .java 文件,无 Maven/Gradle
整个实验包是典型的教学型扁平结构,无构建工具,纯手工管理依赖。标准目录如下:
jlu-db2-lab/ ├── lib/ │ └── db2java.zip # DB2 旧版 JDBC 驱动(核心依赖) ├── src/ # 所有 Java 源文件 │ ├── labstaff.java # 任务 2:基础查询(Statement + ResultSet) │ ├── labupdate.java # 任务 3:更新操作(PreparedStatement + 事务) │ ├── labupdate_gui.java # 任务 4:GUI 版更新(JOptionPane 输入/输出) │ ├── labinsert.java # 任务 5:插入操作(单行/多行/子查询) │ ├── labinsert_null.java # 任务 6:空值统一显示为"空" │ ├── labinsert_error.java # 任务 7:异常分级对话框提示 │ └── labstaff1.java # 任务 8:带 SQLWarning 处理的参数化查询 └── README.md # 环境说明与运行指引提示:所有
.java文件均无 package 声明,全部位于默认包(default package)。这意味着它们必须放在 classpath 根目录下编译运行,不可嵌套子目录,否则NoClassDefFoundError会立刻报错。
3. 核心模块拆解:从 Connection 到 PreparedStatement,一条 SQL 的完整生命周期
3.1 连接建立:DriverManager.getConnection 的三个参数缺一不可
DriverManager.getConnection()是 JDBC 的入口,实验中所有任务均采用相同模式:
Connection sample = DriverManager.getConnection( "JDBC:db2:sample", // URL:DB2 专用格式,"JDBC:db2:" 前缀 + 数据库别名 "db2admin", // 用户名:必须与 DB2 实例中创建的用户一致 "db2admin" // 密码:明文硬编码,教学场景可接受,生产环境严禁 );- URL 解析:
JDBC:db2:sample中sample是数据库别名(Database Alias),非数据库名。该别名需在 DB2 Catalog 中注册(db2 catalog db sample),且指向实际数据库路径。若别名错误,报SQLException: [IBM][CLI Driver] SQL1013N。 - 用户名/密码:
db2admin是 DB2 安装时创建的默认管理员账户。若实验环境修改过密码,此处必须同步更新,否则报SQLException: [IBM][CLI Driver] SQL0801N(认证失败)。 - 连接池缺失:教学代码未引入 HikariCP 或 Druid,每次操作新建连接。这在实验中可接受,但真实项目必须改造为连接池管理,否则高并发下会耗尽 DB2 连接数。
3.2 查询执行:Statement 与 PreparedStatement 的本质区别在哪?
任务 2 使用Statement,任务 3/5/8 使用PreparedStatement。二者差异不仅是“预编译”这个教科书说法,而是安全与性能的双重分水岭:
| 维度 | Statement(任务 2) | PreparedStatement(任务 3/5/8) |
|---|---|---|
| SQL 注入防护 | ❌ 无防护:"SELECT * FROM staff WHERE ID = " + userInput | ✅ 天然免疫:pstmt.setInt(1, userInput)参数化绑定 |
| 执行效率 | 每次执行都需 DB2 解析、编译、生成执行计划 | 首次编译后缓存执行计划,后续仅绑定参数,快 3–5 倍 |
| 类型安全 | ❌ 全靠字符串拼接,"WHERE SALARY > " + salaryStr易错 | ✅pstmt.setDouble(1, salary)强制类型转换,错则早报 |
以任务 3 的更新为例,关键代码段如下:
// 1. 定义带 ? 占位符的 SQL(注意:? 不能用于表名/列名!) String sqlstmt = "UPDATE JLU.staff SET SALARY = SALARY * ? WHERE ID = ?"; // 2. 创建 PreparedStatement(此时 SQL 已被 DB2 解析并预编译) PreparedStatement pstmt = sample.prepareStatement(sqlstmt); // 3. 绑定参数:索引从 1 开始,类型严格匹配 pstmt.setDouble(1, 1.01); // 第一个 ? → double 类型乘数 pstmt.setInt(2, 15); // 第二个 ? → int 类型 ID // 4. 执行:返回受影响行数(int),非 boolean int updateCount = pstmt.executeUpdate(); System.out.println("更新了 " + updateCount + " 行");注意:
executeUpdate()返回int,表示 DML(INSERT/UPDATE/DELETE)影响的行数。若返回 0,说明WHERE条件未匹配到任何记录(如任务 3 中输入 16 时),这并非错误,而是业务正常态。新手常误以为 0=失败,导致加冗余日志。
3.3 结果集处理:ResultSet.next() 是游标移动,不是数据读取
ResultSet是 JDBC 中最易误解的对象。任务 2 和 8 中反复出现rs.next(),但它的真实作用是:
- 移动内部游标:初始位置在第一行之前,
rs.next()将游标移至下一行; - 返回布尔值:
true表示成功移到有效行,false表示已到末尾; - 不读取数据:数据读取需调用
rs.getString("NAME")等 getter 方法。
典型错误写法(任务 2 调试过程暴露):
// ❌ 错误:next() 被调用两次,跳过第一行 boolean more = rs.next(); // 移到第 1 行,返回 true more = rs.next(); // 移到第 2 行,第 1 行数据丢失 String name = rs.getString("NAME"); // 读取的是第 2 行的 NAME! // ✅ 正确:一次 next() + 一次读取,循环处理 while (rs.next()) { // 移到下一行,并检查是否有效 String name = rs.getString("NAME"); // 读取当前行数据 String job = rs.getString("JOB"); double sal = rs.getDouble("SALARY"); System.out.println(name + ", " + job + ", " + sal); }提示:
rs.getString("COL_NAME")中列名大小写敏感,必须与数据库元数据中定义的完全一致(JLU.staff表中列为大写NAME/JOB/SALARY)。若写成rs.getString("name"),返回null。
3.4 事务控制:setAutoCommit(false) 后,commit() 是唯一生效出口
任务 3 明确要求sample.setAutoCommit(false),这是理解数据库事务的关键切口。其逻辑链为:
- 关闭自动提交 → 所有 DML 操作进入“待定状态”,不写入磁盘;
- 手动调用
sample.commit()→ 将待定变更持久化,其他连接可见; - 手动调用
sample.rollback()→ 废弃所有待定变更,回滚到上一个 commit 点。
实验中“修改成功但数据库无变化”的根本原因,就是漏掉了commit():
sample.setAutoCommit(false); // 关闭自动提交 pstmt.executeUpdate(); // 执行更新,但仅在内存/事务日志中 // ❌ 此处缺少 sample.commit(); // → 程序退出时,JVM 关闭连接,DB2 自动 rollback(隐式回滚) // ✅ 正确流程 try { pstmt.executeUpdate(); sample.commit(); // 显式提交,数据真正落库 } catch (SQLException e) { sample.rollback(); // 出错则回滚 throw e; }注意:DB2 的
setAutoCommit(false)是连接级设置,同一Connection对象的所有操作共享该状态。若一个线程中混用多个PreparedStatement,必须确保它们共用同一个Connection并统一管理 commit/rollback。
4. GUI 与异常处理:从控制台到 JOptionPane,错误信息必须对用户友好
4.1 Swing GUI 改造:JOptionPane 是教学场景的最优解
任务 4 和 5 要求将控制台 I/O 替换为图形界面。实验选用javax.swing.JOptionPane,因其零依赖、无需布局管理器、一行代码即可弹窗,完美契合教学快速验证需求。核心方法映射关系如下:
| 控制台操作 | JOptionPane 替代方案 | 示例代码 |
|---|---|---|
System.out.println() | JOptionPane.showMessageDialog()(信息提示) | JOptionPane.showMessageDialog(null, "更新成功!"); |
Scanner.nextLine() | JOptionPane.showInputDialog()(输入对话框) | String idStr = JOptionPane.showInputDialog("请输入员工ID:"); |
System.err.println() | JOptionPane.showMessageDialog()(错误提示) | JOptionPane.showMessageDialog(null, "ID 格式错误!", "输入错误", ERROR_MESSAGE); |
以任务 4 的labupdate_gui.java为例,用户输入与结果反馈的完整闭环:
// 1. 获取用户输入(字符串) String idStr = JOptionPane.showInputDialog("请输入员工ID:"); if (idStr == null) return; // 用户点取消,直接退出 // 2. 类型转换(关键:捕获 NumberFormatException) int id; try { id = Integer.parseInt(idStr); } catch (NumberFormatException e) { JOptionPane.showMessageDialog(null, "ID 必须为整数!", "输入错误", JOptionPane.ERROR_MESSAGE); return; } // 3. 执行数据库更新(省略 JDBC 代码) int count = executeUpdate(id, 1.01); // 假设此方法封装了 PreparedStatement // 4. 反馈结果 if (count > 0) { JOptionPane.showMessageDialog(null, "成功更新 " + count + " 行", "操作成功", JOptionPane.INFORMATION_MESSAGE); } else { JOptionPane.showMessageDialog(null, "未找到 ID 为 " + id + " 的员工", "提示", JOptionPane.WARNING_MESSAGE); }提示:
JOptionPane的parentComponent参数传null表示无父窗口,弹窗居中显示。若传入JFrame实例,则弹窗相对父窗体居中,且模态阻塞父窗体。
4.2 空值统一渲染:ResultSet.getObject() + null 判断是唯一可靠方案
任务 6 要求将结果集中所有NULL值显示为字符串"空"。常见误区是用rs.getString()—— 该方法对NULL列返回null,但若列类型为INT或DATE,rs.getString()可能抛SQLException。正确做法是rs.getObject()+ 显式 null 判断:
// 遍历 ResultSet 所有列(通用处理) for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) { Object value = rs.getObject(i); // 安全获取任意类型值 String display = (value == null) ? "空" : value.toString(); System.out.print(display + "\t"); } System.out.println();rs.getMetaData().getColumnCount()动态获取列数,避免硬编码列数;rs.getObject(i)返回Object,可安全处理NULL及所有 JDBC 类型(Integer,String,Date,BigDecimal等);value.toString()对非 null 值调用,null值已被提前拦截,不会触发 NPE。
4.3 异常分级捕获:SQLState 是 DB2 错误分类的黄金标准
任务 7 要求对“违反约束、类型不匹配、溢出、非法符号”四类错误分别提示。DB2 通过SQLState字符串(5 位码)精准标识错误类型,比getMessage()文本解析更稳定。实验中关键SQLState映射如下:
| 错误类型 | SQLState 前两位 | DB2 典型错误码 | JOptionPane 提示文案 |
|---|---|---|---|
| 主键/唯一约束冲突 | 23 | SQL0803N | "违反主键约束:该ID已存在" |
| 外键约束失败 | 23 | SQL0530N | "外键约束失败:部门不存在" |
| 类型不匹配 | 42 | SQL0408N | "类型不匹配:请检查输入格式(如ID应为数字)" |
| 数值溢出 | 22 | SQL0802N | "数值溢出:计算结果超出字段范围" |
| SQL 语法错误 | 42 | SQL0104N | "SQL 语法错误:语句中包含非法符号" |
异常处理核心代码:
} catch (SQLException e) { String sqlState = e.getSQLState(); String message = e.getMessage(); if (sqlState.startsWith("23")) { JOptionPane.showMessageDialog(null, "违反约束:" + message, "数据约束错误", JOptionPane.ERROR_MESSAGE); } else if (sqlState.startsWith("22")) { JOptionPane.showMessageDialog(null, "数值溢出:" + message, "计算错误", JOptionPane.ERROR_MESSAGE); } else if (sqlState.startsWith("42")) { JOptionPane.showMessageDialog(null, "语法或类型错误:" + message, "输入错误", JOptionPane.ERROR_MESSAGE); } else { JOptionPane.showMessageDialog(null, "数据库错误:" + message, "系统错误", JOptionPane.ERROR_MESSAGE); } }注意:
e.getSQLState()返回String,必须用startsWith()判断前缀,而非equals()。因为SQLState后三位可能随具体错误微调(如23505和23503都属约束类)。
4.4 避坑 / 常见问题 / 排查:8 个任务中踩出的 5 条血泪经验
现象 1:java.lang.ClassNotFoundException: COM.ibm.db2.jdbc.app.DB2Driver
原因:db2java.zip未加入 classpath,或路径错误导致 JVM 找不到该 jar。
解决:确认lib/db2java.zip存在,编译/运行命令中-cp参数包含该路径,且COM.ibm.db2.jdbc.app.DB2Driver类名拼写完全正确(注意大小写和包路径中的点号)。
现象 2:SQLException: [IBM][CLI Driver] SQL0801N(用户名/密码错误)
原因:DriverManager.getConnection()第二、三参数(用户名/密码)与 DB2 实际用户不匹配。
解决:用 DB2 命令行db2 connect to SAMPLE user db2admin using db2admin测试连通性;若失败,重置db2admin密码或检查 DB2 实例是否启动。
现象 3:executeUpdate()返回 0,但业务上应有数据
原因:WHERE条件未匹配到任何行(如WHERE ID = 16,但staff表中无 ID=16 的记录),非程序错误。
解决:先用 DB2 命令db2 select * from JLU.staff where ID = 16验证数据存在性;若数据存在,检查 SQL 中 schema 名(JLU.)是否遗漏。
现象 4:GUI 窗口弹出后程序无响应(假死)
原因:Swing 是单线程模型(Event Dispatch Thread),若 JDBC 操作(如executeQuery())在 EDT 中执行,会阻塞 UI 线程。
解决:将数据库操作移至后台线程(SwingWorker),UI 更新放回 EDT。教学代码未处理,故建议仅在数据量小时使用;真实项目必须改造。
现象 5:ArithmeticException: Overflow发生在pstmt.setDouble(1, 2.0)之后
原因:SALARY字段在 DB2 中定义为SMALLINT(范围 -32768~32767),SALARY * 2后超出上限。
解决:用pstmt.setDouble(1, 1.01)降低倍数;或修改表结构ALTER TABLE JLU.staff ALTER COLUMN SALARY SET DATA TYPE DECIMAL(10,2)扩大精度。
5. 二次开发实战:如何把实验代码改造成可维护的 DAO 工具类
5.1 从散装代码到 DAO 分层:抽取 ConnectionFactory 与 StaffDAO
实验的 8 个.java文件全是面向过程的脚本式代码,重复出现DriverManager.getConnection()、pstmt.setString()等样板。要投入真实项目,第一步必须抽象为 DAO(Data Access Object)模式。核心改造点:
步骤 1:创建ConnectionFactory工具类,统一管理连接
// src/utils/ConnectionFactory.java package utils; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class ConnectionFactory { private static final String URL = "JDBC:db2:sample"; private static final String USER = "db2admin"; private static final String PASSWORD = "db2admin"; static { try { // 驱动加载只需一次,在类加载时完成 Class.forName("COM.ibm.db2.jdbc.app.DB2Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("DB2 Driver not found!", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }- 优势:驱动加载
Class.forName()移至静态块,避免每个 DAO 方法重复调用;连接参数集中管理,修改只需改一处。
步骤 2:创建StaffDAO,封装 staff 表的 CRUD
// src/dao/StaffDAO.java package dao; import utils.ConnectionFactory; import java.sql.*; public class StaffDAO { // 查询单个员工 public Staff findById(int id) throws SQLException { String sql = "SELECT ID, NAME, JOB, SALARY FROM JLU.staff WHERE ID = ?"; try (Connection conn = ConnectionFactory.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setInt(1, id); try (ResultSet rs = pstmt.executeQuery()) { if (rs.next()) { return new Staff( rs.getInt("ID"), rs.getString("NAME"), rs.getString("JOB"), rs.getDouble("SALARY") ); } return null; // 未找到 } } } // 更新员工薪资(支持事务) public int updateSalary(int id, double multiplier) throws SQLException { String sql = "UPDATE JLU.staff SET SALARY = SALARY * ? WHERE ID = ?"; try (Connection conn = ConnectionFactory.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setDouble(1, multiplier); pstmt.setInt(2, id); conn.setAutoCommit(false); // 开启事务 int count = pstmt.executeUpdate(); conn.commit(); // 显式提交 return count; } catch (SQLException e) { // 自动 rollback(try-with-resources 关闭 conn 时触发) throw e; } } }- 关键改进:
- 使用
try-with-resources自动关闭Connection/PreparedStatement/ResultSet,杜绝资源泄漏; updateSalary()内部管理事务,调用方无需关心commit()/rollback();- 返回
Staff实体对象(需定义Staff.javaPOJO),而非原始ResultSet,实现数据与表现分离。
- 使用
5.2 GUI 层升级:用 JTable 替代 JOptionPane,实现真正的表格编辑
任务 5 的JOptionPane.showInputDialog()仅支持单行输入,无法满足“任意行、任意列修改”的任务 8 要求。真实场景应使用JTable:
步骤 1:创建StaffTableModel继承AbstractTableModel
// src/gui/StaffTableModel.java package gui; import dao.StaffDAO; import javax.swing.table.AbstractTableModel; import java.util.List; public class StaffTableModel extends AbstractTableModel { private final String[] columnNames = {"ID", "姓名", "职位", "薪资"}; private List<Staff> data = List.of(); // 初始化为空 @Override public int getRowCount() { return data.size(); } @Override public int getColumnCount() { return columnNames.length; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Staff staff = data.get(rowIndex); return switch (columnIndex) { case 0 -> staff.getId(); case 1 -> staff.getName(); case 2 -> staff.getJob(); case 3 -> staff.getSalary(); default -> null; }; } @Override public String getColumnName(int column) { return columnNames[column]; } // 刷新数据(从数据库加载) public void refreshData() throws SQLException { StaffDAO dao = new StaffDAO(); data = dao.findAll(); // 需在 StaffDAO 中补充 findAll() 方法 fireTableDataChanged(); // 通知 JTable 更新视图 } }步骤 2:在 JFrame 中嵌入 JTable,并监听单元格编辑
// src/gui/StaffEditorFrame.java JTable table = new JTable(new StaffTableModel()); table.setDefaultEditor(Object.class, new DefaultCellEditor(new JTextField())); // 允许编辑 // 监听编辑结束事件 table.getModel().addTableModelListener(e -> { if (e.getType() == TableModelEvent.UPDATE) { int row = e.getFirstRow(); int col = e.getColumn(); StaffTableModel model = (StaffTableModel) table.getModel(); Object newValue = model.getValueAt(row, col); // TODO:调用 StaffDAO.updateById() 更新数据库 System.out.println("更新第" + row + "行第" + col + "列:" + newValue); } });- 效果:用户直接在表格中双击单元格修改,回车确认,
TableModelListener捕获变更并触发数据库更新,彻底告别JOptionPane的割裂感。
5.3 异常处理增强:Log4j 日志 + 自定义业务异常
任务 7 的JOptionPane提示仅面向终端用户,但开发调试需要详细日志。引入 Log4j 2(轻量级):
步骤 1:添加 log4j-api 和 log4j-core 依赖(Maven)
<dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-api</artifactId> <version>2.20.0</version> </dependency> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.20.0</version> </dependency>步骤 2:在 DAO 中添加结构化日志
// StaffDAO.java 中 updateSalary 方法内 Logger logger = LogManager.getLogger(StaffDAO.class); try { // ... 执行更新 logger.info("Staff salary updated: id={}, multiplier={}, rows={}", id, multiplier, count); return count; } catch (SQLException e) { logger.error("Failed to update staff salary", e); // 输出完整堆栈 throw new BusinessException("薪资更新失败,请联系管理员", e); }- 价值:错误发生时,日志中同时包含业务上下文(
id=15)和底层异常(SQL0802N),排查效率提升 50% 以上。
从那以后我每次接手一个老项目,第一件事就是把所有System.out.println()和裸catch (Exception e) { e.printStackTrace(); }替换成Logger+BusinessException;第二件事是把所有DriverManager.getConnection()抽成ConnectionFactory。这两步做完,项目就从“能跑”变成了“敢改”。希望帮到你。
本文还有配套的精品资源,点击获取