简介一套基于Java Swing与MySQL的学生信息管理系统完整项目面向正在学习Java桌面应用开发、需要完成课程设计或毕业设计的在校学生也适合入门者观摩完整小项目。系统覆盖学生信息录入、查询、修改、删除与列表展示采用三层架构将界面、业务逻辑和数据库访问分离能够清楚演示Swing控件、事件监听、JDBC操作及SQL语句在真实项目中的配合方式。压缩包共58个文件以21个Java源码和31个class文件为主另含1个建表SQL脚本、1个运行依赖jar包和工程配置文件整体约2.01MB目录结构清晰便于导入IDE运行并对照源码学习。目前已有5100余人学习下载使用该资源可快速搭建一个可运行的学生管理桌面应用也能为课程报告或后续扩展成更完整的管理系统提供可复用的基础框架。读者可按源码与编译后文件对照理解Swing界面搭建和MySQL数据表设计在实际编码中逐步熟悉桌面应用开发流程。1. JavaSwingMySQL 实现学生信息管理系统为什么这个老题目还在不断被做“JavaSwingMySQL 实现学生信息管理系统”这个题目在课程设计和练手项目里出现频率一直居高不下。它不追求前端工程化也不依赖分布式中间件却能把 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) ) ENGINEInnoDB 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 把界面和数据库连起来连接串配置与参数化 CRUD3.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 ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 useUnicodetrue; 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); } }连接串里这几个参数每一个都有明确用途少写一个就是一类奇怪的坑。参数作用不设置的后果useSSLfalse关闭本地开发环境的 SSL 加密新版驱动报 SSL 警告部分环境连接超时serverTimezoneAsia/Shanghai指定服务器时区MySQL 8 驱动直接抛时间区异常characterEncodingutf8指定字符编码查询出的中文变问号写库也乱码useUnicodetrue配合 characterEncoding 生效部分驱动忽略字符编码参数另一个高频问题是驱动类名。mysql-connector-java 5.x 用的类名是 com.mysql.jdbc.Driver8.x 改成了 com.mysql.cj.jdbc.Driver。如果项目依赖是 8.x 却还在用旧类名Class.forName 会直接抛 ClassNotFoundException。判断办法很简单打开 jar 包看有没有 com.mysql.cj 目录有就是 8.x没有就是 5.x。这里的密码不要写死我一般放在配置文件里读取方便不同环境切换避免每次换数据库都要改代码重新编译。3.2 学生信息 CRUDPreparedStatement 参数化查询完整代码数据访问层是整套系统的核心所有 SQL 都要走 PreparedStatement 参数绑定不要用字符串拼接。下面是按学生管理场景写的完整 DAO包含了查询全部、按关键字搜索、添加、修改、删除五个操作。import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDao { public ListStudent findAll() { ListStudent 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 ListStudent search(String keyword) { ListStudent 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(); ListStudent 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 CHARSETutf8mb4第二点连接串写全 characterEncodingutf8 和 useUnicodetrue第三点IDE 里把项目文件编码改成 UTF-8。如果数据库表已经建好且写入了乱码数据先备份再改表字符集或者直接删表重建数据量不大时重建比转换省心。注意MySQL 的 utf8 实际上是 utf8mb3遇到生僻字会丢数据所以新表一律用 utf8mb4。4.2 界面卡死数据库查询放进了事件线程现象点击“查询”按钮后窗口变白、无法拖动CPU 占用飙升过几秒才恢复数据量大时甚至直接卡到无响应。原因Swing 是单线程模型所有界面事件都在事件分发线程里处理。查询按钮的监听器直接调 studentDao.search()数据库查询阻塞了界面刷新窗口就看起来像死了。解决用 SwingWorker 把数据库查询放到后台线程查完再回到事件线程更新表格。下面是不卡界面的标准写法。searchBtn.addActionListener(e - { String keyword searchField.getText().trim(); new SwingWorkerListStudent, Void() { Override protected ListStudent doInBackground() { return studentDao.search(keyword); } Override protected void done() { try { ListStudent 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 11 就能绕过密码直接登录数据库里密码字段直接明文可见。原因字符串拼接把输入内容变成了 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连接串加 allowPublicKeyRetrievaltrue 和 useSSLfalse。如果工程里同时存在新旧两个版本的驱动会互相干扰删掉旧依赖重新构建即可。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 多希望帮到你。本文还有配套的精品资源点击获取