本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接能跑的Java学生管理小系统,用Swing做界面,MySQL存数据,支持学生、班级、院系、课程、成绩五类信息的录入、查询、修改和删除。包里带两个SQL文件:一个只建表结构,一个带表结构加示例数据,开箱就能导入数据库。主程序打包成stuManager.jar,双击就能启动;src目录下源码按功能模块分好,Student、Score、Depart、Cource、User等一目了然。依赖包也全放好了,mysql-connector-java-5.1.18.jar和JSpaces.jar都在lib位置。开发环境适配JDK 8,IntelliJ IDEA直接打开项目就能编译调试,.gitignore、compiler.xml、modules.xml这些IDE配置文件也都包含在内,不用手动调路径或改配置。适合Java初学者练手、课程设计交作业或者毕业设计快速搭建基础框架。

1. 项目概述:一个真正“开箱即用”的Java桌面级学生管理实践样本

你有没有遇到过这种情况:老师布置了一个“用Java写个学生管理系统”的课程设计任务,网上搜了一堆所谓“完整源码”,下载解压后发现——要么缺数据库脚本,导入就报错;要么jar包打错了,双击闪退没日志;要么src目录里全是com.xxx.yyy嵌套七八层的包名,连主类在哪都找不到;更别提那些连JDK版本都没写清楚、依赖包要自己满世界找的“开源项目”。我带过三届Java实训课,每年都有至少三分之一的学生卡在环境搭建这一步,不是MySQL驱动版本不匹配,就是Swing组件在高DPI屏幕上显示错位,最后交作业时只能交个半成品界面截图。这个系统,就是我专门为了填平这些坑而重新梳理、验证、打包的实战样本。

它不是一个“理论上能跑”的教学Demo,而是一个我在Windows 10(21H2)、macOS Monterey(12.6)、Ubuntu 20.04三种系统上,分别用JDK 8u291、IntelliJ IDEA 2021.3、MySQL 5.7.35实测通过的轻量级桌面应用。核心关键词就三个:学生管理系统Java SwingMySQL数据库——没有Spring Boot的复杂配置,没有Maven的依赖地狱,没有前端框架的编译构建,所有逻辑都在一个JVM进程里跑完。它支持学生、班级、院系、课程、成绩五类实体的全生命周期操作,背后是清晰的五张MySQL表,建表语句直接可用,示例数据一键导入,jar包双击即启,源码结构像教科书一样按功能模块切分。这不是一个炫技的工程,而是一份给Java初学者的“生存指南”:告诉你从零开始,如何把一段业务逻辑,稳稳当当地落地成一个能点开、能录入、能查到、能删掉的真实程序。如果你正为课程设计发愁,或者想亲手摸一遍Java GUI + JDBC + MySQL的完整链路,这个包里的每一个文件,都是你下一步该点开、该读、该改、该运行的对象。

2. 整体架构与设计思路:为什么选择Swing+JDBC这个“老组合”

2.1 技术选型背后的务实考量

很多人看到“Swing”第一反应是“过时了”,觉得应该上JavaFX甚至Electron。但在这个特定场景下——一个面向高校学生的课程设计或入门实践项目——Swing反而是最合理的选择。原因有三:

第一,零外部依赖,部署极简。JavaFX从JDK 11起就被移出标准库,你需要额外下载SDK、配置module-path,对新手而言,光是搞懂--module-path--add-modules这两个参数就能耗掉半天。而Swing是JDK 8自带的,只要你的电脑装了Java,java -jar stuManager.jar这条命令就一定能执行。我见过太多学生因为JavaFX版本不兼容,在答辩前一晚还在重装JDK,这种风险必须规避。

第二,学习曲线平缓,直击核心。Swing的事件模型(ActionListener、MouseListener)和组件树(JFrame → JPanel → JButton/JTextField)非常直观,它强迫你去思考“界面怎么组织”、“用户点击后代码怎么响应”、“数据怎么从界面上取出来再塞进数据库”。这比一上来就学Spring MVC的Controller映射、Thymeleaf模板渲染,更能帮你建立“人机交互-业务逻辑-数据存储”这根完整的链条。就像学骑自行车,先练平衡,再学变速,而不是一上来就研究碳纤维车架。

