简介:基于Java + Swing + MySQL实现的学生选课及成绩管理系统完整源码,面向Java课程设计、毕业设计及工程实训场景,适合有一定基础但希望系统掌握桌面端开发流程的学习者。项目包含登录、选课、成绩录入与查询、学生信息管理等核心模块,并预置管理员、教师、学生三类账号,便于直接运行体验。压缩包共50个文件,其中16个Java源码文件与6个XML配置说明核心实现,20张JPG界面截图和2张PNG系统结构图辅助理解,另有SQL数据库脚本、可运行的MySQL驱动JAR包及说明文档,整体体积仅8.62MB,部署门槛低。已有81人学习下载。对照截图与源码可快速梳理Swing界面事件处理、JDBC数据库交互及三层结构组织方式,同时可复用其分类清晰的目录来扩展选课和成绩管理功能,是一份适合快速上手的完整课设参考。
1. 先把项目拆明白:Java + Swing + MySQL 的选课管理系统到底要做什么
每年课程设计选题的时候,“学生选课及成绩管理系统”基本是 Java 方向出现频率最高的题目之一。原因很直接:它能覆盖登录、增删改查、表关联、业务校验和界面交互,难度又刚好卡在“会 Java SE + 会用 JDBC + 会写基本 SQL”这条线上。用 Java + Swing 做桌面端界面,用 MySQL 存数据,中间用 JDBC 沟通,最后交付一个能跑通完整流程的选课成绩管理工具。适合 Java 基础已经过关、还没接触 Spring 的同学,也适合想快速拿出可演示项目的求职作品。
这个题目真正的价值不在 Swing 组件用得有多花哨,而在三层架构的拆分、数据库表的设计、选课和成绩录入涉及的事务处理。这些是以后做任何企业级 Web 项目都会用到的底层能力。接下来按“拆需求 → 建表 → 搭界面 → 写 DAO → 踩坑 → 升级”的顺序,把整个系统从零讲到能复现。
2. 系统设计与数据库建模:先把用户、课程、选课成绩四张表建起来
2.1 三种角色和六个模块:管理员、教师、学生分别能做什么
拿到题目先别急着写代码,第一步把角色和功能拆清楚。这套系统通常分三种角色:
- 管理员:管理课程信息,包括新增、修改、下架课程;重置教师和学生账号;查看选课统计。
- 教师:查看自己名下的课程列表,录入或修改学生成绩。
- 学生:浏览可选课程,选课、退课,查看自己的成绩和学分。
对应出来就是六个核心模块:登录模块、课程管理模块、选课模块、退课模块、成绩录入模块、成绩查询模块。如果追求完整度,还可以加一个成绩统计分析,比如按课程计算平均分、最高分、最低分。
功能拆分时有一个容易犯的错:把管理员的课程管理逻辑直接写在学生界面里。界面层要做权限控制,角色不同,菜单项和可见面板就不同。这个判断应该在登录成功后就走,而不是等用户点进某个功能再报错。
我的建议是,先画一个简单的功能清单表格,标清楚每个角色能看到哪些菜单。代码里用角色字符串去判断,这样后面做权限扩展时最省事。比如管理员进来显示“课程维护”按钮,学生进来显示“可选课程列表”和“我的成绩”。
2.2 表结构设计:为什么选课和成绩共用一张 t_sc 表
数据库表设计是这套系统最值得花时间的地方。网上不少课程设计喜欢拆出五张、六张表,把选课记录和成绩分开成两张,逻辑上没问题,但对 Swing + JDBC 这种项目来说反而增加 JOIN 成本。常见做法是选课记录和成绩放在同一张关联表里:学生选课时插入一条记录,grade 字段默认为 NULL,教师录入成绩时再 UPDATE 这个字段。一个字段同时表达“选没选”和“考了多少分”,简洁直接。
核心表四张就够:
- t_user:登录账号,包含 username、password、role 三个字段。
- t_student:学生档案,包含学号、姓名、性别、专业、班级。
- t_course:课程档案,包含课程编号、课程名、学分、任课教师、容量上限、已选人数。
- t_sc:选课成绩关联表,包含 student_id、course_id、grade。
t_user 和 t_student 分开是刻意的。登录只需要用户名密码和角色,如果强行和学生档案放在一张表,查询时会把不相关的字段也捞出来,逻辑混在一起。常见做法是 t_student 里加一个 user_id 外键指向 t_user。
2.3 建表 SQL:外键、唯一约束和默认数据一次到位
建表是一步到位的活,后面再改表结构会很痛苦。特别是字段编码,MySQL 8.0 安装教程里虽然默认字符集是 utf8mb4,但建库时显式指定更保险。另外,t_sc 表一定要加联合唯一约束,保证同一个学生不能重复选同一门课。选课容量靠 t_course 的 selected_num 字段配合 UPDATE 语句维护。
CREATE DATABASE course_system DEFAULT CHARACTER SET utf8mb4; USE course_system; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender VARCHAR(10), major VARCHAR(50), class_name VARCHAR(50), user_id INT, FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0, teacher VARCHAR(50), max_num INT NOT NULL DEFAULT 50, selected_num INT NOT NULL DEFAULT 0, semester VARCHAR(20) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_sc ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, grade DECIMAL(5,2) DEFAULT NULL, UNIQUE KEY uk_stu_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES t_student(id), FOREIGN KEY (course_id) REFERENCES t_course(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO t_user (username, password, role) VALUES ('admin', 'admin123', 'admin'), ('teacher1', '123456', 'teacher'), ('student1', '123456', 'student');这段 SQL 有几个关键点值得说明。t_sc 表不单独存课程名、学生名,只存 id 关联,这是第三范式的标准做法,避免冗余数据导致的不一致。UNIQUE KEY 的作用是数据库层面的最后一道防线,就算业务代码忘了判断重复,这条约束也能拦住。外键负责保证引用的学生和课程一定存在,防止插入脏数据。
另外一个实际问题是,如果老师要求必须能录入成绩,且一个学生重修同一门课,联合唯一约束会限制同一条选课记录只能有一个成绩。这种情况可以把表改成带 semester 字段或者去掉唯一约束,但作为课程设计,按学期限定选课已经很够了。建议默认按一个学期做。
3. Swing 界面层:登录窗口、主框架和 JTable 数据刷新怎么做
3.1 登录窗口:校验账号密码并按角色跳转到不同界面
Swing 写界面容易,写得顺手是另一回事。登录窗口是整个系统的入口,代码量不大,但涉及一个关键点:拿到用户角色后如何决定下一个窗口的内容。常见做法是登录成功之后直接 new 一个主窗口对象,把 User 对象传进去,主窗口构造方法里根据角色去构建不同的菜单和面板。
public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; private JButton loginBtn; public LoginFrame() { setTitle("学生选课及成绩管理系统 - 登录"); setSize(420, 300); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setResizable(false); JPanel panel = new JPanel(new GridLayout(3, 2, 10, 10)); panel.setBorder(BorderFactory.createEmptyBorder(30, 30, 30, 30)); panel.add(new JLabel("用户名:")); usernameField = new JTextField(); panel.add(usernameField); panel.add(new JLabel("密码:")); passwordField = new JPasswordField(); panel.add(passwordField); loginBtn = new JButton("登录"); panel.add(new JLabel()); panel.add(loginBtn); loginBtn.addActionListener(e -> doLogin()); setContentPane(panel); } private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { JOptionPane.showMessageDialog(this, "登录成功,当前角色: " + user.getRole()); new MainFrame(user).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "账号或密码错误"); } } }这里有两个细节值得展开。JPasswordField 调用 getPassword() 返回的是 char 数组,不是 String,这样做的原因是字符串不可变,char 数组用完可以手动清空,避免密码残留在内存里。另一个细节是 dispose() 释放登录窗口,避免关掉主窗口后登录窗口还留在任务栏。
密码校验我建议写在 DAO 层而不是把用户表整个查出来再比对。直接WHERE username = ? AND password = ?一条 SQL 返回结果,有记录就登录成功,没有就提示错误。
课程设计阶段,密码用明文存储可以接受,但如果你想在答辩时加分,可以在注册或初始化密码时用 MD5 加盐,或者用 Java 自带的 MessageDigest 做 SHA-256 摘要。数据库里永远不存明文密码,这是面试时经常被追问的点。
3.2 主框架:菜单栏 + CardLayout 切换内容面板
主窗口在 Swing 项目里通常用一个 JFrame 承载菜单栏、工具栏和内容区。内容区里多个页面之间切换,最顺手的方式是 CardLayout。它把每个功能面板当作一张卡牌叠在一起,通过字符串名字切换显示,和后面要做的权限控制天然配合。
public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel contentPanel; private User currentUser; public MainFrame(User user) { this.currentUser = user; setTitle("学生选课及成绩管理系统"); setSize(960, 600); setLocationRelativeTo(null); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); buildMenuBar(); cardLayout = new CardLayout(); contentPanel = new JPanel(cardLayout); getContentPane().add(contentPanel, BorderLayout.CENTER); if ("student".equals(user.getRole())) { addStudentPanels(); } else if ("teacher".equals(user.getRole())) { addTeacherPanels(); } else { addAdminPanels(); } cardLayout.show(contentPanel, "默认"); } private void buildMenuBar() { JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("系统"); JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> System.exit(0)); menu.add(exitItem); menuBar.add(menu); setJMenuBar(menuBar); } private void addStudentPanels() { JPanel coursePanel = new CourseListPanel(currentUser); JPanel scorePanel = new ScoreQueryPanel(currentUser); contentPanel.add(coursePanel, "course"); contentPanel.add(scorePanel, "score"); } }CardLayout 的 key 用字符串管理,切换时调用cardLayout.show(contentPanel, "course")即可。这里要注意,每个面板要设置好首选大小,或者统一交给 BorderLayout 拉伸,否则切过去之后组件可能面目全非。
实际项目里,菜单栏和内容面板的联动通常靠 ActionListener 完成。比如点击菜单项“我的成绩”,监听器里拿到 CardLayout 引用并调用 show 方法。不要在每个面板里自己去 new JFrame,那是新手最容易写出来的死胡同,弹窗套弹窗,关都关不完。
3.3 JTable 显示与刷新:TableModel 的两个常见误用
列表展示是 Swing 系统里最核心的交互,JTable 本身不直接存数据,数据放在 TableModel 里。很多课程设计翻车就翻在这里:数据库数据变了,界面上的 JTable 不会自动感知,必须重新 setModel 或者触发 TableModel 的更新事件。
private void refreshCourseTable() { List<Course> courseList = new CourseDao().findAll(); DefaultTableModel model = new DefaultTableModel( new String[]{"课程编号", "课程名称", "学分", "教师", "已选/容量"}, 0); for (Course c : courseList) { model.addRow(new Object[]{ c.getCourseNo(), c.getCourseName(), c.getCredit(), c.getTeacher(), c.getSelectedNum() + "/" + c.getMaxNum() }); } courseTable.setModel(model); }这段代码的目的是每次刷新时创建一个全新的 DefaultTableModel 对象并塞进 JTable。第一次点击按钮显示数据,第二次数据库里新增了课程,再点一次,数据也能正确显示。这种“全套替换”的方式代码简单,数据量不大时完全够用。
第二种常见误用是直接table.getModel()拿一个 DefaultTableModel,然后往上 addRow。这个思路没错,但如果 TableModel 在构造时指定列名和初始行数,反复 addRow 会导致行数不断叠加,只增不减。每刷新一次,数据翻倍。排查时看到表格行数越来越多,十有八九就是没有清理旧行。
刷新完表格后,JTable 的列宽也需要做一次调整。直接把table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF)关掉自动拉伸,再用table.getColumnModel().getColumn(i).setPreferredWidth(w)设置各列宽度,界面会整齐很多。如果不设置,课程名和教师名挤在一坨,演示时观感很差。
4. JDBC 数据访问层:连接封装、DAO 模板与选课事务
4.1 JDBC 连接封装:驱动加载和连接参数的写法
JDBC 是这套系统的毛细血管,连接写不好,后面所有功能都会跟着抽风。MySQL 8.0 及以上版本,驱动类名必须是com.mysql.cj.jdbc.Driver,老教程里常见的com.mysql.jdbc.Driver在 8.x 已经移除了,ClassNotFoundException 就是这么来的。
连接参数里还必须带 characterEncoding 和 serverTimezone,否则容易出现中文乱码和时区报错。我一般在项目里单独写一个 DbUtil 类,把连接参数集中管理,后面换连接池时只需要改这一个类。
public class DbUtil { private static final String URL = "jdbc:mysql://localhost:3306/course_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }Class.forName 在 JDBC 4.0 之后不是必须的,因为 SPI 机制会自动加载驱动,但写上也无妨,反而能让刚入门的人看明白驱动加载这一步存在。USER 和 PASSWORD 用常量写死是课程设计的常见做法,但如果是多人协作或者想提交到 Github,这里建议改成读配置文件,比如 db.properties。
连接串里的 useSSL=false 要留意。MySQL 8 默认开启 SSL,本地开发环境没有证书配置就会告警。加了这个参数虽然不解决安全问题,但能消掉本地环境下烦人的 SSL 告警。生产环境不能照抄,需要证书配置。
4.2 DAO 层典型方法:登录查询和课程列表怎么写
DAO 层推荐结构是每个表一个 DAO,比如 UserDao、CourseDao、StudentDao、ScoreDao。每个方法只做一件事,名字直白:findByUsername、findAllCourses、insertCourse、updateGrade。界面层不直接拼 SQL,这是分层的基本要求。
public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT id, username, role FROM t_user " + "WHERE username = ? AND password = ?"; try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRole(rs.getString("role")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }PreparedStatement 的使用有两个原因。第一是防 SQL 注入,字符串拼接的登录 SQL 在面试时基本必被问到;第二是预编译能复用执行计划,对循环批量操作提升明显。关于排序的需求,查询课程列表的时候一般会按创建时间或课程编号排,比如ORDER BY id ASC,MySQL 排序默认是升序,字段上用数字主键排比字符串更稳定。
关于 try-with-resources,这段代码值得注意:Connection、PreparedStatement、ResultSet 都实现了 AutoCloseable 接口,写在 try 的括号里,不管执行成功还是抛异常,都会自动关闭。这个语法从 Java 7 就有了,比我当年用 finally 手写 close 简洁得多,也安全得多。
4.3 选课和成绩录入:事务、锁与唯一约束在这里兜底
选课是这套系统里最需要严谨对待的业务。用户点击“选课”按钮,背后至少两步操作:第一步检查课程是否已满,第二步插入选课记录并更新已选人数。这两件事必须在一个事务里完成,否则可能出现“插入了记录但已选人数没变”的不一致状态。
public boolean selectCourse(int studentId, int courseId) { String checkSql = "SELECT max_num, selected_num FROM t_course " + "WHERE id = ? FOR UPDATE"; String insertSql = "INSERT INTO t_sc (student_id, course_id) " + "VALUES (?, ?)"; String updateSql = "UPDATE t_course SET selected_num = selected_num + 1 " + "WHERE id = ?"; try (Connection conn = DbUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement checkPs = conn.prepareStatement(checkSql)) { checkPs.setInt(1, courseId); try (ResultSet rs = checkPs.executeQuery()) { if (!rs.next()) { return false; } int max = rs.getInt("max_num"); int selected = rs.getInt("selected_num"); if (selected >= max) { return false; } } } try (PreparedStatement insertPs = conn.prepareStatement(insertSql)) { insertPs.setInt(1, studentId); insertPs.setInt(2, courseId); insertPs.executeUpdate(); } try (PreparedStatement updatePs = conn.prepareStatement(updateSql)) { updatePs.setInt(1, courseId); updatePs.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { // 回滚交给 try-with-resources 自动关闭连接时处理 try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement("SELECT 1")) { ps.executeQuery(); } catch (SQLException ignored) { } e.printStackTrace(); return false; } }这段代码的核心在FOR UPDATE。选课开始前先锁住课程行,防止两个学生同时读到“还有最后一个名额”然后同时插入。锁的粒度是行锁,InnoDB 引擎下才有效,MyISAM 不支持事务和行锁,这是 MySQL 各存储引擎的明显差异。
MySQL 事务处理的边界也要说清楚:连接必须把 autoCommit 先关掉,然后执行多条 SQL 后 commit。一旦中间抛出 SQLException,事务应该整体回滚。上面示例在 catch 块里没有显式调用 rollback 是因为连接关闭时未提交事务会自动回滚,但为了代码可读性,还是建议显式加上conn.rollback()。
成绩录入的逻辑比选课简单,本质上就是 UPDATE 语句。一个值得注意的细节:成绩字段允许 NULL 表示未录入,录入时先判断是否为 NULL,再决定 UPDATE 还是 INSERT。如果这门课选了但成绩表里没有记录,应该提示先检查选课关系是否存在,而不是直接报错。
5. 避坑记录:乱码、JTable 不刷新、连接泄漏和驱动冲突
5.1 中文乱码:连接串、MySQL 表编码和 IDEA 控制台三层排查
现象:Swing 界面上显示的中文全是问号,或者控制台打印出来的 SQL 中文变成了乱码。
原因:三层都可能出问题。第一层是数据库建库建表时字符集没指定,MySQL 默认排序规则和中文不兼容;第二层是 JDBC 连接串没带 characterEncoding;第三层是 IDEA 控制台自身的编码设置和项目文件编码不一致。
解决:建库用DEFAULT CHARACTER SET utf8mb4,连接串加?useUnicode=true&characterEncoding=utf8,IDEA 里把 Settings → Editor → File Encodings 的三处编码全部改成 UTF-8。如果还乱码,检查 MySQL 服务端的 character_set_server,命令行执行SHOW VARIABLES LIKE 'character_set%';。
这个坑我印象很深。当年界面是全中文的,数据库表也是中文数据,查询结果偏偏在 JTable 里显示成问号。最后发现是建库时没指定字符集,MySQL 默认落到 latin1,连接参数加得再全也没用。
5.2 JTable 数据不刷新:旧 TableModel 新数据永远是黑匣子
现象:数据库里的课程数据变了,JTable 上显示的还是旧数据,重新点查询按钮也没反应。
原因:JTable 不会自动感知数据库变化,必须通过 TableModel 通知机制或者重新替换 TableModel。很多新手把查到的 List 直接往旧的 DefaultTableModel 里 addRow,结果旧数据没清掉,新数据又叠上去。
解决:刷新方法里直接new DefaultTableModel(列名数组, 0),把数据库查出来的 List 遍历 addRow,最后table.setModel(model)。这也要求每次刷新时 TableModel 必须是新对象,或者手动调用model.setRowCount(0)清行。
这个问题的现象具有迷惑性,因为程序不报错,看起来一切正常,数据就是不变。
5.3 连接泄漏:finally 和 try-with-resources 必须二选一
现象:程序跑一段时间后,点击任何功能都卡住,控制台报 “Too many connections”。
原因:Connection 没有被关闭。MySQL 默认最多 151 个连接,每次操作都DriverManager.getConnection()打开连接,用完不关,连接数很快就耗尽。
解决:所有数据库操作必须保证 Connection、PreparedStatement、ResultSet 三个对象都被 close。最干净的办法是 try-with-resources,JDK 7 以上的项目都支持。如果项目环境实在旧,就在 finally 里逐个关闭,注意 close 操作要单独 try-catch,不要因为一个异常导致后面的对象关不掉。
数据库连接属于宝贵的资源,关闭连接没有任何玄学,纯粹是代码里少写了自动关闭机制这一件事。
5.4 驱动类的 cj 前缀:mysql-connector 版本带来的 ClassNotFoundException
现象:运行登录功能时抛ClassNotFoundException: com.mysql.jdbc.Driver。
原因:项目里导入的是 mysql-connector-java 8.0 的 jar 包,这个版本的驱动类名迁移到了com.mysql.cj.jdbc.Driver。网上很多老旧的 mysql 安装教程写的还是 5.x 的类名,照着抄就踩坑。
解决:统一用com.mysql.cj.jdbc.Driver,或者干脆删掉 Class.forName 这行代码,让 JDBC 4.0 的 SPI 机制自动加载。顺便确认 pom.xml 或 lib 目录里只有一个 mysql 驱动 jar 包,防止版本冲突。mysql 5.7 和 mysql 8.0 的驱动包能混用的情况极少,别图省事。
驱动类加载是最典型的“配置一下就好”的问题,别看它小,答辩现场遇到这个错误,手忙脚乱翻手机查解决方案的样子并不好看。
6. 把课程设计升级成可交付项目:连接池、导出 Excel 与分层重构
6.1 用连接池替换 DriverManager:先跑通 HikariCP 再谈优化
课程设计阶段用 DriverManager 没问题,但如果你想让答辩老师眼前一亮,把连接方式换成连接池是第一件值得做的事。HikariCP 是目前 Java 生态里最常用的连接池,集成方式很简单,在 pom.xml 引入依赖后,把 DbUtil 类改成从 HikariDataSource 拿连接,DAO 层代码一行都不用动。
public class DbUtil { private static HikariDataSource dataSource; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/course_system" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"); config.setUsername("root"); config.setPassword("123456"); config.setMaximumPoolSize(20); dataSource = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }换成连接池之后,最直观的变化是程序启动变快,反复点查询按钮时不再频繁握手建立 TCP 连接。回答面试题时,连接池的意义可以展开说:复用连接、控制并发上限、连接超时管理。
6.2 用 POI 导出成绩单:验证数据流向的最后一步
导出 Excel 功能几乎是这类系统的标配。Apache POI 是 Java 操作 Excel 最常见的类库,课程设计不需要复杂的格式,创建 Workbook 和 Sheet,把查询出的结果循环写入单元格,保存到本地文件即可。这个功能本身不难,但对数据流向是一个很好的验证:从 MySQL 到 DAO 到界面展示,再通过 POI 输出成文件,整条链路跑通,系统的可用性就有了实感。
一个小小的自查技巧:导出前先在界面上核对一遍表格里的数据行数和 Excel 里的行数是否一致。如果两边不一致,问题大概率出在 DAO 查询结果被界面层过滤了,这是 JTable 刷新问题换了件马甲。
6.3 从 Swing 到 Web 的分层重构:哪些代码可以保留
在这个课程设计基础上,如果你后面去学 Spring Boot + Vue,会发现 DAO 层里的大部分方法可以平移过去。Service 层的业务逻辑保持不变,Swing 界面替换成 Web 前端,数据库四张表基本不用动。
这个项目对你最大的价值就是说出“我理解分层”。Person、DAO、Service 之间没有相互引用,只是通过方法调用接口协作。课程设计可能只是几个窗口的桌面程序,但底层思考方式和企业级应用是一致的。
我当年做完这个题目后,最大的感受是:扎实地把 Swing 界面和 JDBC 写明白,比盲目追逐新框架有用得多。问题不在技术新旧,而在于能不能把流程说清楚。建议你在答辩前亲手跑一遍从安装 MySQL、建库、连接、登录、选课到查成绩的完整路径,把踩过的坑记录下来,这比背一百道 java 面试题都实在。希望帮到你。
本文还有配套的精品资源,点击获取