☰
Java Swing+MySQL学生选课系统:JDBC事务与数据库设计实战
2026/10/8 6:11:09 网站建设 项目流程

简介:基于Java Swing与MySQL实现的学生选课管理系统课程设计资源,面向Java初学与课程设计人群,提供一套可直接运行、复用的完整项目方案。系统囊括系统管理、数据管理、数据查询三大核心模块,覆盖用户登录与密码设置、学生/课程/选课信息的新增删除修改、以及多维查询等典型功能,运行于Eclipse/IDEA、JDK8、MySQL5.7以上环境,功能链路完整,适合理解Swing界面开发与MySQL数据库交互。资源包共3个文件,总大小3.34MB,包含项目源码压缩包、数据库初始化SQL脚本及一份课程设计实训报告(docx),按“源码—数据—报告”组织,结构清晰,便于对照使用。已有268人学习下载,读者可借助完整源码快速部署调试,参考报告梳理论文框架与设计思路,高效完成课程作业或毕业设计。

1. 学生选课管理系统:Java Swing 与 MySQL 的典型落地组合

如果你在学校或培训机构待过,大概率见过这种场景:教务老师还在用 Excel 维护几百人的选课名单,学生要发邮件或者跑到办公室才能改选一次课。这套基于 Java Swing + MySQL 的学生选课管理系统,目标就是把“课程信息维护、学生选课、退课、成绩录入、统计查询”这几件高频事收敛到一个桌面程序里。它是 Java 课程设计里最经典的题目之一,难点不在业务逻辑,而在你把 Swing 界面事件、JDBC 事务、数据库表关系这三样东西缝在一起时,是否足够稳。

这套方案适合两类人:一是准备做毕业设计或课程设计的学生,需要一份能跑通、能答辩、有实训报告支撑的完整项目;二是刚学完 Java SE,想找一个“不依赖 Web 容器、双击就能跑”的桌面项目来锻炼 JDBC 和 GUI 编程的开发者。它的价值在于,选课系统天然包含一对多、多对多的表关系,你能在一百来行 SQL 里把外键约束、级联删除、事务回滚这些概念全部实战一遍,比刷十道练习题都管用。我对这套组合的评价是:技术栈虽然不算新,但它稳定、可控、容易调试,作为一个练手和交付物都合适的项目,依然是当下课程设计里最稳妥的选择之一。

2. 技术选型为什么是 Swing + MySQL:桌面端 JDBC 项目的经典组合

2.1 Swing 和 JavaFX 的选择:稳定优先于新潮

很多人在做桌面选课系统时会纠结:用 Swing 还是 JavaFX?我的经验是,除非你有明确的动画、图表或富媒体需求,不然 Swing 依然是课程设计里更安全的选择。原因有三点:第一,Swing 是 JDK 自带的 GUI 工具包,不需要额外引入运行时和构建插件,放在普通 JDK 环境里就能编译运行;第二,Swing 的资料沉淀极多,遇到 “JTable 刷新不生效”“JFrame 关闭后进程不退出”这类问题,随便搜一下就有现成答案;第三,Swing 的组件模型足够完成选课系统里所有的需求,包括表格展示课程列表、下拉框选择院系、文本域录入反馈意见。

JavaFX 虽然界面更现代,但它需要独立 SDK、可能在 IDEA 里需要额外配置 VM 参数,这对一个以“快速跑通、顺利答辩”为目标的实训项目来说是多余的复杂度。你交的是“学生选课管理系统”,不是“交互动效展示平台”,Swing 的平庸恰恰是它的优势。我现在做这类桌面工具时,一般先用 Swing 做原型,如果后续确实需要更精致的界面,再考虑迁移到 JavaFX 或者干脆改成 Web 版,但那是后话了。

2.2 MySQL 5.7 还是 8.0:先看你手头的环境

数据库方面,这个项目用 MySQL 是标配。你需要先做一个决定:用 MySQL 5.7 还是 8.0。我建议你在自己机器上装哪个方便用哪个,但要注意驱动版本。如果你用 8.0,JDBC 驱动类名是com.mysql.cj.jdbc.Driver,URL 需要加serverTimezone=Asia/Shanghai,否则连数据库时会报时区错误。如果你用 5.7,驱动类名是com.mysql.jdbc.Driver,URL 则不需要带时区参数,但两者在代码里混用会导致 ClassNotFoundException 或者奇怪的通信异常。

