☰
Java Swing+MySQL学生信息管理系统:JDBC实战与避坑指南
2026/10/18 16:56:37 网站建设 项目流程

简介:一套基于Java Swing与MySQL的学生信息管理系统完整项目,面向正在学习Java桌面应用开发、需要完成课程设计或毕业设计的在校学生,也适合入门者观摩完整小项目。系统覆盖学生信息录入、查询、修改、删除与列表展示,采用三层架构将界面、业务逻辑和数据库访问分离,能够清楚演示Swing控件、事件监听、JDBC操作及SQL语句在真实项目中的配合方式。压缩包共58个文件,以21个Java源码和31个class文件为主,另含1个建表SQL脚本、1个运行依赖jar包和工程配置文件,整体约2.01MB,目录结构清晰,便于导入IDE运行并对照源码学习。目前已有5100余人学习下载,使用该资源可快速搭建一个可运行的学生管理桌面应用,也能为课程报告或后续扩展成更完整的管理系统提供可复用的基础框架。读者可按源码与编译后文件对照理解Swing界面搭建和MySQL数据表设计,在实际编码中逐步熟悉桌面应用开发流程。

1. Java+Swing+MySQL 实现学生信息管理系统:为什么这个老题目还在不断被做

“Java+Swing+MySQL 实现学生信息管理系统”这个题目,在课程设计和练手项目里出现频率一直居高不下。它不追求前端工程化,也不依赖分布式中间件,却能把 JDBC、SQL、Swing 事件分发、ResultSet 映射这些基本功全部串起来。很多人以为它简单,真正动手才发现:中文乱码、界面卡死、驱动版本不匹配、SQL 注入,每个都足以卡住一下午。这篇文章按“先定表结构、再写 DAO、最后调界面”的完整路径展开,把连接串参数、CRUD 写法、必踩的坑按顺序排好。适合正在做课设、想补 JDBC 基础、或者要给内部团队做个轻量小工具的人——这个项目不新,但用它练基本功和交付小型桌面工具,依然是最省成本的路径。

2. 先把地基打对:Swing 界面拆分与 MySQL 表结构设计

2.1 界面不写在一个类里:MVC 式拆分与实体类设计

我接手过不少这类系统的半成品,最常见的形态是:一个 JFrame 里塞了查询面板、表格、按钮、监听器、JDBC 代码,甚至还有 ResultSet。这种写法在演示时能跑,但改一个需求就开始遭罪。要加一个“按专业筛选”的查询条件,得先在一千行的事件代码里找 setString 写在哪个方法里,改完还不确定会不会影响其他按钮。

常见做法是拆三层:entity 放实体,dao 放数据库操作,view 放界面。Swing 不需要严格的 MVC 框架,只要 dao 不引用任何界面组件,监听器里直接调 dao,结构就能保持清晰。实体类字段要和 student 表一一对应,类型尽量贴合 Java 习惯。下面这个 Student 类是整套系统的数据载体,后面所有方法都在和它打交道。

