
简介一份Java学生成绩管理系统课程设计报告面向计算机、软件工程等专业需要完成课程设计或毕业设计的学生与开发者。文档完整展示了一个基于C/S模式的学生成绩管理系统的设计过程涵盖学生信息管理、课程成绩维护、按学号或姓名查询、分数段统计、报表输出、用户权限管理等核心模块并给出需求分析、概要设计、数据库设计等关键环节适合作为系统开发与报告撰写的参考范本。压缩包内共1个doc文件约205KB全文采用高校课程设计报告标准格式包含任务书、进度安排、摘要、正文等部分。已有786人学习使用内容贴近课堂项目实战可直接复用或按需二次修改。1. 这门课设课到底在考什么学生把界面截图发过来说“老师点查询按钮之后界面卡死了”。点开代码一看查询按钮的事件里直接写 JDBC 查询再把结果while (rs.next())一条条拼到文本框里。数据库慢、网络慢、界面刷新慢三个慢叠在一起窗口直接被锁死。这是做学生成绩管理系统时出现频率最高的问题它拷问的并不是 Java 语法背得熟不熟而是你对数据、界面、逻辑三者边界的理解。Java 学生成绩管理系统课程设计这个题目在过去十年的教学大纲里反复出现几乎每一届都要做一遍。它把数据库建模、JDBC 访问、集合排序、面向对象分层这几个后端基础全部串在一个场景里工作量不大不小刚好适合做一次完整的练手。做它的意义不在“交一份作业”而是通过这个项目把 Java 的常用类、集合框架、SQL 语句、异常处理这些零散的知识点组织成一个能跑起来的系统。适合谁来读这篇内容三类人第一类是正在被课设折磨的学生需要一套能讲清楚原理又能照抄的代码第二类是刚学完 Java 基础想用一个项目把知识串起来的新手开发第三类是带毕设或带新人的工程师想找一个验收标准的参照。2. 先把需求拆开在系统需求分析里边界比功能更要命2.1 成绩管理系统的三类角色和七个用例拿到题目先别急着写代码。你哪怕用 ProcessOn 画一张用例图也能发现“学生成绩管理”这句话拆开之后大有文章。按教学里最常见的划分系统里面至少有三个角色管理员、教师、学生。管理员负责维护学生基本信息比如入学、转班、退学教师负责录入和修改成绩学生只能查询自己的成绩和排名。数据库里的表随角色的出现自然分化学生表、课程表、成绩表。功能模块拆到用例粒度参照系是七个登录可选、学生信息管理、班级信息管理、课程信息管理、成绩录入、成绩修改删除、成绩查询与排名。其中最容易做砸的是“查询与排名”这一块因为课堂上教的 SQL 通常停留在SELECT * FROM student一到写排名统计就手足无措。这就排出了第一个技术考点SQL 的聚合函数和排序写法。动手画设计图时我一般会把用例图和数据流图分开导出用例图放进需求分析章节数据流图放进系统设计章节。ProcessOn 导出的系统图可以直接放到文档里使用导出成高分辨率 PNG 后插入 Word 或 Markdown 皆可注意不要截图后直接粘贴会自动丢失细节且图片发虚。2.2 技术选型JDBC MySQL 是最稳的课设组合选型要回答两个问题用什么存储用什么连接。存储层面课程设计严格禁止用文件存储。用文本文件存成绩查询、修改、并发控制都要自己写一棵树长歪在半路后面根本扶不回来。关系型数据库是标准答案MySQL 8.0 是常见选择如果机器上装不上 MySQL改用内嵌的 H2 数据库也能兼容 JDBC 接口考试演示时还能避免被人拷问“你怎么保证数据不丢”。连接层面JDBC 是必须掌握的底层路径。很多学生直接上手 MyBatis、Spring Data JPA课程答辩时被问一句“底层是怎么查的”就答不上来这是最得不偿失的。我的建议是先手写 JDBC跑通之后再用第三方 ORM。这一篇内的代码示例全部基于 JDBC方便在答辩时边讲解边演示。JDK 版本选 8 或 17 都行。JDK 8 稳定兼容老机器JDK 17 是 LTS 版本现在不少学校上机的环境已经换到 17写代码时只要不使用var这类新语法两边编译都不报错。模块核心功能涉及技术常见错误学生管理新增、修改、删除、查询学生信息PreparedStatement、ResultSet删除学生时忘记连带删除成绩课程管理课程基本信息的增删改查CRUD 模板代码没有处理课程编号重复成绩录入按学生和课程录入分数INSERT 外键校验分数超 0-100 范围未校验成绩统计平均分、排名、分段统计GROUP BY、子查询、排序并列名次处理不统一用户登录三种角色进入不同操作界面角色字段 条件判断不做权限判断所有人能删数据常见误区是把五个模块当成五个独立页面来做实际上它们共享同一套数据库连接和相同的校验逻辑代码组织在业务层统一处理界面只是薄薄的一层壳。2.3 建表语句里的三张核心表建表的顺序必须固定先建学生表、课程表最后建成绩表因为成绩表引用了前两者的主键。课程设计里最常见的数据库设计错误是把班级作为字符串直接写进学生表一旦班级改名要全表扫描修改。正确的做法是建classes班级表再用外键关联但考虑到课设篇幅这里把班级字段直接冗余到学生表代码讲解时注明边界即可。CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 学号, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号业务编号, name VARCHAR(50) NOT NULL COMMENT 姓名, class_name VARCHAR(50) COMMENT 班级, gender CHAR(1) DEFAULT 男 COMMENT 性别, birthday DATE COMMENT 出生日期, enroll_date DATE COMMENT 入学日期 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, student_id INT NOT NULL COMMENT 关联学生表主键, course_id INT NOT NULL COMMENT 关联课程表主键, score DECIMAL(5,2) COMMENT 成绩0-100, exam_date DATE COMMENT 考试日期, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;三个关键参数说明成绩字段使用DECIMAL(5,2)允许存储 999.99范围足够且不丢失小数位。UNIQUE KEY uk_student_course这个联合唯一约束保证了同一个学生在同一门课只有一条成绩记录这是业务上“成绩唯一”的硬性前提代码里再校验一次只是双保险。外键约束. 注意三张表都必须设置 InnoDB 引擎和utf8mb4字符集前者保证外键和事务生效后者保证中文姓名写入不乱码。提示MySQL 8.0 以上默认引擎就是 InnoDB但课设验收的机器上可能跑着 5.7显式声明 ENGINE 不会出问题反而更稳妥。3. 三层结构手写 JDBC从最基础的查询写到可复用的 DAO3.1 为什么必须写三层而不是一个类干到底先看反面素材。有学生把全部逻辑写在main方法里数据库连接写在main开头查询写在中段界面打印写在末尾。功能能跑但答辩老师让“给系统加一个导出 Excel 功能”时他需要把整个 main 方法从头理一遍改完还有一半地方报错说变量找不到。这是耦合度太高造成的后果也是 Java 课程设计中必须避免的结构性缺陷。标准做法是三层结构界面层只负责接收输入和显示结果业务逻辑层做数据校验和统计计算数据访问层DAO负责与数据库打交道。这样分层的直接好处是把数据库从 MySQL 换成 H2只需要改 DAO 实现类界面层一行不动。Java 学生成绩管理系统课程设计规模不大三层各放一个包即可不需要引入重量级框架。3.2 写一个连接工具类把重复代码收干净每个 DAO 方法里都要获取连接、关闭连接重复代码一旦超过三处就值得提取。JDBC 连接的获取步骤在 Java 里很固定加载驱动、用DriverManager拿连接。在 Java 9 之后Class.forName(com.mysql.cj.jdbc.Driver)这一步其实可以省略但保留着不会报错课设代码里保留反而方便讲解驱动加载机制。import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逐个参数说明URL 里的student_db是数据库名必须先创建useSSLfalse关闭 SSL 加密本地开发时能减少一层握手时间serverTimezoneAsia/Shanghai指定时区不加这段 MySQL 8 会抛Server time zone异常characterEncodingutf8保证中文写入不乱码。密码建议放在db.properties配置文件里用Properties类读取答辩时可以解释“配置与代码分离”的设计思想。3.3 成绩查询使用 PreparedStatement 防止 SQL 注入成绩查询是系统里最常被调用的方法至少要支持两种查询方式按学生查所有成绩按课程查所有成绩。参数是从界面层传进来的用户输入的内容绝不能直接拼接到 SQL 字符串里这就是PreparedStatement存在的意义。拼 SQL 在课设演示时看不出问题但在“Java 面试题”里几乎必问练习时养成好习惯可以少走弯路。import java.sql.*; import java.util.ArrayList; import java.util.List; public class ScoreDao { public ListString findScoresByStudent(int studentId) { String sql SELECT c.course_name, s.score FROM score s JOIN course c ON s.course_id c.id WHERE s.student_id ? ORDER BY c.course_name; ListString result new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, studentId); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { String line rs.getString(course_name) : rs.getDouble(score); result.add(line); } } } catch (SQLException e) { e.printStackTrace(); } return result; } }这段代码有四个讲究。第一try-with-resources会自动关闭Connection、PreparedStatement、ResultSet不需要手写finally块这是 Java 7 以后的推荐写法也能避免连接泄漏。第二JOIN course联表查询直接拿到课程名不需要在 Java 代码里做二次查询。第三?占位符配合ps.setInt(1, studentId)参数从外部传入数据库驱动会对内容做转义。第四返回类型用ListString是简化处理实际项目应当返回ListScoreVO把课程名和分数封装成对象。提示如果出现Connection is closed异常先检查是不是在 try 块执行完毕后还拿这个连接去查询。try-with-resources在块结束自动关闭连接连接生命周期的边界必须清晰。3.4 成绩录入事务保证成绩表不出现半截数据录入成绩不只是执行一个 INSERT。一个学生的成绩可能有多个科目界面往往采用表格批量录入的方式一次提交涉及对多门课程的更新。假设要处理“把 5 门成绩一起保存”的操作前 3 门插入成功、第 4 门插入失败数据库里就残留了半截数据排名统计立刻错乱。解决办法是开启事务要么全部成功要么全部回滚。public boolean saveScores(int studentId, MapInteger, Double courseScoreMap) { String sql INSERT INTO score (student_id, course_id, score, exam_date) VALUES (?, ?, ?, NOW()) ON DUPLICATE KEY UPDATE score VALUES(score); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps conn.prepareStatement(sql)) { for (Map.EntryInteger, Double entry : courseScoreMap.entrySet()) { ps.setInt(1, studentId); ps.setInt(2, entry.getKey()); ps.setDouble(3, entry.getValue()); ps.addBatch(); } ps.executeBatch(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } finally { conn.setAutoCommit(true); } } catch (SQLException e) { e.printStackTrace(); return false; } }这里的关键是setAutoCommit(false)它把连接的默认提交行为关掉之后每个 SQL 执行后不会立即写库直到调用commit()才统一生效。addBatch()把多条 insert 放到一个批次里执行性能上优于循环单条执行这也是面试中可能会被追问的点——executeBatch与单条executeUpdate的区别。ON DUPLICATE KEY UPDATE处理了重复录入的场景同一个学生同一门课成绩已存在时自动更新而不是报错配合表里的uk_student_course唯一键确保成绩的唯一性。事务的边界要和业务边界保持一致。单条成绩录入不需要事务插入失败直接抛异常就好批量录入、批量删除才需要事务。把setAutoCommit(true)放在 finally 里恢复原状避免连接归还连接池时处于非自动提交状态这是资深开发才会注意的隐患。3.5 删除学生时先处理成绩表的外键学生退学或录入错误时管理员要删除学生信息。如果直接执行DELETE FROM student WHERE id ?MySQL 会报外键约束错误因为成绩表里还有记录引用这个学生ID。有两个方案先用DELETE FROM score WHERE student_id ?删除成绩再删学生或者建表时加上ON DELETE CASCADE。课设里我建议用前一种方案把删除逻辑写在同一个事务里代码路径清晰答辩时也容易解释“为什么要保证数据一致性”。public boolean deleteStudent(int studentId) { String deleteScore DELETE FROM score WHERE student_id ?; String deleteStudent DELETE FROM student WHERE id ?; try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(deleteScore); PreparedStatement ps2 conn.prepareStatement(deleteStudent)) { ps1.setInt(1, studentId); ps1.executeUpdate(); ps2.setInt(1, studentId); ps2.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } finally { conn.setAutoCommit(true); } } catch (SQLException e) { e.printStackTrace(); return false; } }这个例子里两个PreparedStatement在同一个 try-with-resources 块中声明用分号分隔资源关闭顺序是逆序先关闭 ps2再关闭 ps1最后关闭 conn。如果先删学生表再删成绩表外键约束立即报错回滚顺序不能调换。另一个要注意点是删除操作在数据库里是不可逆的界面层应当弹出确认框DAO 层也可以增加一个软删除字段deleted来保留历史数据具体取决于题目要求。4. 成绩排名与多字段排序Java 排序是面试现场的标准考题4.1 SQL 排名语句CROSS JOIN 求总分再排名查总分排名是成绩管理系统里的高配功能。基本思路分两步先用GROUP BY按学生分组求总成绩再把所有人按总分排序。想知道某个学生的名次需要统计总分比他高的人数有几个再加一就是他自己的名次。SELECT s.name, SUM(sc.score) AS total FROM student s JOIN score sc ON s.id sc.student_id GROUP BY s.id, s.name ORDER BY total DESC;这是求所有人总分排名的语句。若要求某一个人的排名标准写法是子查询配合CROSS JOINSELECT t1.name, t1.total, (SELECT COUNT(*) 1 FROM (SELECT SUM(score) AS total FROM score GROUP BY student_id) t2 WHERE t2.total t1.total) AS rank FROM (SELECT s.id, s.name, SUM(sc.score) AS total FROM student s JOIN score sc ON s.id sc.student_id GROUP BY s.id, s.name) t1 ORDER BY t1.total DESC;COUNT(*)1 的含义是统计总分高于当前学生的人数加上 1 就是当前名次。这种写法能正确处理并列排名——两个学生总分相同名次也相同且不影响后续名次的计数逻辑。如果课程设计要求“并列名次则序号相同”用DENSE_RANK()窗口函数更简洁但 MySQL 8.0 以下版本不支持窗口函数课设环境不确定时子查询方案兼容性最高。4.2 Java 端排序Comparator 才是性能瓶颈的隐藏考点成绩排名在数据量小时用 SQL 排序就够但课程设计里常常遇到一个需求界面支持点击表头切换排序按总分排、按平均分排、按学号排。每点击一次就重新查一遍数据库性能不可控学生在列表页反复点击还会把数据库连接池打爆。常见做法是一次查出所有记录放入List之后在内存里用Collections.sort反复排序。这也是 Java 面试八股文里出镜率极高的考点Comparator的用法。ListStudentScore list scoreService.getScoreSummary(); // 点击“平均分”表头后按平均分降序排列 list.sort(Comparator .comparing(StudentScore::getAverage) .reversed() .thenComparing(StudentScore::getStudentNo));一眼能看懂comparing按哪个字段排.reversed()变成降序.thenComparing处理平均分相同时按学号排。需要按不同字段动态排序时把比较器放入一个MapString, ComparatorStudentScore根据点击的表头取对应的比较器扩展性比if-else判断字段名好得多。给零基础同学的提醒是把Double包装类和double基本类型区分开comparing接收的是ToIntFunction这类函数式接口自动拆箱可能产生NullPointerException空值处理要在 comparator 里显式给出。ComparatorStudentScore oneKeyComparator (a, b) - { int cmp Double.compare(b.getAverage(), a.getAverage()); if (cmp 0) { return a.getStudentNo().compareTo(b.getStudentNo()); } return cmp; };手动写比较的逻辑适用于“排序规则复杂”的场景比如先按总分降序再按语文成绩降序再按学号升序。Comparator 的三个约定——自反性传递性一致性——理解后对调试排序问题大有帮助面试官大概率会根据这段代码追问TreeMap与HashMap排序字段的差异。4.3 equals 与 hashCode成绩对象放入 Set 去重的前提排名和查询功能会频繁对成绩对象做比较与去重。比如统计“有多少个学生参加过考试”如果直接new两个内容相同的成绩对象默认的比较判定它们不相等因为默认的equals是地址比较。要把“学号课程号唯一”作为业务等价的依据必须重写equals和hashCode。IDE 可以自动生成这两个方法但必须理解为什么hashCode要与equals保持一致。Override public boolean equals(Object o) { if (this o) return true; if (o null || getClass() ! o.getClass()) return false; StudentScore that (StudentScore) o; return studentId that.studentId courseId that.courseId; } Override public int hashCode() { return Objects.hash(studentId, courseId); }Java 泛型集合的底层依赖 hashCode 定位存储桶再通过 equals 精确比较。写了equals不写hashCode两个内容相同的对象可能被放进不同桶HashSet 去重失效。课程设计里最容易掉进去的坑是equals里的字段与hashCode里的字段不一致导致返回值互相矛盾。建议两者都用主键字段来生成就是上面这段代码的逻辑。这个知识点在 Java 基础面试题中出现频繁做课设时用实际代码验证一遍比死记答案牢靠得多。涉及“服务器公网怎么访问”一类的运行时环境问题与排序无关放到第 6 章说明。5. 用 AWT 还是 Swing把管理系统界面做出来并解决卡顿5.1 界面方案选型控制台版能过审Swing 版更完整课程设计答辩现场十组里有八组做的是控制台界面输入学号、打印成绩胜在实现简单、逻辑透明缺点是让非技术人员比如答辩评委看不出“管理系统”的形状。如果用 Swing 做一个带按钮、表格、菜单栏的桌面界面整体观感会提升一个档次工作量增加有限数据处理逻辑完全不改。我这里给出的是桌面图形界面的标准做法不用控制台因为控制台输入的阻塞式交互在课程设计中不讨喜。Swing 与 JavaFX 的选择上默认选 Swing。JavaFX 在 JDK 8 后从标准库中移出需要单独引入依赖在部分学校的演示机器上配置成本高Swing 零依赖编译即用能减少现场翻车概率。5.2 JTable 展示成绩列表的最小代码JTable 是 Swing 里展示表格数据的核心组件它支持表头点击排序、单元格选中、滚动条等基础交互。关键点在于使用DefaultTableModel管理数据刷新列表时只更新TableModelSwing 会自动重绘界面不需要手动清空控件再逐行添加。import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.util.List; public class ScoreTablePanel extends JPanel { private JTable table; private DefaultTableModel model; private ScoreDao scoreDao new ScoreDao(); public ScoreTablePanel() { String[] columns {学号, 姓名, 课程, 成绩}; model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; table new JTable(model); table.setRowHeight(28); this.add(new JScrollPane(table)); loadData(); } private void loadData() { model.setRowCount(0); ListStudentScore list scoreDao.getAllScores(); for (StudentScore sc : list) { Object[] row {sc.getStudentNo(), sc.getName(), sc.getCourseName(), sc.getScore()}; model.addRow(row); } } }isCellEditable返回 false让表格变为只读防止用户直接在界面上修改数据库记录这是一层最基础的权限防线。model.setRowCount(0)先清空所有行再循环加新数据是刷新列表时的标准动作。忘记这行会反复追加行成绩被显示两遍这是初学者最爱犯的错误。5.3 Swing 卡顿的真相事件分发线程EDT阻塞回到文章开头那个卡死的问题。Swing 是单线程模型所有界面刷新必须在事件分发线程EDT上执行。点击查询按钮后JButton的事件处理方法会在 EDT 上同步执行你在里面做了数据库查询这个线程被阻塞界面自然卡住不动。数据库查询快的时候问题不明显成绩数据一多、网络一波动界面会直接冻结操作系统提示“程序无响应”。解决方案是引入 SwingWorker 做异步查询把数据库查询放到工作线程查询完成后再回到 EDT 更新界面。class QueryWorker extends SwingWorkerListStudentScore, Void { private String keyword; QueryWorker(String keyword) { this.keyword keyword; } Override protected ListStudentScore doInBackground() throws Exception { return scoreDao.searchByKeyword(keyword); } Override protected void done() { try { ListStudentScore data get(); model.setRowCount(0); for (StudentScore sc : data) { model.addRow(new Object[]{sc.getStudentNo(), sc.getName(), sc.getCourseName(), sc.getScore()}); } } catch (Exception e) { JOptionPane.showMessageDialog(null, 查询失败: e.getMessage()); } } }doInBackground在后台工作线程中执行负责耗时的数据库访问done方法自动回到 EDT负责安全地更新界面。理解这个分工就能举一反三任何耗时的数据库操作、文件读写、网络请求都不应该直接出现在按钮事件处理中。课程设计里还会被问到的一个话题是“如果用户的查询特别慢怎么办”在doInBackground之外再加一个加载动画或者禁用按钮防止重复点击就能让系统观感再上一个档次。6. 验收前夜的三件事打包、乱码与连接泄漏6.1 用 Maven 打出可运行的 jar 包并验证课程设计提交的不只是源代码还要有一份能直接运行的程序。用 IDE 直接点击运行可以演示但换到答辩机器上没有 JDK 环境就彻底瘫痪。更稳的做法是打出一个可执行的 fat jar运行时只需要目标机器有 JRE 或 JDK。在项目的pom.xml中配置打包插件让 Maven 把项目依赖一并打包进一个 jar。核心是maven-assembly-plugin的assembly目标指定主类。打出 jar 后打开命令行执行验证mvn clean package java -jar target/student-score-system.jar运行不报错后需要确认标准输出没有乱码。默认 Maven 打包使用 UTF-8Windows 平台的命令行默认编码是 GBK运行时中文可能会显示乱码处理方式是在启动命令指定文件编码参数java -Dfile.encodingUTF-8 -jar target/student-score-system.jar6.2 端口占用与数据库版本差异Swing 课设一般不用端口但答辩现场有时会连 Web 版本一起演示。如果是做 Web 的学生成绩管理系统设计与实现Spring Boot 默认端口 8080多人共用一个台式机演示端口被占用是高频事故。换一个不常用端口往往能避开检查改动方法是在配置文件中指定server.port8082数据库版本差异也是高发问题。MySQL 5.7 与 MySQL 8.0 的 JDBC 驱动类名不同5.7 用的是com.mysql.jdbc.Driver8.0 用的是com.mysql.cj.jdbc.Driver。一旦报 “ClassNotFoundException”第一件事不是翻驱动包而是确认服务器上 MySQL 的版本。另一种表现是时区错误如果使用旧版本连接 MySQL 8报 “The server time zone value” 错误同样按第 2 章的 URL 参数添加时区即可。6.3 用 JConsole 检查连接泄漏连接泄漏是课程设计中最隐蔽的错误。getConnection()每次调用都会创建一条物理连接如果代码中连接没有关闭到达数据库上限后系统整体无响应。错误代码如下// 错误写法conn 未使用 try-with-resources异常时连接不被回收 Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery();这条代码在正常运行路径上最后关闭了连接。可一旦中间某一步抛出异常后面的关闭代码根本不执行。修改后的写法是文章第 3 章的 try-with-resources哪怕异常抛到方法外finally 块也会自动关闭。验证连接是否泄漏JDK 自带 JConsole 工具在 bin 目录下执行启动连接上 Java 进程后查看 MBean 的数据库连接数反复执行查询操作若连接数只增不减基本可以确定泄漏位置。翻转一下视角查询执行完但ResultSet未读取也会保持连接打开这是最容易忽略的边界情况——先用next()扫描完整个结果集再关闭连接两者顺序不能颠倒。本文还有配套的精品资源点击获取