另一个常见坑是 MySQL 8.0 的加密规则。如果你在连接时遇到 “Public Key Retrieval is not allowed” 的错误,在 JDBC URL 后追加allowPublicKeyRetrieval=true&useSSL=false就能解决。这个参数不写在驱动代码里,而是写在连接字符串里。下面是一个兼容 MySQL 5.7 和 8.0 的 JDBC 配置模板,我一般会写在db.properties文件里,方便以后修改:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/course_select?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true jdbc.username=root jdbc.password=123456

这段配置里,characterEncoding=utf8是为了避免中文乱码;serverTimezone=Asia/Shanghai只对 MySQL 8.0 有效;useSSL=false是因为本地开发不需要加密连接,能省去证书配置的麻烦。注意allowPublicKeyRetrieval=true这个参数,如果你用的驱动是 8.0 且 MySQL 用户秘钥是 caching_sha2_password,不加这个会在第一次连接时报错。

2.3 IDEA 里的项目结构:一个 Module 就够,别搞多 Module

这个项目我建议用 IDEA 新建一个普通的 Java 项目,不要用 Maven,也不要用 Gradle,除非你后续打算把项目转成 Web 服务。原因还是在“可控”两个字上:不引入构建工具,你只需要把 mysql-connector.jar 导入到项目的 lib 目录,然后在 Project Structure 里把它添加为 Library 即可。如果用 Maven,虽然依赖管理方便,但很多新手会在坐标版本、私服配置上浪费时间,这些都不是选课系统本身的关注点。

IDEA 里建议的项目结构是:

course-selection-system/ ├── src/ │ ├── com.course.ui/ # Swing 界面类 │ │ ├── LoginFrame.java │ │ ├── StudentFrame.java │ │ └── AdminFrame.java │ ├── com.course.dao/ # JDBC 数据访问层 │ │ ├── StudentDao.java │ │ ├── CourseDao.java │ │ └── SelectionDao.java │ ├── com.course.model/ # 实体类 │ │ ├── Student.java │ │ ├── Course.java │ │ └── Selection.java │ ├── com.course.util/ # 工具类 │ │ ├── DBUtil.java │ │ └── TableModelUtil.java │ └── com.course.MainApp.java ├── lib/ │ └── mysql-connector-java-8.0.30.jar └── sql/ └── init.sql

这里我把MainApp.java放到com.course包下,作为程序入口,只负责调用LoginFrame。不要把 main 方法写在某个界面类里,否则后续做单元测试和代码审查都会别扭。另外我建议把init.sql放在项目根目录下的sql文件夹中,这样实训报告里可以直接引用建表语句,答辩时也方便演示。

3. 数据库设计与初始化:从三张表到两个核心事务

3.1 表结构设计:学生表、课程表、选课表,再多一张就是冗余

选课系统的核心数据模型是三张表:学生表(student)、课程表(course)、选课关系表(selection)。其中选课表是连接前两张表的多对多关系表,它只存student_id和course_id两个外键,以及选课时间、成绩这两个业务字段。为什么要单独拆出来而不是在学生表里加一个course_ids字段?因为关系型数据库的设计原则是“一行只表达一个事实”,你在学生表里塞一个逗号分隔的课程 ID 列表,后续统计“每个课程选了多少人”的时候就只能靠字符串分割,既慢又容易出错,这种反模式在面试里也是典型的扣分项。

下面是初始化 SQL,我在课程设计项目里惯用的写法是这样。注意我刻意用了ENGINE=InnoDB和CHARSET=utf8mb4,并打开了外键约束:

-- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学号主键', student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号,唯一索引', name VARCHAR(30) NOT NULL COMMENT '学生姓名', password VARCHAR(64) DEFAULT '123456' COMMENT '登录密码,默认123456', major VARCHAR(50) COMMENT '专业', class_name VARCHAR(50) COMMENT '班级', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生表'; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '课程主键', course_no VARCHAR(20) NOT NULL UNIQUE COMMENT '课程编号', name VARCHAR(50) NOT NULL COMMENT '课程名称', teacher VARCHAR(30) COMMENT '授课教师', credit DECIMAL(3,1) DEFAULT 2.0 COMMENT '学分', max_students INT DEFAULT 50 COMMENT '选课人数上限', selected_count INT DEFAULT 0 COMMENT '已选人数,可在应用层维护' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 选课关系表 CREATE TABLE selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,2) COMMENT '成绩,教务录入', UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_sel_student FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, CONSTRAINT fk_sel_course FOREIGN KEY (course_id) REFERENCES course(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='选课关系表';