import java.util.Date; public class Student { private int id; // 数据库自增主键 private String studentNo; // 学号 private String name; // 姓名 private int gender; // 1=男,2=女 private Date birthday; // 出生日期 private String phone; // 联系电话 private String major; // 专业 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getStudentNo() { return studentNo; } public void setStudentNo(String studentNo) { this.studentNo = studentNo; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getGender() { return gender; } public void setGender(int gender) { this.gender = gender; } public Date getBirthday() { return birthday; } public void setBirthday(Date birthday) { this.birthday = birthday; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getMajor() { return major; } public void setMajor(String major) { this.major = major; } }

这个实体类里有几个值得注意的选择。gender 用 int 而不是 boolean,是因为男和女之外还可能出现“保密”“未知”这类状态,int 扩展起来成本最低;如果用 boolean,后面加状态就得改表结构和所有判断逻辑。birthday 用 java.util.Date,而 JDBC 的 setDate 需要的是 java.sql.Date——前者是后者的父类,所以从 ResultSet 里取出的 java.sql.Date 可以直接赋值给 java.util.Date,但反方向不行,写代码时要注意。getter/setter 虽然啰嗦,但一定要写全,JTable 的列渲染和后续加日志、加导出功能都依赖它们,别图省事用字段直接访问。

2.2 学生表结构设计:字段类型选择与建表 SQL

写 Java 之前,先把表结构定下来。我的习惯是先在命令行里建库建表,把 SQL 跑通,再回 IDE 里写代码。这样能把字符集、字段类型、索引这些容易出问题的地方,提前挡在代码之外。下面这份建表语句可以直接复制执行。

CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_db; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '内部主键', student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender TINYINT NOT NULL DEFAULT 1 COMMENT '1=男, 2=女', birthday DATE NULL COMMENT '出生日期', phone VARCHAR(20) NULL COMMENT '联系电话', major VARCHAR(50) NULL COMMENT '专业', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', KEY idx_name (name) ) ENGINE=InnoDB COMMENT='学生信息表';

几个关键设计理由,照着抄容易,理解为什么才是关键。主键用自增 id,而不是把 student_no 当主键——学号带业务含义,将来学校改学号规则,或者出现借读、转学这类特殊情况,用业务字段做主键会连带改一堆关联表。内部主键对外无意义,只做唯一标识。gender 用 TINYINT 而不是 CHAR(1) 存“男/女”,排序和过滤都方便,界面显示时再做一次映射,也避免中文在字符集混乱时直接写进表里。name 加普通索引 idx_name,是因为按姓名模糊查询是最常用的操作,数据量到几千行时,全表扫描会明显拖慢搜索。

字符集这里有个很多人没注意的细节:MySQL 的 utf8 实际是 utf8mb3,能省一点空间,但遇到生僻字或 emoji 会丢数据。新项目直接用 utf8mb4,空间代价几乎可以忽略,却能少一类“数据库里存不进汉字”的玄学问题。created_at 和 updated_at 这两个时间字段不是必需,但强烈建议保留。它能在你排查“这条数据什么时候改的”时,提供最直接的证据,代价只是几句话。

2.3 主界面布局:查询区、表格区、按钮区的摆放顺序

管理类界面的布局很固定,用 BorderLayout 就能解决:北侧放查询工具栏,中间放 JScrollPane 套 JTable,南侧放按钮面板。这样窗口拉伸时表格能自适应变大,按钮和查询区保持固定高度。下面是主窗口的骨架代码。

public class MainFrame extends JFrame { private StudentDao studentDao = new StudentDao(); private JTable studentTable = new JTable(); private DefaultTableModel tableModel; private JTextField searchField = new JTextField(20); public MainFrame() { setTitle("学生信息管理系统"); setSize(900, 600); setDefaultCloseOperation(EXIT_ON_CLOSE); setLocationRelativeTo(null); // 北侧查询区 JToolBar toolBar = new JToolBar(); toolBar.add(new JLabel("姓名/学号:")); toolBar.add(searchField); JButton searchBtn = new JButton("查询"); toolBar.add(searchBtn); add(toolBar, BorderLayout.NORTH); // 中间表格区 tableModel = new DefaultTableModel( new Object[]{"ID", "学号", "姓名", "性别", "出生日期", "电话", "专业"}, 0); studentTable.setModel(tableModel); add(new JScrollPane(studentTable), BorderLayout.CENTER); // 南侧按钮区 JPanel buttonPanel = new JPanel(); buttonPanel.add(new JButton("添加")); buttonPanel.add(new JButton("修改")); buttonPanel.add(new JButton("删除")); buttonPanel.add(new JButton("刷新")); add(buttonPanel, BorderLayout.SOUTH); setVisible(true); } }

这个骨架把三块区域的职责分开:查询区管条件输入,表格区管数据展示,按钮区管操作入口。注意学生表格里“性别”这一列,界面上应该显示“男/女”,而数据库里存的是 1/2。这个映射不要写死在 TableModel 里,我一般放在实体类加一个 getGenderText() 方法,返回“男”或“女”,这样表格刷新代码里可以直接调用,不用在每一处刷新逻辑里重复判断。

到这里,表结构和界面轮廓已经有了,但查询按钮还是空的。下一步就是把 JDBC 访问层写好,让界面上的按钮真正能操作数据库。

3. 用 JDBC 把界面和数据库连起来:连接串配置与参数化 CRUD

3.1 JDBC 连接 MySQL:驱动加载与连接串参数说明

JDBC 连接是后面所有操作的地基,连接串写不对,后面全是白费。我通常把连接逻辑单独抽成一个 DatabaseUtil 类,每次需要连接时调静态方法即可。下面是完整写法。

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DatabaseUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_db" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8" + "&useUnicode=true"; private static final String USER = "root"; private static final String PASSWORD = "your_password"; static { try { // mysql-connector-java 8.x 的驱动类名 Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("MySQL驱动未找到,检查依赖或驱动类名"); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

连接串里这几个参数每一个都有明确用途,少写一个就是一类奇怪的坑。

参数作用不设置的后果
useSSL=false关闭本地开发环境的 SSL 加密新版驱动报 SSL 警告,部分环境连接超时
serverTimezone=Asia/Shanghai指定服务器时区MySQL 8 驱动直接抛时间区异常
characterEncoding=utf8指定字符编码查询出的中文变问号,写库也乱码
useUnicode=true配合 characterEncoding 生效部分驱动忽略字符编码参数

另一个高频问题是驱动类名。mysql-connector-java 5.x 用的类名是 com.mysql.jdbc.Driver,8.x 改成了 com.mysql.cj.jdbc.Driver。如果项目依赖是 8.x 却还在用旧类名,Class.forName 会直接抛 ClassNotFoundException。判断办法很简单:打开 jar 包看有没有 com.mysql.cj 目录,有就是 8.x,没有就是 5.x。这里的密码不要写死,我一般放在配置文件里读取,方便不同环境切换,避免每次换数据库都要改代码重新编译。

3.2 学生信息 CRUD:PreparedStatement 参数化查询完整代码

数据访问层是整套系统的核心,所有 SQL 都要走 PreparedStatement 参数绑定,不要用字符串拼接。下面是按学生管理场景写的完整 DAO,包含了查询全部、按关键字搜索、添加、修改、删除五个操作。

import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDao { public List<Student> findAll() { List<Student> list = new ArrayList<>(); String sql = "SELECT id, student_no, name, gender, birthday, phone, major FROM student"; try (Connection conn = DatabaseUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } catch (SQLException e) { e.printStackTrace(); } return list; } public List<Student> search(String keyword) { List<Student> list = new ArrayList<>(); String sql = "SELECT id, student_no, name, gender, birthday, phone, major FROM student " + "WHERE student_no LIKE ? OR name LIKE ?"; try (Connection conn = DatabaseUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public boolean addStudent(Student s) { String sql = "INSERT INTO student(student_no, name, gender, birthday, phone, major) " + "VALUES(?,?,?,?,?,?)"; try (Connection conn = DatabaseUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setInt(3, s.getGender()); ps.setDate(4, s.getBirthday() == null ? null : new java.sql.Date(s.getBirthday().getTime())); ps.setString(5, s.getPhone()); ps.setString(6, s.getMajor()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public boolean updateStudent(Student s) { String sql = "UPDATE student SET student_no=?, name=?, gender=?, birthday=?, phone=?, major=? WHERE id=?"; try (Connection conn = DatabaseUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStudentNo()); ps.setString(2, s.getName()); ps.setInt(3, s.getGender()); ps.setDate(4, s.getBirthday() == null ? null : new java.sql.Date(s.getBirthday().getTime())); ps.setString(5, s.getPhone()); ps.setString(6, s.getMajor()); ps.setInt(7, s.getId()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public boolean deleteStudent(int id) { String sql = "DELETE FROM student WHERE id=?"; try (Connection conn = DatabaseUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } private Student mapRow(ResultSet rs) throws SQLException { Student s = new Student(); s.setId(rs.getInt("id")); s.setStudentNo(rs.getString("student_no")); s.setName(rs.getString("name")); s.setGender(rs.getInt("gender")); s.setBirthday(rs.getDate("birthday")); s.setPhone(rs.getString("phone")); s.setMajor(rs.getString("major")); return s; } }

这段代码有几个值得反复看的点。所有 SQL 都用了 ? 占位符,参数通过 setString、setInt、setDate 传入,输入内容永远只会被当作数据,不会变成 SQL 结构的一部分,这是抵御 SQL 注入的最基本手段。try-with-resources 保证了 PreparedStatement、ResultSet 和 Connection 一定被关闭,不会因为某个异常分支漏掉 close 导致连接泄漏。Java 初学者最常见的问题就是只关了 Connection,漏了 ResultSet,结果跑一段时间数据库连接数直接打满。

日期转换这里特别容易写错:setDate 接收的是 java.sql.Date,而实体类里存的是 java.util.Date,如果直接传 s.getBirthday() 会编译报错。上面代码先判空再 new java.sql.Date(...),两个问题一起解决。单表操作用不到事务,每个方法自己提交就够了;等后面加了“删除学生同时删除成绩表”这种多表联动,再考虑手动控制 Connection 的 commit 和 rollback。

3.3 表格加载与刷新:从 ResultSet 到 JTable 的转换

DAO 写好之后,界面上的查询按钮终于能干活了。刷新表格的代码放在 MainFrame 里,核心逻辑是把 DAO 返回的 List 转成 JTable 能显示的表格行。

private void refreshTable() { String keyword = searchField.getText().trim(); List<Student> students = studentDao.search(keyword); tableModel.setRowCount(0); for (Student s : students) { tableModel.addRow(new Object[]{ s.getId(), s.getStudentNo(), s.getName(), s.getGender() == 1 ? "男" : "女", s.getBirthday(), s.getPhone(), s.getMajor() }); } }

setRowCount(0) 的作用是清空旧数据,然后每一行通过 addRow 加进去。如果不先清空,每次点击查询表格都会在后面追加数据,看起来就像数据重复了。searchField.getText().trim() 去掉了输入框里的首尾空格,避免用户在输入法状态下多打了空格导致查不到结果。性别列这里做了 1 转“男”、其他转“女”的显示映射,数据库里的原始值不会被改动,显示和存储分离,这是桌面应用里很值钱的一个习惯。

数据量不大时,每次刷新全量加载没有问题。但表格超过几千行,刷新就会开始卡顿,这个阶段要引入分页查询,SQL 里加 LIMIT 和 OFFSET,界面上再加翻页按钮。Swing 的 JTable 本身不适合承载几万行的数据,硬塞进去滚动都会掉帧。

4. 学生信息管理系统避坑指南:乱码、卡死与 SQL 注入

4.1 中文乱码:连接串少一个参数,界面全是问号

现象:添加到数据库里的学生姓名显示为“张??”或直接在 JTable 里显示成问号。

原因:三层都有嫌疑——数据库表字符集用了 latin1、JDBC 连接串少了 characterEncoding、IDE 项目文件编码不是 UTF-8。很多人只排查 Java 代码,却忽略了建表语句里的默认字符集。

解决:优先把三处统一成 UTF-8。第一点,建表语句指定 DEFAULT CHARSET=utf8mb4;第二点,连接串写全 characterEncoding=utf8 和 useUnicode=true;第三点,IDE 里把项目文件编码改成 UTF-8。如果数据库表已经建好且写入了乱码数据,先备份再改表字符集,或者直接删表重建,数据量不大时重建比转换省心。

注意:MySQL 的 utf8 实际上是 utf8mb3,遇到生僻字会丢数据,所以新表一律用 utf8mb4。

4.2 界面卡死:数据库查询放进了事件线程

现象:点击“查询”按钮后窗口变白、无法拖动,CPU 占用飙升,过几秒才恢复,数据量大时甚至直接卡到无响应。

原因:Swing 是单线程模型,所有界面事件都在事件分发线程里处理。查询按钮的监听器直接调 studentDao.search(),数据库查询阻塞了界面刷新,窗口就看起来像死了。

解决:用 SwingWorker 把数据库查询放到后台线程,查完再回到事件线程更新表格。下面是不卡界面的标准写法。

searchBtn.addActionListener(e -> { String keyword = searchField.getText().trim(); new SwingWorker<List<Student>, Void>() { @Override protected List<Student> doInBackground() { return studentDao.search(keyword); } @Override protected void done() { try { List<Student> students = get(); tableModel.setRowCount(0); for (Student s : students) { tableModel.addRow(new Object[]{...}); } } catch (Exception ex) { JOptionPane.showMessageDialog(MainFrame.this, "查询失败:" + ex.getMessage(), "错误", JOptionPane.ERROR_MESSAGE); } } }.execute(); });

doInBackground 在后台线程执行查询,done 方法会在事件线程被回调,所以这里可以直接更新 tableModel。get() 方法会抛出后台线程里产生的异常,界面能弹出错误提示,而不是在所有操作都完成后静默失败。如果不用 SwingWorker,用 new Thread 加 SwingUtilities.invokeLater 也能实现,但异常的传递、线程的生命周期管理都要自己写,代码会散。这个坑在几百行数据时不一定暴露,等数据上万才会翻车,所以一开始就按异步写最稳。

4.3 密码存储:明文入库与 SQL 拼接是两条红线

现象:登录校验写成 “SELECT * FROM user WHERE username='” + username + "' AND password='" + password + "'”,用户名输入 admin' OR '1'='1 就能绕过密码直接登录;数据库里密码字段直接明文可见。

原因:字符串拼接把输入内容变成了 SQL 结构的一部分,这是 SQL 注入的根源;密码明文存放则是把用户信任丢在数据库里不管。

解决:第一步,登录查询改成 PreparedStatement 参数绑定;第二步,密码不能明文入库。这个级别的系统常用做法是加盐哈希:注册时生成随机盐值,把“密码 + 盐”做 MD5 或 SHA-256,库里存盐值和哈希结果;登录时用同样的盐和密码重新计算哈希,比对是否一致。MD5 本身已不推荐用于新系统的加密场景,但作为课设或内部工具的安全过渡,加盐之后的强度远比明文高。正式项目直接用 BCrypt 或 PBKDF2,不要自己写加密逻辑。

4.4 驱动版本与 MySQL 版本不匹配:ClassNotFound 和 SSL 警告

现象:项目依赖 mysql-connector-java 5.1.x,连接 MySQL 8 报 ClassNotFoundException: com.mysql.cj.jdbc.Driver;或者报 “Public Key Retrieval is not allowed”。

原因:旧驱动不认识新驱动类名;MySQL 8 默认的认证插件 caching_sha2_password 对旧驱动兼容性差,连接时密钥检索被拒绝。

解决:驱动版本和 MySQL 版本要匹配。连接 MySQL 8 一般用 mysql-connector-java 8.x,驱动类名写 com.mysql.cj.jdbc.Driver,连接串加 allowPublicKeyRetrieval=true 和 useSSL=false。如果工程里同时存在新旧两个版本的驱动,会互相干扰,删掉旧依赖重新构建即可。

5. 从“能跑”到“能交差”:校验、验证与交付前自查

如果只是想练手,CRUD 写完就可以收工了。但真要交付给别人用,还有几个细节值得补上。必填校验不能只在界面弹窗,DAO 层也要有防线。比如学号为空时添加学生,数据库会报字段不能为空,但界面给用户看的应该是中文提示。我一般会在调用 addStudent 之前做一次规则校验,还能顺带避免学号重复这样的业务错误。

public String validateStudent(Student s) { if (s.getStudentNo() == null || s.getStudentNo().trim().isEmpty()) { return "学号不能为空"; } if (s.getName() == null || s.getName().trim().isEmpty()) { return "姓名不能为空"; } StudentDao dao = new StudentDao(); if (dao.existsByStudentNo(s.getStudentNo(), s.getId())) { return "学号已存在"; } return null; }

existsByStudentNo 的 SQL 很简单:SELECT COUNT(*) FROM student WHERE student_no=?,修改场景下还要过滤掉当前行自身,所以带上 id 作为排除条件。这个校验方法在添加和修改时都能复用,返回 null 表示校验通过,否则返回给 JOptionPane 直接展示。

交付前还有一个自查习惯:准备一批边界数据在本地跑一遍——空字符串学号、重复学号、超长姓名、特殊单引号、日期为 null,每一条都应该有明确提示而不是异常堆栈。把 SQL 先在命令行里跑通再写 Java,能省掉很多排查时间。

如果还想往上走一步,可以给系统加学生状态字段(在读/休学/毕业)、专业字典表、操作日志表,这些都是从“作业”到“工具”的常见演进方向。我自己吃过亏:有一次改动查询逻辑,只在 Java 层改了字段名没在 MySQL 里先验证,结果查出来全是 null,排错了半天才发现是 SQL 里字段名写错了。后来养成了习惯,所有 SQL 都先单独跑一遍再贴进代码。这个项目不复杂,但把校验和交付自查做扎实,能学到的东西远比敲一遍 CRUD 多,希望帮到你。

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

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

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

立即咨询