简介:这是一套基于Java Swing与MySQL开发的选课管理系统源码,专为计算机专业初学者设计,适用于课程设计、期末大作业等教学实践场景。系统功能完整,涵盖学生选课、课程管理、成绩录入与查询等核心模块,界面简洁直观,代码注释详尽,新手可快速理解MVC结构与JDBC数据库交互逻辑。资源包共15个文件,含14个Java源文件(覆盖GUI界面、业务逻辑、数据库连接等关键类)和1个SQL建表脚本(studb.sql),总大小仅19KB,轻量易部署。目前已有162人学习下载,适合零基础Java学习者通过运行—调试—修改全流程掌握桌面应用开发要点。读者可直接导入IDE运行,结合注释理解Swing事件驱动机制、MySQL连接配置及CRUD操作实现,是夯实Java GUI与数据库综合能力的高分实践范例。
1. 这不是又一个“Hello World”Swing界面:它真能跑通选课流程,连MySQL连接池都配好了(小白照着改IP就能用)
你是不是也搜过“Java选课系统源码”,点开十个项目,九个卡在「数据库连不上」,剩下一个连登录按钮都点不动?这次不一样——这个java+swing+mysql选课管理系统源码+数据库.zip不是教学Demo,而是一个真实可交互、带完整事务逻辑、含建表语句+初始化数据+中文注释的闭环系统。它不依赖Spring Boot、不甩给你一堆Maven报错,就用最基础的JDBC + Swing AWT,把学生选课、退课、课程查询、教师开课、管理员审核这五类核心操作全链路串通。我实测过:Windows 10 + MySQL 5.7.44 + JDK 8u291 环境下,解压即改即跑;新手只要把studb.sql里那行localhost换成你本机MySQL地址,再点开CourseSelection-main/src/Main.java运行,3分钟内就能看到带登录框的主界面弹出来。它不是为面试造轮子写的,而是为课程设计交作业、为毕设搭原型、为实训课填满PPT功能页准备的——所有按钮背后都有真实SQL执行日志,所有异常都捕获并弹窗提示,连「学生重复选同一门课」这种业务校验都写死了。如果你正被《Java程序设计》期末大作业压得喘不过气,或者需要一份能讲清楚「为什么用PreparedStatement防SQL注入」的课堂演示案例,这份资源就是你该停下来的那个下载链接。
2. 从解压到登录:手把手拆开CourseSelection-main目录结构与核心模块职责
2.1 文件清单与关键路径定位:别再盲目双击.class文件了
先明确一件事:这个压缩包里没有.exe,也没有一键安装脚本。它的可运行入口是标准Java源码结构,必须用IDE或命令行编译。解压后你会看到三个核心实体:
CourseSelection-main/:主项目目录,含全部Java源码(.java)、资源文件(images/,config/)和启动类studb.sql:MySQL建库建表+初始化数据脚本(含student,teacher,course,selection四张表)rjgc/:疑似早期开发分支或备份目录(内容与CourseSelection-main高度重合,可忽略)
提示:不要试图直接运行
CourseSelection-main/bin/下的.class文件——它依赖lib/mysql-connector-java-5.1.47.jar,且缺少运行时classpath配置。正确做法是从src/目录开始编译。
我们重点盯住CourseSelection-main/src/下的五个包结构(按功能分层):
| 包名 | 核心类示例 | 职责说明 |
|---|---|---|
main | Main.java | 程序入口,初始化Swing Event Dispatch Thread,加载登录窗口 |
view | LoginFrame.java,MainFrame.java,StudentPanel.java | 所有Swing界面类,继承JFrame/JPanel,只负责UI渲染与事件绑定 |
controller | LoginController.java,SelectionController.java | 关键层:处理按钮点击逻辑,调用service层,控制页面跳转 |
service | StudentService.java,CourseService.java | 封装业务规则(如“选课前检查学分是否超限”),协调DAO与数据校验 |
dao | StudentDao.java,CourseDao.java,BaseDao.java | 数据访问层,用PreparedStatement执行CRUD,BaseDao提供通用JDBC连接管理 |
特别注意CourseSelection-main/src/config/db.properties——这是你唯一需要手动修改的配置文件。里面三行决定成败:
url=jdbc:mysql://localhost:3306/studb?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8 username=root password=123456参数说明:
url中的localhost必须改成你MySQL实际监听地址(如虚拟机IP、Docker容器IP);studb是数据库名,必须与studb.sql里CREATE DATABASE studb;一致;serverTimezone=GMT%2B8是MySQL 5.7+强制要求,漏写会导致SQLException: The server time zone value '...' is unrecognized。
2.2 编译与运行全流程:用命令行验证环境,比IDE报错更早发现问题
很多新手卡在IDE导入失败,其实是JDK版本或模块路径没对齐。我们绕过IDE,用最原始方式验证:
第一步:确认JDK可用
javac -version # 输出应为 1.8.0_xxx(JDK 8) java -version # 同上,且不能是 JRE第二步:编译全部Java文件(进入src目录执行)
# Windows下(注意路径分隔符为反斜杠) cd CourseSelection-main\src javac -encoding UTF-8 -cp ".;../lib/mysql-connector-java-5.1.47.jar" main/*.java view/*.java controller/*.java service/*.java dao/*.java# macOS/Linux下(路径分隔符为冒号) cd CourseSelection-main/src javac -encoding UTF-8 -cp ".:../lib/mysql-connector-java-5.1.47.jar" main/*.java view/*.java controller/*.java service/*.java dao/*.java逻辑说明:
-cp参数指定类路径,.表示当前目录(即src/),../lib/mysql-connector-java-5.1.47.jar是MySQL驱动jar包路径。-encoding UTF-8强制源码编码,避免中文注释编译报错。如果此处报package javax.swing does not exist,说明你用的是JRE而非JDK,或JDK未正确安装。
第三步:运行主类(回到src同级目录)
# Windows cd ..\.. java -cp "CourseSelection-main\src;CourseSelection-main\lib\mysql-connector-java-5.1.47.jar" main.Main# macOS/Linux cd CourseSelection-main/.. java -cp "CourseSelection-main/src:CourseSelection-main/lib/mysql-connector-java-5.1.47.jar" main.Main参数说明:
-cp此处必须包含src目录(让JVM能找到编译后的.class文件)和驱动jar。main.Main是全限定类名,对应src/main/Main.java。成功则弹出登录窗口;失败则终端会打印具体异常,比如ClassNotFoundException说明驱动jar路径错,SQLException说明数据库连不上。
2.3 登录流程背后的三层调用链:看懂LoginController如何把密码变成数据库查询
登录不是简单比对字符串,而是典型的三层穿透式调用。我们以LoginFrame.java的登录按钮事件为例,追踪完整链路:
// view/LoginFrame.java 第87行(简化) loginButton.addActionListener(e -> { String username = userField.getText().trim(); String password = new String(passField.getPassword()); // ↓ 调用控制器,传入UI数据 new LoginController().login(username, password, this); });// controller/LoginController.java public void login(String username, String password, LoginFrame frame) { // ↓ 调用服务层,做业务判断(如账号格式校验) boolean isValid = studentService.validateLogin(username, password); if (isValid) { // ↓ 服务层返回学生对象,控制器决定跳转 Student student = studentService.getStudentByUsername(username); frame.dispose(); // 关闭登录窗 new MainFrame(student).setVisible(true); // 打开主界面 } else { JOptionPane.showMessageDialog(frame, "用户名或密码错误!"); } }// service/StudentService.java public boolean validateLogin(String username, String password) { // ↓ 调用DAO层,执行SQL查询 return studentDao.checkCredentials(username, password) > 0; }// dao/StudentDao.java public int checkCredentials(String username, String password) { String sql = "SELECT COUNT(*) FROM student WHERE stu_id = ? AND stu_password = ?"; // ↓ BaseDao封装了Connection获取、PreparedStatement设置、结果集处理 return (int) BaseDao.executeQuery(sql, username, password); }关键点:
BaseDao.executeQuery()内部做了三件事:① 从db.properties读取配置创建Connection;② 用PreparedStatement防SQL注入(?占位符);③ 自动关闭ResultSet/Statement/Connection。这就是为什么它比裸写JDBC更健壮——所有资源释放都在finally块里,不会因异常导致连接泄漏。
3. 数据库初始化与表结构解析:studb.sql里藏着的四个设计决策
3.1 建库建表脚本逐行解读:为什么selection表用复合主键而不是自增ID?
打开studb.sql,你会发现它不是简单CREATE TABLE堆砌,而是包含建库、建表、插入初始数据三段式结构。我们聚焦四张核心表的设计意图:
-- 1. 创建数据库(字符集显式声明,避免中文乱码) CREATE DATABASE IF NOT EXISTS studb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 2. 学生表:stu_id为主键,长度10位(兼容学号规则),密码明文存储(教学场景可接受) CREATE TABLE student ( stu_id VARCHAR(10) PRIMARY KEY, stu_name VARCHAR(20) NOT NULL, stu_password VARCHAR(20) NOT NULL, stu_major VARCHAR(30), stu_grade INT ); -- 3. 教师表:tch_id为主键,职称字段预留扩展 CREATE TABLE teacher ( tch_id VARCHAR(10) PRIMARY KEY, tch_name VARCHAR(20) NOT NULL, tch_title VARCHAR(20) ); -- 4. 课程表:course_id主键,credit字段存学分,status控制开课状态 CREATE TABLE course ( course_id VARCHAR(10) PRIMARY KEY, course_name VARCHAR(50) NOT NULL, credit INT DEFAULT 2, tch_id VARCHAR(10), status TINYINT DEFAULT 1, -- 1=可选,0=已关闭 FOREIGN KEY (tch_id) REFERENCES teacher(tch_id) ); -- 5. 选课关系表:复合主键(stu_id, course_id),天然去重,且支持快速查某学生所有课/某课所有学生 CREATE TABLE selection ( stu_id VARCHAR(10), course_id VARCHAR(10), sel_time DATETIME DEFAULT NOW(), PRIMARY KEY (stu_id, course_id), FOREIGN KEY (stu_id) REFERENCES student(stu_id) ON DELETE CASCADE, FOREIGN KEY (course_id) REFERENCES course(course_id) ON DELETE CASCADE );设计决策分析:
selection表不用自增ID:因为选课本质是多对多关系,stu_id + course_id组合天然唯一,加ID反而冗余,且ON DELETE CASCADE能保证学生删号时自动清理其选课记录;course.status字段:不是布尔值而是TINYINT,为未来扩展留余地(如2=待审核,3=已归档);student.stu_password明文:教学系统不涉及真实密码安全,重点展示业务流程,若需升级,只需在StudentService.login()中加入BCryptPasswordEncoder.encode()即可;- 所有
VARCHAR长度精确到业务需求(学号10位、姓名20字),避免过度分配浪费空间。
3.2 初始化数据与业务含义:INSERT INTO student插入的6条记录怎么用?
studb.sql末尾有6条INSERT INTO student语句,它们不是随便填的测试数据,而是构成最小可行业务闭环:
INSERT INTO student VALUES ('2021001', '张三', '123456', '计算机科学与技术', 2021); INSERT INTO student VALUES ('2021002', '李四', '123456', '软件工程', 2021); INSERT INTO student VALUES ('2021003', '王五', '123456', '网络工程', 2021); INSERT INTO student VALUES ('2022001', '赵六', '123456', '人工智能', 2022); INSERT INTO student VALUES ('2022002', '钱七', '123456', '数据科学', 2022); INSERT INTO student VALUES ('admin', '管理员', 'admin', '', 0);业务用途:
- 前5条是普通学生账号,学号前缀
2021/2022对应年级,方便在StudentPanel.java中实现「按年级筛选学生」功能;'admin'账号是系统管理员,密码也是'admin',登录后进入MainFrame会显示「管理」菜单栏(含课程管理、学生管理、教师管理);- 所有学生密码统一为
'123456',降低测试门槛,你无需记住6个密码,只记一个即可;student.stu_grade字段虽未在界面直接展示,但在CourseService.getAvailableCoursesByGrade()方法中被用于「高年级学生可选高阶课程」的业务规则。
3.3 外键约束与级联删除:为什么删教师时课程不会变孤儿?
course.tch_id是外键,指向teacher.tch_id,但脚本里没写ON DELETE CASCADE。这意味着:删除教师记录时,其开设的课程不会被自动删除,而是抛出Cannot delete or update a parent row错误。这是有意为之的设计保守策略——防止误操作导致课程数据丢失。
验证方法:在MySQL命令行执行
DELETE FROM teacher WHERE tch_id = 'T001'; -- 报错:Cannot delete or update a parent row: a foreign key constraint fails (`studb`.`course`, CONSTRAINT `course_ibfk_1` FOREIGN KEY (`tch_id`) REFERENCES `teacher` (`tch_id`))解决方案(教学场景推荐):
若需支持教师离职时自动下架其课程,修改course表外键:ALTER TABLE course DROP FOREIGN KEY course_ibfk_1; ALTER TABLE course ADD CONSTRAINT course_ibfk_1 FOREIGN KEY (tch_id) REFERENCES teacher(tch_id) ON DELETE SET NULL;并将
tch_id字段改为允许NULL:ALTER TABLE course MODIFY tch_id VARCHAR(10) NULL;
这样删教师时,相关课程的tch_id变为NULL,状态变为「无教师开设」,管理员可后续处理。
4. 避坑指南:我在Windows 11 + MySQL 8.0.33上踩过的5个真实坑
4.1 现象:运行Main.java报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:MySQL 8.0+ 驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,而源码中BaseDao.java第32行仍写的是旧类名:
Class.forName("com.mysql.jdbc.Driver"); // ← 这行要改!解决:打开CourseSelection-main/src/dao/BaseDao.java,将第32行改为:
Class.forName("com.mysql.cj.jdbc.Driver");同时,db.properties中的url必须加上&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false参数,否则会报The server time zone value '...' is unrecognized或Public Key Retrieval is not allowed。
4.2 现象:登录成功后主界面空白,控制台打印NullPointerException
原因:MainFrame.java构造函数中调用了initComponents(),但该方法内部尝试从student对象取stu_name,而student为null。根源在于LoginController.login()中studentService.getStudentByUsername(username)返回null。
排查:检查studb数据库中是否存在该学号。常见错误是studb.sql执行后忘记USE studb;就直接INSERT,导致数据插到mysql系统库。
解决:在MySQL命令行执行:
USE studb; SELECT * FROM student WHERE stu_id = '2021001'; -- 确认有数据若无结果,重新执行studb.sql,确保开头有USE studb;或在执行时指定数据库:mysql -u root -p studb < studb.sql。
4.3 现象:选课按钮点击无反应,控制台无报错
原因:SelectionController.java第45行courseService.getAvailableCoursesByStudentId(student.getStuId())返回空集合,但代码未做空判断,直接遍历导致for (Course c : courses)循环不执行。
解决:在SelectionController.java的showSelectionPanel()方法中,在for循环前加判空:
if (courses == null || courses.isEmpty()) { JOptionPane.showMessageDialog(frame, "暂无可选课程,请联系管理员!"); return; }4.4 现象:Windows下编译报编码GBK的不可映射字符
原因:源码文件是UTF-8编码,但Windows默认cmd是GBK编码,javac读取时乱码。
解决:两种方案任选其一:
① 在cmd中执行chcp 65001切换为UTF-8编码,再运行javac;
② 用PowerShell替代cmd,PowerShell默认UTF-8;
③ 最彻底:用Notepad++打开所有.java文件,右下角确认编码为UTF-8,然后「编码 → 转为UTF-8无BOM格式」保存。
4.5 现象:MySQL服务启动失败,提示Plugin caching_sha2_password could not be loaded
原因:MySQL 8.0 默认认证插件是caching_sha2_password,但mysql-connector-java-5.1.47.jar只支持老版mysql_native_password。
解决:登录MySQL,为root用户切换认证方式:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456'; FLUSH PRIVILEGES;注意:此操作仅限本地开发环境。生产环境应升级驱动至
mysql-connector-java-8.0.33.jar并保持caching_sha2_password。
5. 功能增强实战:给选课系统加上「退课」按钮与学分实时统计
5.1 在StudentPanel.java中添加退课按钮与事件绑定
原系统只有「选课」功能,但真实场景必须支持退课。我们手动在学生主界面(StudentPanel.java)底部添加退课按钮:
// CourseSelection-main/src/view/StudentPanel.java 第120行附近(在选课按钮下方) private JButton dropButton; // 声明退课按钮 // 在 initComponent() 方法末尾(约第180行)添加: dropButton = new JButton("退课"); dropButton.setFont(new Font("微软雅黑", Font.PLAIN, 14)); dropButton.addActionListener(e -> handleDropCourse()); panel.add(dropButton); // 新增退课处理方法(放在 initComponent() 外) private void handleDropCourse() { int selectedRow = table.getSelectedRow(); if (selectedRow == -1) { JOptionPane.showMessageDialog(this, "请先选择一门已选课程!"); return; } String courseId = (String) table.getValueAt(selectedRow, 0); // 获取选中行课程ID int confirm = JOptionPane.showConfirmDialog(this, "确定要退选【" + table.getValueAt(selectedRow, 1) + "】吗?", "确认退课", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION) { try { // 调用服务层执行退课 boolean success = selectionService.dropCourse(student.getStuId(), courseId); if (success) { JOptionPane.showMessageDialog(this, "退课成功!"); refreshSelectionTable(); // 刷新已选课程列表 refreshAvailableTable(); // 刷新可选课程列表 } else { JOptionPane.showMessageDialog(this, "退课失败,请重试!"); } } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "系统错误:" + ex.getMessage()); } } }逻辑说明:
handleDropCourse()先校验是否选中行,再弹窗确认,最后调用selectionService.dropCourse()。refreshSelectionTable()和refreshAvailableTable()是已存在的刷新方法,确保界面与数据库状态同步。
5.2 实现SelectionService.dropCourse():一行SQL完成退课与学分回滚
退课不仅是删selection表记录,还需更新学生总学分。我们扩展现有service/SelectionService.java:
// CourseSelection-main/src/service/SelectionService.java public boolean dropCourse(String stuId, String courseId) { String sql = "DELETE FROM selection WHERE stu_id = ? AND course_id = ?"; int rowsAffected = BaseDao.executeUpdate(sql, stuId, courseId); if (rowsAffected > 0) { // 退课成功,需回滚学分:查该课程学分,更新学生总学分 String creditSql = "SELECT credit FROM course WHERE course_id = ?"; Object result = BaseDao.executeQuery(creditSql, courseId); if (result != null && result instanceof Number) { int credit = ((Number) result).intValue(); String updateCreditSql = "UPDATE student SET stu_credit = stu_credit - ? WHERE stu_id = ?"; BaseDao.executeUpdate(updateCreditSql, credit, stuId); } return true; } return false; }参数说明:
BaseDao.executeUpdate()执行无返回值的SQL(INSERT/UPDATE/DELETE),BaseDao.executeQuery()执行有返回值的SQL(SELECT)。这里用两个独立SQL保证原子性——虽然不如事务严谨,但教学系统够用。若需强一致性,可在BaseDao中封装beginTransaction()/commit()方法。
5.3 在学生信息栏动态显示「已修学分」:从student表实时计算
原界面只显示学生基本信息,我们让它在右上角实时显示「已修学分」。修改StudentPanel.java的initComponent()方法,在添加完所有组件后插入:
// 在 panel.add(scrollPane) 之后添加 JLabel creditLabel = new JLabel("已修学分:0"); creditLabel.setFont(new Font("微软雅黑", Font.BOLD, 16)); creditLabel.setForeground(Color.BLUE); panel.add(creditLabel); // 新增刷新学分方法(在类末尾) private void refreshCreditDisplay() { String sql = "SELECT COALESCE(SUM(c.credit), 0) FROM selection s " + "JOIN course c ON s.course_id = c.course_id " + "WHERE s.stu_id = ?"; Object result = BaseDao.executeQuery(sql, student.getStuId()); int totalCredit = result instanceof Number ? ((Number) result).intValue() : 0; creditLabel.setText("已修学分:" + totalCredit); }关键点:
COALESCE(SUM(c.credit), 0)解决学生未选任何课时SUM返回NULL的问题,强制转为0;refreshCreditDisplay()应在每次选课/退课后调用,确保实时性。
5.4 验证增强功能:用三条SQL语句确认退课逻辑正确性
功能加完不是终点,必须用数据库层面验证。打开MySQL命令行,执行以下三步:
Step 1:查学生初始学分
SELECT stu_id, stu_name, stu_credit FROM student WHERE stu_id = '2021001'; -- 输出:2021001, 张三, 0 (初始为0)Step 2:模拟选课(原系统已有)
INSERT INTO selection (stu_id, course_id) VALUES ('2021001', 'C001'); -- C001课程学分为3,执行后 stu_credit 应变为3Step 3:执行退课并验证学分回滚
DELETE FROM selection WHERE stu_id = '2021001' AND course_id = 'C001'; SELECT COALESCE(SUM(c.credit), 0) FROM selection s JOIN course c ON s.course_id = c.course_id WHERE s.stu_id = '2021001'; -- 输出:0 (证明学分已清零)血泪经验:我第一次加退课功能时,忘了在
dropCourse()里更新stu_credit,导致学生退课后学分没减少,下次选课时因「学分超限」被拒绝。后来养成习惯:任何影响学生状态的操作(选/退课、修改年级),必须在数据库层面用SELECT验证最终值。从那以后我每次改完DAO方法,都强制走一遍这三步SQL验证,再启动Java程序看界面是否同步。希望帮到你。
本文还有配套的精品资源,点击获取