这里说明几个设计决策:第一,student_no用UNIQUE而不是直接做主键,是为了让主键id在后续关联时保持稳定,学号即使被教务处调整也不影响已有选课关系;第二,course表里的selected_count字段是冗余设计,主要是为了方便在课程列表界面直接展示已选人数,不用每展示一行课程就COUNT(*)一次,这个字段可以通过事务保证一致;第三,selection表上的UNIQUE KEY uk_student_course是防重复选课的关键,应用层就算代码有并发问题,数据库这一层也会拦住重复插入。

3.2 初始化数据与事务演示:把实训报告的案例测出来

建表之后,建议你顺手插入几条演示数据,方便后面测试登录和选课功能。这个数据集不要太大,一两百个学生、二三十门课足够。插入时注意学生密码我用的是'123456',真实环境里应该加密存储,但作为课程设计演示,明文密码在实训报告里更容易讲清楚认证流程。

下面这段 SQL 用于插入 5 个学生和 5 门课程,并且预置一条选课记录,用来验证登录后的界面能正确显示“我已选的课程”:

INSERT INTO student (student_no, name, major, class_name) VALUES ('2023001', '张三', '计算机科学与技术', '计科2301'), ('2023002', '李四', '软件工程', '软工2302'), ('2023003', '王五', '数据科学', '数据2301'), ('2023004', '赵六', '计算机科学与技术', '计科2302'), ('2023005', '钱七', '网络工程', '网工2301'); INSERT INTO course (course_no, name, teacher, credit, max_students, selected_count) VALUES ('C001', 'Java程序设计', '陈老师', 3.0, 50, 0), ('C002', '数据库原理', '刘老师', 3.5, 40, 1), ('C003', '操作系统', '李老师', 3.0, 45, 0), ('C004', '计算机网络', '王老师', 2.5, 35, 0), ('C005', '软件工程', '赵老师', 2.0, 60, 0); -- 预置一条选课记录:张三选了数据库原理 INSERT INTO selection (student_id, course_id) VALUES (1, 2);

注意这里选课记录插入后,course表里C002的selected_count也应该同步更新为 1。这个同步动作在真实代码里应该在同一个事务里完成。SQL 文件里如果你手动跑这两条语句,需要记得把selected_count一起UPDATE掉,否则界面上的已选人数不准确,很容易在演示的时候被老师挑出毛病。

4. 用 JDBC 写一个永不串号的学生端选课逻辑:从连接池到事务

4.1 数据库连接工具类:别把连接参数散落在各处

写 Swing 界面时最容易出现的情况是,每个按钮事件里都直接写DriverManager.getConnection(url, user, password),结果换一台电脑跑项目时,要全局搜索替换数据库密码。这种做法在课程设计里不算致命,但会让你的代码在实训报告里显得很业余。我建议你写一个DBUtil工具类,把连接管理工作收口:

package com.course.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/course_select?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"; 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); } public static void close(Connection conn, java.sql.Statement stmt, java.sql.ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }

这个工具类里,Class.forName是在注册 JDBC 驱动,MySQL 8.0 的驱动在 SPI 机制下可以省略这行,但写上它能让驱动加载过程更明确,排查问题时也少一个变量。close方法里我把ResultSet、Statement、Connection的关系理清楚,关闭顺序必须从里到外,否则你可能会在日志里看到连接泄漏。这里我没有引入连接池,因为 Swing 桌面应用是单用户模式,并发量极低,每次操作新建连接、用完即关就够了。如果后续你把它改造成多用户并发访问,再考虑换成 HikariCP,但现在不要过度设计。

4.2 核心选课事务:先查容量,再插入,最后更新计数