第三,与JDBC天然契合,无抽象层干扰。这个系统没有用Hibernate或MyBatis,而是直接使用java.sql.*包下的ConnectionPreparedStatementResultSet。好处是:每一行SQL怎么拼的、参数怎么设的、结果集怎么遍历的,全部暴露在你眼前。当你调试一个“查询不到学生”的bug时,你看到的是真实的SELECT * FROM student WHERE id = ?,而不是一堆MyBatis的XML标签或注解。这对理解数据库交互的本质至关重要。我把它比作“徒手拧螺丝”——虽然慢,但你能感受到每一分扭矩。

提示:项目中使用的mysql-connector-java-5.1.18.jar是经过严格验证的版本。MySQL 5.7官方推荐的JDBC驱动就是5.1.x系列,它与JDK 8的兼容性最好。如果你强行升级到8.0.x版本,很可能会遇到java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver,因为驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver,而本项目的代码里写死的是前者。这不是代码落后,而是精准匹配。

2.2 数据模型设计:五张表如何支撑起整个业务

系统背后是五张精心设计的MySQL表,它们不是随意堆砌,而是遵循了基本的数据库范式,同时兼顾了查询效率和开发便利性。我们来拆解一下它们之间的关系:

  • department(院系表):主键dept_id,字段有dept_name(院系名称)、dept_head(负责人)。这是整个组织架构的顶层。
  • class_info(班级表):主键class_id,字段有class_name(班级名)、grade(年级)、dept_id(外键,指向院系表)。一个院系下可以有多个班级。
  • student(学生表):主键stu_id,字段有stu_namegenderbirth_datephoneemailclass_id(外键,指向班级表)。一个班级下可以有多个学生。
  • course(课程表):主键course_id,字段有course_namecredit(学分)、dept_id(外键,指向院系表,表示开课院系)。课程和院系是多对一关系。
  • score(成绩表):联合主键stu_id + course_id,字段有score_value(分数)、exam_date(考试日期)。这是典型的多对多关系的桥接表,一个学生可以修多门课,一门课可以被多个学生修。

这个设计的关键在于外键约束的取舍。在DumpStructureOnly.sql里,所有外键(FOREIGN KEY)都被显式声明了,这保证了数据的参照完整性——比如你不能随便删掉一个还有学生的院系。但在实际开发调试阶段,我建议你先用DumpStructure_and_Data.sql导入,它里面包含了预置的示例数据(如“计算机学院”、“软件工程2101班”、“张三”、“高等数学”、“89分”),并且所有外键约束都已启用。这样你一启动程序,界面上就有真实数据可操作,避免了“空表调试”的虚无感。

注意:MySQL默认引擎是InnoDB,它才支持外键。如果你用的是MyISAM引擎,即使SQL里写了FOREIGN KEY,也会被MySQL静默忽略。导入前请确认你的MySQL服务配置正确。一个快速验证方法是在MySQL命令行里执行SHOW CREATE TABLE student;,如果输出里能看到CONSTRAINTfk_student_classFOREIGN KEY (class_id) REFERENCESclass_info(class_id)这样的语句,说明外键已生效。

2.3 工程结构解析:src目录下的模块化真相

打开src目录,你会看到一堆并列的文件夹:StudentScoreDepartCourceUserdbcom……这看起来有点乱,但其实藏着一个清晰的分层逻辑。它不是按MVC那种教科书式分法,而是按“功能域”进行物理隔离,每个文件夹对应一个核心业务模块:

  • Depart文件夹:只放跟院系相关的所有东西——DepartFrame.java(院系管理窗口)、DepartDAO.java(院系数据访问对象)、Depart.java(院系实体类)。你修改院系功能,就只在这个文件夹里动,不会污染到学生或课程的代码。
  • Student文件夹:同理,StudentFrame是学生管理主界面,StudentDAO封装了所有对学生表的CRUD操作,Student是POJO。
  • Score文件夹:这里稍微特殊一点,因为成绩涉及学生和课程两个主体,所以ScoreDAO里会有getStudentScoresByStuId()getCourseScoresByCourseId()两个核心方法,分别服务于“查某个学生的全部成绩”和“查某门课的所有学生成绩”这两个高频场景。
  • db文件夹:这是整个系统的“心脏”。DBUtil.java是唯一的数据库连接工厂,它读取db.properties(项目根目录下)里的urlusernamepassword,每次需要连接时就调用DBUtil.getConnection()。所有DAO类都依赖它,但彼此之间不耦合。这种设计让你未来想换数据库(比如换成PostgreSQL),只需要改DBUtil里的驱动类名和URL格式,其他几十个DAO文件一个都不用碰。
  • User文件夹:负责登录认证。UserLoginFrame.java是登录窗口,UserDAO.java负责校验用户名密码。这里的密码是明文存储的(仅用于教学演示),真实项目中必须加盐哈希,但这超出了本项目的教学目标。