下面是选课功能里最关键的一段代码。我把它写在SelectionDao.selectCourse(int studentId, int courseId)方法里,这个方法要承担两个职责:一是检查课程是否已满、学生是否已选过;二是完成插入选课记录并更新课程已选人数。整个过程必须在一个事务里执行,否则在“检查容量”和“插入记录”之间如果其他线程也执行了同样操作,就可能导致超选。

package com.course.dao; import com.course.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class SelectionDao { public boolean selectCourse(int studentId, int courseId) { String checkCourseSql = "SELECT max_students, selected_count FROM course WHERE id = ? FOR UPDATE"; String checkSelectedSql = "SELECT id FROM selection WHERE student_id = ? AND course_id = ?"; String insertSql = "INSERT INTO selection (student_id, course_id) VALUES (?, ?)"; String updateCountSql = "UPDATE course SET selected_count = selected_count + 1 WHERE id = ?"; Connection conn = null; PreparedStatement checkCourseStmt = null; PreparedStatement checkSelectedStmt = null; PreparedStatement insertStmt = null; PreparedStatement updateCountStmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); // 开启事务,关闭自动提交 conn.setAutoCommit(false); // 第一步:锁定课程行,防止并发超选 checkCourseStmt = conn.prepareStatement(checkCourseSql); checkCourseStmt.setInt(1, courseId); rs = checkCourseStmt.executeQuery(); if (!rs.next()) { conn.rollback(); return false; // 课程不存在 } int maxStudents = rs.getInt("max_students"); int selectedCount = rs.getInt("selected_count"); if (selectedCount >= maxStudents) { conn.rollback(); return false; // 课程已满 } // 第二步:检查是否重复选课 checkSelectedStmt = conn.prepareStatement(checkSelectedSql); checkSelectedStmt.setInt(1, studentId); checkSelectedStmt.setInt(2, courseId); rs = checkSelectedStmt.executeQuery(); if (rs.next()) { conn.rollback(); return false; // 已经选过这门课 } // 第三步:插入选课记录 insertStmt = conn.prepareStatement(insertSql); insertStmt.setInt(1, studentId); insertStmt.setInt(2, courseId); insertStmt.executeUpdate(); // 第四步:更新课程已选人数 updateCountStmt = conn.prepareStatement(updateCountSql); updateCountStmt.setInt(1, courseId); updateCountStmt.executeUpdate(); // 全部成功,提交事务 conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } e.printStackTrace(); return false; } finally { DBUtil.close(conn, insertStmt, rs); } } }

这段代码的逻辑说明如下。第一步里FOR UPDATE是关键,它会在事务期间锁住course表里那一行,其他事务要更新同一行时必须等当前事务提交或回滚。第二步的重复检查能拦截大部分重复插入,但由于应用层可能并发执行,数据库层的外键约束和UNIQUE索引仍然是最后的兜底防线。事务的提交或回滚都放在try-catch里,任何一个步骤失败都会把所有改动回滚,避免出现“选课记录插入成功,但课程已选人数没更新”的数据不一致。

这里有个参数值得你注意:setAutoCommit(false)必须在getConnection()之后、第一条 SQL 执行之前调用,如果放在finally里再改回去就没有意义了。另外,这个事务里rs变量在第一步和第二步之间重复使用,我每次执行完查询后就继续复用同一个ResultSet,其实这是有风险的,因为同一个Statement上复用ResultSet会关闭前一个结果集。好在这里第一步的rs在第二步执行前已经用完了,所以没有踩坑,但更规范的写法是为每一步查询单独准备Statement和ResultSet。这是我在实际开发中踩过的坑,所以特别提醒你。

4.3 Swing 事件线程:按钮点击之后,代码跑在哪里

Swing 是单线程模型,所有界面更新必须在事件分发线程(EDT)上执行。这里有个常见的做法是:在按钮的事件监听器里,直接调用SelectionDao.selectCourse(),然后根据返回值弹出JOptionPane提示框并刷新表格模型。这个做法对于课程设计来说没有问题,因为本地数据库查询很快,你不会感受到界面卡顿。但如果你把项目扩展到网络数据库或者数据量变大,就需要在SwingWorker里执行 JDBC 操作,避免 EDT 被数据库 IO 阻塞导致界面无响应。

下面这段代码是选课按钮的典型实现,我把刷新表格数据的逻辑一起写进去:

btnSelectCourse.addActionListener(e -> { int selectedRow = courseTable.getSelectedRow(); if (selectedRow < 0) { JOptionPane.showMessageDialog(this, "请先在表格中选择一门课程"); return; } // 从表格模型取课程 ID,注意这里是 model 的索引,不是 view 的索引 int courseId = (int) courseTableModel.getValueAt(selectedRow, 0); boolean success = new SelectionDao().selectCourse(currentStudentId, courseId); if (success) { JOptionPane.showMessageDialog(this, "选课成功"); loadMyCourses(); // 刷新“我的课程”表格 loadAllCourses(); // 刷新全部课程表格,更新已选人数 } else { JOptionPane.showMessageDialog(this, "选课失败:课程已满或已选过该课程"); } });

这里有一个培训时经常被忽略的细节:JTable的getSelectedRow()返回的是视图模型(view)的行索引,而表格数据我们存放在DefaultTableModel中,两者在表格经过排序或筛选后可能不一致。如果课程表格只是顺序展示,没有排序功能,那么 view 索引和 model 索引是一致的;但如果你加了TableRowSorter排序,就必须用table.convertRowIndexToModel(selectedRow)转换。我在做这个系统时加过排序功能,结果选课选错了课程,所以在这提醒你:拿到选中行以后,第一件事就是做索引转换,否则容易给学生选错课。

5. 避坑与排查:实训报告里最容易翻车的 5 个细节

5.1 中文乱码:不是数据库的锅,是连接参数没带对

现象:登录界面显示中文正常,但课程列表里所有中文都变成“?”。原因:数据库表已经是utf8mb4,但 JDBC 连接字符串里没有characterEncoding=utf8,导致客户端和服务器之间的字符集不一致。解决:在db.properties或DBUtil里把 URL 改成jdbc:mysql://localhost:3306/course_select?useUnicode=true&characterEncoding=utf8,同时确认 IDEA 里项目的文件编码是 UTF-8,不要用 GBK。另外,如果你发现INSERT语句传中文参数乱码,检查PreparedStatement的参数是否在 setString 之前就经过了正确编码,一般加连接参数后这个问题会一并消失。

5.2 mysql-connector 版本与 MySQL 服务器版本不匹配

现象:程序能编译,但运行到Class.forName("com.mysql.jdbc.Driver")时报 ClassNotFoundException;或者连接时报Communications link failure。原因:MySQL 8.0 的驱动类名改了,你用 5.1.x 驱动连 8.0 服务器会失败。解决:确认你的 lib 目录下 jar 包版本。如果是 8.x,驱动类名是com.mysql.cj.jdbc.Driver,并且 URL 里要带serverTimezone。我一般直接在 MySQL 官网下载mysql-connector-j-8.0.30.jar,然后导入 IDEA 的 Project Structure > Libraries。这个坑在实训报告里值得写一段,因为它是 Java 连接数据库最典型的升级兼容问题。

5.3 重复选课成功:应用层检查被绕过

现象:连续快速点击“选课”按钮两次,数据库里出现两条一样的选课记录。原因:并发时两个线程都通过了checkSelectedSql的检查,然后先后执行了插入。解决:不要只依赖应用层判断,数据库里的UNIQUE KEY uk_student_course (student_id, course_id)是最后防线。当插入第二条时,MySQL 会抛出DuplicateKeyException,你的catch (SQLException e)会捕获它并回滚。为了让实训报告更好看,我建议你在这个 catch 里判断e.getErrorCode() == 1062,然后提示“您已选过这门课”,而不是显示“选课失败”这种模糊文案。

5.4 Swing 界面卡死:JDBC 操作阻塞了事件分发线程

现象:点击“加载全部课程”按钮后,窗口变成白屏,光标转圈,几秒钟后才恢复。原因:MainApp直接调用 DAO 查询,数据库执行时间较长时阻塞了 EDT。解决:最简单的做法是在查询方法里加一个Thread.sleep(100)模拟慢查询,然后改用SwingWorker<Void, Void>在后台线程执行查询,在done()方法里更新表格模型。不过对于本地 MySQL 和几千条数据,不这样做也感觉不出来。我的建议是,实训报告里可以写这个方案,但代码里不必过度设计,只要答辩时能解释清楚 EDT 模型就行。

5.5 关闭主窗口后 Java 进程不退出

现象:点窗口右上角关闭按钮,界面消失了,但 IDEA 里程序还在运行,进度条一直是绿色。原因:没有设置setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE),默认是HIDE_ON_CLOSE,窗口隐藏了但 JVM 还在跑。解决:在JFrame构造函数里加this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);。这算是最基础的 Swing 常识,但几乎每届都有学生在这上面踩坑,因为光标点在窗口右上角时根本没意识到只是隐藏。也有同学用DISPOSE_ON_CLOSE配合System.exit(0),效果一样,但EXIT_ON_CLOSE是最直接的。

6. 把项目做成能答辩的样子:从报表导出到体检清单

项目到了收尾阶段,除了选课、退课这些核心功能外,教务管理员通常需要把选课名单导出来存档。一个实用的技巧是直接在 Swing 界面里用JFileChooser选择保存路径,然后用FileWriter把ResultSet的内容写成 CSV 文件。这个功能不需要引入第三方库,二十行代码就能完成,而且容易在答辩时演示。我只保留一个建议——先写表头,再写数据行,用英文逗号分隔,中文内容加双引号,这样用 Excel 打开时不容易乱。下面是一个简易的导出实现:

private void exportSelectionToCsv() { JFileChooser chooser = new JFileChooser("."); chooser.setDialogTitle("导出选课名单"); int result = chooser.showSaveDialog(this); if (result != JFileChooser.APPROVE_OPTION) { return; } File file = chooser.getSelectedFile(); if (!file.getName().endsWith(".csv")) { file = new File(file.getAbsolutePath() + ".csv"); } String sql = "SELECT s.student_no, s.name, s.major, c.name AS course_name, sc.select_time " + "FROM selection sc " + "JOIN student s ON sc.student_id = s.id " + "JOIN course c ON sc.course_id = c.id " + "ORDER BY c.id, s.student_no"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery(); BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file), "UTF-8"))) { writer.write("学号,姓名,专业,课程名称,选课时间\n"); while (rs.next()) { writer.write(String.format("\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", rs.getString("student_no"), rs.getString("name"), rs.getString("major"), rs.getString("course_name"), rs.getString("select_time"))); } JOptionPane.showMessageDialog(this, "导出成功:" + file.getAbsolutePath()); } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "导出失败:" + ex.getMessage()); } }

这里用try-with-resources把连接、语句、结果集和文件流都收口,是 Java 7 以后的推荐写法,即使有异常也会自动关闭资源。输出到 CSV 文件时用了OutputStreamWriter指定 UTF-8 编码,防止 Excel 打开乱码。注意String.format里的占位符顺序要和writer.write的拼接保持一致,一般我在开发时如果导出的表格列很多,会先在一张纸上画出表头顺序,再写代码,这样不容易出错。

答辩演示时,除了导出功能,我还会在实训报告里附上一份“项目运行环境清单”,包括 JDK 版本(推荐 8 或 11)、MySQL 版本(5.7 或 8.0)、IDEA 版本(2019.3 及以上均可)、mysql-connector-java-8.0.30.jar 以及数据库初始化脚本。这个清单看起来不起眼,但评委老师最常问的就是“你换一台电脑能跑起来吗”,你把这几个版本信息写清楚,能省掉不少追问。另外,我习惯给项目根目录配一个 README.txt,里面写清楚三步启动流程:先用 MySQL 执行sql/init.sql,再在 IDEA 导入项目,最后修改DBUtil.java里的数据库密码为本地密码。这样哪怕过了一个月再回来看这个项目,也能快速接手。

最后说一个我自己的教训:我在早期做这类课程设计时,总是忍不住往项目里堆功能,比如加一个“公告板”模块,加一个“课程评价”模块,结果数据库表越来越多,代码越来越乱,最后连自己都理不清外键关系。后来我带学生做项目时,都会先让他们把核心流程走顺——学生登录、选课、退课、管理员审核、统计报表,一个功能只能有一个界面、一个 DAO 方法以及一段事务逻辑,做完这些之后,其余的可选功能都只是锦上添花。Selbst当老师问起“你的项目有什么亮点”时,你不必说“界面特别好看”,你可以说“我的选课事务能保证数据一致”,这个说服力强得多。希望这些经验和代码片段能帮到你,祝你的选课管理系统一次通过答辩。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询