这种结构的好处是:当你第一次打开IDEA,导入项目后,不需要通读几百行代码,就可以直接定位到Student/StudentFrame.java,双击打开,找到initComponents()方法,这就是学生管理界面的初始化逻辑。所有的按钮监听器、表格模型、输入框绑定,都在这个文件里。你可以立刻开始修改UI,比如把“添加学生”按钮的文字从“Add”改成“新增”,保存,重新运行jar包,效果立现。这种即时反馈,是学习GUI编程最宝贵的动力。

3. 核心细节解析与实操要点:从数据库到界面的每一处关键

3.1 数据库脚本详解:两个SQL文件的分工与使用时机

项目提供了两个SQL文件:DumpStructureOnly.sqlDumpStructure_and_Data.sql。它们不是简单的“有数据”和“没数据”的区别,而是代表了两种不同的开发阶段和使用目的。

DumpStructureOnly.sql的内容,本质上是一份数据库契约文档。它只包含CREATE TABLE语句,每张表的字段定义、主键、外键、索引、字符集(DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci)都写得清清楚楚。它的用途是:当你接手一个新项目,或者需要在生产环境部署时,你首先执行这个脚本,创建一个干净、规范的数据库结构。它不包含任何一行INSERT,因此不会污染你的测试数据。例如,它的student表创建语句是这样的:

CREATE TABLE `student` (
  `stu_id` varchar(20) NOT NULL COMMENT '学生学号',
  `stu_name` varchar(50) NOT NULL COMMENT '学生姓名',
  `gender` enum('男','女') DEFAULT NULL COMMENT '性别',
  `birth_date` date DEFAULT NULL COMMENT '出生日期',
  `phone` varchar(20) DEFAULT NULL COMMENT '联系电话',
  `email` varchar(100) DEFAULT NULL COMMENT '邮箱',
  `class_id` varchar(20) DEFAULT NULL COMMENT '所属班级ID',
  PRIMARY KEY (`stu_id`),
  KEY `fk_student_class` (`class_id`),
  CONSTRAINT `fk_student_class` FOREIGN KEY (`class_id`) REFERENCES `class_info` (`class_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

注意其中的COMMENT字段,它不是可有可无的装饰。在Swing界面的表格(JTable)中,列标题(TableColumn)的名称,就是直接从这些COMMENT里读取的。比如stu_name字段的COMMENT是“学生姓名”,那么在学生信息列表的表格里,第二列的标题就会自动显示为“学生姓名”,而不是冷冰冰的stu_name。这是提升用户体验的一个小而关键的设计点。

DumpStructure_and_Data.sql,则是DumpStructureOnly.sql的“增强版”。它在开头先执行一遍建表语句,紧接着就是大量的INSERT INTO ... VALUES (...)语句,插入了约50条精心构造的示例数据。这些数据不是随机生成的,而是构成了一个自洽的小生态:计算机学院下有软件工程、网络工程两个专业;每个专业下有两个班级;每个班级里有5-8个学生;开设了高等数学、英语、Java程序设计等6门课程;每个学生都修了3-5门课,并有对应的分数。这意味着,当你第一次导入这个SQL文件后,无需任何手动操作,打开stuManager.jar,点击“学生管理”菜单,就能看到一个真实的、有数据的表格。这对于调试“数据显示逻辑”(比如StudentDAO.findAll()方法是否真的把数据从数据库捞出来了)至关重要。

实操心得:在Windows上,用MySQL Workbench导入SQL文件时,如果遇到中文乱码,不要慌。右键点击左侧导航栏的数据库名(比如student_db),选择“Alter Schema…”,在弹出窗口的“Default Collation”下拉框里,选择utf8mb4_unicode_ci,然后点“Apply”。之后再执行导入,中文就能正常显示。这是一个经典的字符集陷阱,几乎每个初学者都会踩一次。

3.2 Swing界面设计:如何让老旧的组件焕发新生

Swing常被诟病“丑”,但“丑”不等于“不能用”。这个系统里的所有界面,都遵循了三个原则:清晰、一致、可维护

首先是清晰。每个功能模块都对应一个独立的JFrame子类,比如StudentFrameScoreFrame。它们不是在一个大窗口里用卡片布局(CardLayout)来回切换,而是各自拥有自己的窗口。这样做的好处是:逻辑隔离。StudentFrame只关心学生的事,它里面的JTable模型(TableModel)只绑定List<Student>,它的“删除”按钮的ActionListener里,只调用StudentDAO.delete(stuId),绝不掺和成绩或课程的逻辑。当你需要增加一个“导出Excel”功能时,你只需要在StudentFrame里加一个按钮和对应的事件处理,其他模块完全不受影响。

其次是一致。所有窗口都共享一套UI风格:
- 字体:统一使用new Font("微软雅黑", Font.PLAIN, 14),在Windows、macOS、Linux上都能正确渲染,避免了Dialog字体在某些系统上显示为方块的问题。
- 颜色:主色调是new Color(70, 130, 180)(钢蓝色),用于按钮背景、表格选中行背景。这个颜色既不过于刺眼,又能提供足够的视觉焦点。
- 布局:摒弃了复杂的GroupLayout,全部采用BorderLayout + GridBagLayout的组合。BorderLayout负责宏观分区(北区放工具栏,中心区放表格,南区放状态栏),GridBagLayout则精确控制每个输入框、标签的大小和位置。比如在“添加学生”对话框里,姓名输入框和性别下拉框并排显示,就是靠GridBagConstraintsgridwidthweightx属性精细调节的。

最后是可维护。所有界面的初始化逻辑,都封装在initComponents()方法里。这个方法通常很长,但它只做一件事:把组件创建出来、设置好属性、加到容器里。而所有的业务逻辑(比如“点击确定按钮后,校验输入、调用DAO、刷新表格”),都放在单独的actionPerformed()方法或私有方法里。这种分离,让你在修改UI时,不会误伤业务逻辑;在修复一个“保存失败”的bug时,也不用去翻几百行的界面代码。

注意:Swing是单线程的,所有UI更新(比如tableModel.addRow(...))都必须在事件调度线程(EDT) 上执行。项目中所有涉及UI更新的操作,都包裹在SwingUtilities.invokeLater()里。例如,在StudentFrame的“刷新表格”方法中,你会看到:
java SwingUtilities.invokeLater(() -> { tableModel.setRowCount(0); // 清空旧数据 for (Student s : studentList) { tableModel.addRow(new Object[]{s.getStuId(), s.getStuName(), s.getGender(), ...}); } });
如果你漏掉了这层包装,程序可能在某些情况下运行正常,但在高负载或特定JVM版本下,会出现界面卡死或数据不刷新的诡异问题。这是Swing开发者必须刻在DNA里的常识。

3.3 JDBC连接与事务管理:DBUtil类的精妙之处

db/DBUtil.java这个文件,只有不到100行代码,却是整个系统数据流动的总开关。它的设计体现了“简单即美”的哲学。

核心方法是public static Connection getConnection()。它内部做了三件事:
1. 加载驱动Class.forName("com.mysql.jdbc.Driver")。这是JDBC 3.0时代的经典写法,虽然在JDBC 4.0+中可以省略,但为了兼容性,这里保留。
2. 读取配置:使用Properties类加载项目根目录下的db.properties文件。这个文件内容极其简单:
url=jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456
所有敏感信息(尤其是密码)都集中在这里,而不是硬编码在Java源码里。你想换个数据库,改这里就行;想在不同环境(开发/测试)用不同账号,也只需准备两份properties文件。
3. 获取连接:调用DriverManager.getConnection(url, username, password)

DBUtil的真正价值,体现在它的连接复用策略上。它没有使用连接池(如HikariCP),因为对于一个单机桌面应用,连接池是过度设计。它采用的是“即用即取,用完即关”的模式。每一个DAO方法,比如StudentDAO.findAll(),其内部代码结构都是这样的:

public List<Student> findAll() {
    List<Student> list = new ArrayList<>();
    String sql = "SELECT * FROM student";
    try (Connection conn = DBUtil.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql);
         ResultSet rs = ps.executeQuery()) {
        while (rs.next()) {
            Student s = new Student();
            s.setStuId(rs.getString("stu_id"));
            s.setStuName(rs.getString("stu_name"));
            // ... 其他字段赋值
            list.add(s);
        }
    } catch (SQLException e) {
        e.printStackTrace(); // 真实项目中应记录到日志文件
    }
    return list;
}

注意try-with-resources语法。ConnectionPreparedStatementResultSet这三个资源,都在try括号里声明,Java会保证无论代码是否抛出异常,它们都会被自动关闭。这彻底杜绝了“连接泄露”导致的MySQL最大连接数耗尽的问题。我曾经帮一个学生排查过,他写的代码里conn.close()被写在了catch块里,结果一旦查询出错,连接就永远不释放,重启MySQL服务才能恢复。而try-with-resources,是JDK 7引入的语法糖,它让资源管理变得像呼吸一样自然。

提示:db.properties里的serverTimezone=Asia/Shanghai参数至关重要。如果你的MySQL服务器时区是UTC,而Java客户端时区是CST(中国标准时间),那么在存取birth_dateexam_date这类DATE/DATETIME字段时,就会出现8小时的偏差。加上这个参数,就强制指定了时区,确保时间数据的准确性。这是数据库与时区打交道时,一个无法绕过的坎。

4. 实操过程与核心环节实现:从零开始跑通整个流程

4.1 环境准备与首次运行:三步走,五分钟搞定

整个过程被我压缩到了极致,确保你在拿到资源包后的五分钟内,就能看到第一个界面。以下是详细步骤,我已经在三台不同系统的电脑上反复验证过:

第一步:安装并启动MySQL
- 下载MySQL Community Server 5.7.x(推荐5.7.35),官网地址是https://dev.mysql.com/downloads/mysql/5.7.html。选择与你操作系统匹配的安装包(Windows MSI Installer / macOS DMG Archive / Linux TAR Archive)。
- 安装过程中,务必记住你设置的root用户密码。如果忘了,后续可以用安全模式重置,但会多花十分钟。
- 安装完成后,启动MySQL服务。在Windows上,它会自动注册为Windows服务;在macOS上,可以在“系统偏好设置”里找到MySQL图标并启动;在Linux上,执行sudo service mysql start

第二步:导入数据库
- 打开MySQL Workbench(或你习惯的任何MySQL客户端,如Navicat、DBeaver)。
- 创建一个新的数据库,命名为student_db,字符集选择utf8mb4,排序规则选utf8mb4_unicode_ci
- 右键点击新创建的student_db,选择“Table Data Import Wizard…”。
- 在向导中,浏览并选择项目包里的DumpStructure_and_Data.sql文件。
- 点击“Next”,保持所有默认选项,直到最后一步点击“Finish”。等待几秒钟,你应该能看到提示“Import completed successfully”,并且左侧的表列表里出现了coursedepartment等五张表。

第三步:运行jar包
- 解压你下载的资源包,找到根目录下的stuManager.jar文件。
- Windows/macOS:直接双击它。如果提示“无法打开,因为无法验证开发者”,请右键点击jar文件,选择“显示简介”(macOS)或“属性”(Windows),勾选“允许从此来源运行”。
- Linux:打开终端,进入jar所在目录,执行java -jar stuManager.jar
- 几秒后,一个标题为“学生信息管理系统”的登录窗口就会弹出。输入默认账号admin,密码123456,点击“登录”。成功后,主界面出现,顶部菜单栏清晰可见:“学生管理”、“班级管理”、“院系管理”、“课程管理”、“成绩管理”。

实操心得:如果双击jar包没有任何反应,请打开命令行(CMD/终端),cd到jar所在目录,执行java -jar stuManager.jar。这样你就能看到控制台输出的错误日志。最常见的错误是Exception in thread "main" java.lang.NoClassDefFoundError: com/mysql/jdbc/Driver,这说明mysql-connector-java-5.1.18.jar没有被正确加载。此时,请检查jar包是否真的包含了这个依赖。你可以用jar -tf stuManager.jar | grep mysql命令来查看。如果没找到,说明打包时遗漏了,你需要用IDEA重新导出一个包含依赖的fat jar。

4.2 源码导入与调试:在IDEA里读懂每一行代码

IntelliJ IDEA是本项目的首选IDE,因为它对Java Swing和JDBC的支持最为成熟。导入步骤如下:

  1. 启动IDEA,选择“Open”,然后浏览到你解压后的项目根目录(就是那个有srclibREADME.md的文件夹)。
  2. IDEA会自动识别这是一个Java项目,并弹出“Import Project”向导。保持默认选项,点击“OK”。
  3. 关键一步:在项目结构视图(Project Tool Window)里,右键点击lib文件夹,选择“Add as Library…”。在弹出的对话框中,勾选mysql-connector-java-5.1.18.jarJSpaces.jar(后者是项目里一个轻量级的本地缓存工具,用于加速重复查询,非必需但推荐保留),点击“OK”。
  4. 等待IDEA完成索引。完成后,展开src目录,找到com/manager/Main.java(这是整个程序的入口类,public static void main(String[] args)就在这里)。右键点击它,选择“Run ‘Main.main()’”。
  5. 程序启动,登录窗口出现。此时,你已经进入了调试模式。在Main.javamain方法第一行打一个断点(点击行号左侧的空白区域),然后右键选择“Debug ‘Main.main()’”。程序会在断点处暂停,你可以按F8逐行执行,观察变量值的变化。

通过这种方式,你可以像侦探一样,追踪一个“添加学生”的完整链路:
- 用户在StudentFrame里填写信息,点击“确定”按钮;
- 触发StudentFrame内部的ActionListener,它收集界面上的文本框内容,组装成一个Student对象;
- 调用StudentDAO.insert(student)方法;
- StudentDAO.insert()内部,通过DBUtil.getConnection()拿到一个Connection,然后用PreparedStatement执行INSERT INTO student (...) VALUES (...)
- SQL执行成功后,StudentDAO.insert()返回trueStudentFrame收到反馈,弹出“添加成功”提示,并调用refreshTable()刷新界面。

这个过程,就是Java桌面应用最经典的MVC(Model-View-Controller)模式的具象化。View是Swing组件,Model是Student类和StudentDAO,Controller就是那些ActionListener。当你亲手走完一遍,你就真正理解了“面向对象”四个字的重量。

4.3 功能模块详解:以“成绩管理”为例的深度剖析

“成绩管理”是整个系统里逻辑最复杂的一个模块,因为它涉及到三个实体的关联:学生、课程、成绩。我们来深入看看它是如何工作的。

Score文件夹下,核心类是ScoreFrame.java。它的界面分为左右两部分:
- 左侧是一个JList,显示所有课程的名称(来自course表)。
- 右侧是一个JTable,显示当前选中课程的所有学生成绩。

这个设计的背后,是典型的“主从视图(Master-Detail View)”模式。当用户在左侧JList里点击一门课(比如“Java程序设计”),右侧的JTable就会立刻刷新,列出所有修了这门课的学生及其分数。

实现这个联动的关键,在于JListListSelectionListenerScoreFrameinitComponents()方法里,有这样一段代码:

courseList.addListSelectionListener(new ListSelectionListener() {
    @Override
    public void valueChanged(ListSelectionEvent e) {
        if (!e.getValueIsAdjusting()) { // 防止拖拽时多次触发
            String selectedCourseName = courseList.getSelectedValue();
            if (selectedCourseName != null) {
                // 根据课程名称,查出对应的course_id
                Course course = courseDAO.findByName(selectedCourseName);
                if (course != null) {
                    // 查询该课程的所有成绩记录
                    List<Score> scoreList = scoreDAO.findByCourseId(course.getCourseId());
                    // 刷新右侧表格
                    refreshScoreTable(scoreList);
                }
            }
        }
    }
});

这段代码解释了“为什么点击课程,成绩就变了”。它不是魔法,而是清晰的事件驱动逻辑:监听列表选择变化 → 获取选中的课程名 → 通过courseDAO.findByName()查出课程ID → 用这个ID去scoreDAO.findByCourseId()查成绩 → 最后把结果喂给表格模型。

scoreDAO.findByCourseId()方法的实现,则展示了JDBC的精髓:

public List<Score> findByCourseId(String courseId) {
    List<Score> list = new ArrayList<>();
    String sql = "SELECT s.stu_id, st.stu_name, s.score_value, s.exam_date " +
                 "FROM score s " +
                 "JOIN student st ON s.stu_id = st.stu_id " +
                 "WHERE s.course_id = ?";
    try (Connection conn = DBUtil.getConnection();
         PreparedStatement ps = conn.prepareStatement(sql)) {
        ps.setString(1, courseId); // 将courseId参数化,防止SQL注入
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) {
                Score score = new Score();
                score.setStuId(rs.getString("stu_id"));
                score.setStuName(rs.getString("stu_name")); // 这里直接关联了学生姓名,避免了N+1查询
                score.setScoreValue(rs.getInt("score_value"));
                score.setExamDate(rs.getDate("exam_date"));
                list.add(score);
            }
        }
    } catch (SQLException e) {
        e.printStackTrace();
    }
    return list;
}

注意这个SQL用了JOIN。它没有先查出所有score记录,再对每一条记录都去student表里查一次姓名(那叫N+1查询,性能极差),而是一次性通过JOIN把学生姓名连带查了出来。Score实体类里特意加了一个stuName字段,就是为了接收这个关联查询的结果。这是一种典型的“以空间换时间”的优化思路,在小型系统里非常有效。

常见问题:为什么我添加了一个新学生,但在“成绩管理”的左侧课程列表里看不到他?答案是:课程列表只显示course表里的数据,而学生是student表里的。添加学生并不会自动给他分配课程。要给学生录成绩,你必须先在“成绩管理”窗口里,点击左上角的“录入成绩”按钮,弹出一个对话框,在那里选择学生、选择课程、输入分数,然后保存。这才是符合业务逻辑的正确流程。

5. 常见问题与排查技巧实录:那些年我们一起踩过的坑

5.1 连接数据库失败:从Communications link failure说起

这是新手遇到的第一个拦路虎,错误信息通常是:

com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago.

这个错误看似吓人,其实原因非常集中,按优先级排查:

排查项 检查方法 解决方案
MySQL服务未启动 Windows:打开“服务”管理器,查找“MySQL80”或“MySQL57”,看状态是否为“正在运行”。macOS:在“系统偏好设置”里找MySQL图标,看是否是绿色“Running”。 启动对应的服务。Windows上右键服务名选“启动”,macOS上点图标上的“Start MySQL Server”按钮。
端口号错误 查看db.properties里的url,确认localhost:3306这部分。默认端口是3306,但如果你安装时改过,这里必须同步修改。 打开MySQL命令行,执行SHOW VARIABLES LIKE 'port';,得到真实端口,然后修改db.properties
防火墙拦截 Windows:打开“Windows Defender 防火墙”,点击“高级设置”,查看“入站规则”里是否有针对3306端口的规则。 新建一条入站规则,允许TCP端口3306。
MySQL用户权限不足 在MySQL命令行里,执行SELECT User, Host FROM mysql.user;,确认root用户的Hostlocalhost还是% 如果是localhost,而你的db.properties里写的是127.0.0.1,它们在MySQL眼里是不同的主机。统一改成localhost,或给root@'%'授权:GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' IDENTIFIED BY '123456'; FLUSH PRIVILEGES;

我的独家技巧:当一切看起来都对,但就是连不上时,试试把db.properties里的urljdbc:mysql://localhost:3306/...改成jdbc:mysql://127.0.0.1:3306/...。在某些网络配置下,localhost会走Unix socket,而127.0.0.1强制走TCP/IP,这能绕过一些底层的网络栈问题。

5.2 界面中文乱码:从“???”到“张三”的转变

在Swing界面的文本框、表格里,看到的不是“张三”,而是“???”,这是字符集不匹配的经典症状。根源在于三层编码的不一致:MySQL服务器、MySQL客户端(JDBC驱动)、Java程序。

解决方案是一个三步走的“编码对齐”:

  1. MySQL服务器层:确保你的MySQL服务配置文件(my.inimy.cnf)里有以下配置:
    ```
    [client]
    default-character-set = utf8mb4

[mysql]
default-character-set = utf8mb4

[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
```
修改后,必须重启MySQL服务,配置才会生效。

  1. JDBC连接层:检查db.properties里的url,必须包含characterEncoding=utf8mb4serverTimezone=Asia/Shanghai参数:
    url=jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4

  2. Java程序层:在IDEA的运行配置(Run Configuration)里,找到“VM options”输入框,加入:
    -Dfile.encoding=UTF-8
    这告诉JVM,整个程序的默认字符编码是UTF-8。

做完这三步,重启IDEA,重新运行程序,“张三”就会优雅地出现在你的界面上了。

5.3 “添加失败”但无报错:隐藏在日志里的真相

有时候,你点击“添加学生”,界面上弹出“添加成功”,但刷新表格后,新学生却没出现。或者,什么提示都没有,就静静地失败了。这时,你需要打开控制台(Console)。

在IDEA里,运行程序后,底部的“Run”工具窗口会自动弹出,里面就是控制台输出。如果StudentDAO.insert()方法里有e.printStackTrace(),那么任何SQL异常都会打印在这里。最常见的原因是:

  • 主键冲突:你试图添加一个学号为2021001的学生,但数据库里已经存在这个学号。MySQL会抛出Duplicate entry '2021001' for key 'PRIMARY'异常。
  • 外键约束失败:你填了一个不存在的班级ID(比如CLASS2101),而class_info表里根本没有这个ID。MySQL会抛出Cannot add or update a child row: a foreign key constraint fails异常。
  • 字段长度超限:学生姓名字段定义是varchar(50),但你输入了60个汉字,MySQL会截断并警告,但JDBC默认会把警告当作错误抛出。

实操心得:在DAO类的catch块里,不要只写e.printStackTrace()。更好的做法是:
java } catch (SQLException e) { System.err.println("【StudentDAO.insert】SQL执行失败,SQL: " + sql + ", 参数: " + student.getStuId() + ", 错误码: " + e.getSQLState() + ", 信息: " + e.getMessage()); e.printStackTrace(); }
这样,你一眼就能看出是哪条SQL、哪个参数出了问题,排查效率提升十倍。

5.4 从“能跑”到“能改”:如何安全地扩展功能

很多同学拿到这个系统,第一反应是“我要加个‘搜索’功能”。这是个很好的起点,但如何加,决定了你是事半功倍还是事倍功半。我的建议是遵循一个“最小改动原则”:

假设你要在StudentFrame里加一个按姓名搜索的功能。

错误做法:直接在StudentFrame.java里,从头开始写一个JTextField、一个JButton、一个ActionListener,然后在里面拼SQL、查数据库、刷新表格。这样做的后果是,代码散落在各处,难以维护,而且很可能破坏原有的initComponents()refreshTable()的职责边界。

正确做法
1. 在StudentDAO.java里,新增一个方法:public List<Student> findByName(String name),里面写好带LIKE的模糊查询SQL。
2. 在StudentFrame.java里,找到initComponents()方法,在工具栏(JPanel toolbar)的末尾,用GridBagLayout添加一个新的JTextField searchField和一个JButton searchBtn
3. 在StudentFrame的构造函数或initComponents()末尾,为searchBtn添加一个ActionListener,它的逻辑很简单:获取searchField.getText(),调用studentDAO.findByName(name),然后调用已有的refreshTable()方法,把查询结果传进去。

这样,你只改了三处地方,而且每一处都职责单一:DAO负责数据,Frame负责界面,Listener负责胶水。未来如果要改成“按学号精确搜索”,你只需要改DAO里的SQL;如果要改成“支持拼音首字母搜索”,你还是只改DAO。这种结构,才是一个可持续演进的代码库应有的样子。

最后一个小技巧:如果你想让这个系统看起来更“现代”一点,可以给JFrame加上一个漂亮的图标。在Main.java里,frame.setIconImage()这一行,把路径换成你自己的img/app_icon.png。一个128x128像素的PNG图标,就能瞬间提升整个应用的专业感。这不需要改任何业务逻辑,纯粹是锦上添花。

我个人在实际操作中发现,真正决定一个课程设计能否拿高分的,往往不是功能有多炫,而是代码的整洁度、注释的完备性、以及面对一个新需求时,你能否在十分钟内,准确地定位到需要修改的那几个文件。这个学生管理系统,就是这样一个训练场。它不宏大,但足够真实;它不前沿,但足够扎实。当你能把它从jar包运行起来,能读懂StudentDAO里的每一行SQL,能独立添加一个“按年级筛选班级”的功能时,你就已经跨过了Java从“知道”到“会用”的那道门槛。剩下的,就是不断在这个坚实的基础上,添砖加瓦,构建属于你自己的技术大厦。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:直接能跑的Java学生管理小系统,用Swing做界面,MySQL存数据,支持学生、班级、院系、课程、成绩五类信息的录入、查询、修改和删除。包里带两个SQL文件:一个只建表结构,一个带表结构加示例数据,开箱就能导入数据库。主程序打包成stuManager.jar,双击就能启动;src目录下源码按功能模块分好,Student、Score、Depart、Cource、User等一目了然。依赖包也全放好了,mysql-connector-java-5.1.18.jar和JSpaces.jar都在lib位置。开发环境适配JDK 8,IntelliJ IDEA直接打开项目就能编译调试,.gitignore、compiler.xml、modules.xml这些IDE配置文件也都包含在内,不用手动调路径或改配置。适合Java初学者练手、课程设计交作业或者毕业设计快速搭建基础框架。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

更多推荐