☰
Java+Swing+MySQL学生信息管理系统:从建库到打包全流程实战
2026/9/28 8:04:17 网站建设 项目流程

简介:这是一份基于Java+Swing+MySQL构建的学生信息管理系统完整项目,适合Java初学者、课程设计使用者以及需要快速搭建桌面管理应用的学习者。系统采用三层架构,涵盖数据录入、查询、修改、删除与展示等核心功能,并包含登录验证与权限控制模块,可帮助读者理解面向对象编程、Swing界面设计与数据库交互的完整流程。压缩包共58个文件,以Java源码、编译后的class文件为主,附带SQL建表脚本、项目配置文件和依赖jar包,整体大小约2.01MB,结构清晰便于导入IDE运行和二次开发。已有5132人浏览学习,适合对照源码练习数据库增删改查操作,也可作为毕业设计或课设的参考资料。

1. 学生信息管理系统为什么要用 Java+Swing+MySQL 这套组合

每到期末和毕设季,Java+Swing+MySQL 实现学生信息管理系统会出现在大量课程设计题目里。这个项目看起来很传统,却是最稳妥的练手方向:Swing 负责桌面界面,MySQL 存数据,JDBC 做桥梁,三样东西都是 Java 后端开发绕不开的基础。相比 Web 项目,它不需要配置 Tomcat,不需要处理前端跨域,逻辑链路短,调通一条数据流的成本低很多。

这篇文章把整个系统的拆解过程完整讲一遍:从 MySQL 建库建表,到 JDBC 数据访问层,再到 Swing 登录界面和 JTable 增删改查,最后落在验证打包和一串真实踩坑记录上。新手能照着逐层跑通,已经写过类似系统的读者,重点看第 5 章的边界问题和第 6 章的打包验证,这两部分是课设答辩最容易丢分的地方。

2. 技术选型和环境准备:先把地基打稳再动代码

2.1 为什么不是 JavaFX,也不是 MyBatis

Swing 到今天看起来是有些过时,但它有一个无法替代的优势:JDK 自带,不需要额外引入 UI 依赖,写完就能跑。JavaFX 虽然界面更现代,配置 Scene Builder、处理 FXML 对新手来说是不小的负担,放在课程设计里反而拖慢进度。做这个需求,Swing 是最低成本的路径。

数据访问层上,常见做法是直接用 JDBC 手写 DAO,不引入 MyBatis。原因很实在:这个系统的表就两三张,SQL 语句也就那几条,引入 MyBatis 反而要处理 XML 映射、MyBatis 配置文件这些额外的东西。但有一件事必须做——用 PreparedStatement,不要用 Statement 拼 SQL。这不仅是为了防止 SQL 注入,更是为了避免姓名里带单引号时直接报语法错误。这个点很多系统都会忽略,后面避坑章节专门展开。

2.2 JDK 版本和 MySQL 版本怎么配才不会翻车

很多人在动手前卡在环境上,其实就两个版本关键点:JDK 建议用 8 或 11,对应javax.swing包原生存在,不需要额外装;MySQL 用 5.7 或 8.0 都行,但 JDBC 驱动必须和数据库版本对应。这是最容易翻车的第一个坑——MySQL 5.x 用com.mysql.jdbc.Driver,MySQL 8.x 必须用com.mysql.cj.jdbc.Driver,两者驱动的类名是不同的。

启动 MySQL 前,先确认服务已经跑起来。Windows 下在命令行执行:

net start mysql mysql -u root -p

能进入 mysql 命令行界面就说明服务正常。如果执行 mysql 命令提示找不到,多半是没配环境变量,把 MySQL 安装目录下的 bin 路径加进 PATH 即可。之后记得确认一下 root 密码和端口,默认 3306,如果你本机装过多个版本的 MySQL,端口可能被改成 3307 或 3308,这个值后面要写进 JDBC 连接串,写错会报 connection refused。

2.3 建立数据库和 student 表:字段设计决定后期改代码的工作量

建库建表是整个系统里最值得先做的一步。我的建议是直接用命令行或 MySQL Workbench 执行 SQL,把表结构和几条测试数据先准备好,再开始写 Java 代码,这样后面调 JDBC 时能立刻确认是代码问题还是数据问题。

CREATE DATABASE IF NOT EXISTS student_info DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE student_info; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, stu_no VARCHAR(20) NOT NULL, name VARCHAR(50) NOT NULL, gender VARCHAR(4) DEFAULT '男', age INT, major VARCHAR(50), phone VARCHAR(20), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_stu_no (stu_no) ); CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL ); INSERT INTO admin (username, password) VALUES ('admin', '123456'); INSERT INTO student (stu_no, name, gender, age, major, phone) VALUES ('2023001', '张伟', '男', 20, '计算机科学与技术', '13800138000'), ('2023002', '李娜', '女', 21, '软件工程', '13900139000');

字符集用utf8mb4而不是utf8,因为utf8在 MySQL 里最多存 3 字节,遇到生僻字或 emoji 会报错,虽然学生表里大概率用不到,但建表时顺手写对没有坏处。stu_no加了唯一索引,这是防止同一学号被重复录入的数据库级防线,比只在 Java 代码里判断要可靠。admin表用来做登录验证,课程设计阶段密码不加密可以接受,但最好在代码注释里标注这里是明文存储,仅作演示用。

数据库里所有 SQL 语句执行成功后,用SHOW TABLES;和SELECT * FROM student;确认数据在,这一步过了,后面 Java 代码的调试就会顺畅很多。

3. JDBC 数据访问层:写一个能直接用的 DBUtil 和 DAO

3.1 连接工具类:连接串里的每个参数都是经验值

DBUtil 是整个系统的地基。很多人的代码在别人电脑上跑不通,问题就出在连接串的参数上。我一般这样写:

import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_info" + "?useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&characterEncoding=utf8" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

参数逐个说明:useSSL=false是关闭 SSL 加密,本地开发不需要加密传输,不加这个参数 MySQL 8 会输出一大段 SSL 警告;serverTimezone=Asia/Shanghai指定时区,不指定时 MySQL 8 会直接报时区错误,这是最常见的启动失败原因;characterEncoding=utf8保证中文不会乱码,配套数据库建表时的utf8mb4,从源头到传输全链路都是 UTF-8;allowPublicKeyRetrieval=true只在 MySQL 8 下需要,不配置会报Public Key Retrieval is not allowed。

注意Class.forName这段写进了静态代码块,类加载时就会执行,确保 Driver 只注册一次。如果使用 MySQL 5.x,把驱动类名改成com.mysql.jdbc.Driver,驱动 jar 也从 5.x 的版本里找。

3.2 StudentDAO:增删改查全部用 PreparedStatement

DAO 层就是四个方法,按课程设计标准来写,每个方法都接收参数、返回结果,界面层只管调用,不直接拼 SQL。

import java.sql.*; import java.util.ArrayList; import java.util.List; public class StudentDAO { public boolean addStudent(Student s) { String sql = "INSERT INTO student (stu_no, name, gender, age, major, phone) " + "VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStuNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getMajor()); ps.setString(6, s.getPhone()); 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 = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public boolean updateStudent(Student s) { String sql = "UPDATE student SET stu_no=?, name=?, gender=?, age=?, major=?, phone=? " + "WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getStuNo()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setString(5, s.getMajor()); ps.setString(6, s.getPhone()); ps.setInt(7, s.getId()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } public List<Student> queryAll() { String sql = "SELECT * FROM student ORDER BY id"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.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> queryByKeyword(String keyword) { String sql = "SELECT * FROM student WHERE stu_no LIKE ? OR name LIKE ? ORDER BY id"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.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; } private Student mapRow(ResultSet rs) throws SQLException { Student s = new Student(); s.setId(rs.getInt("id")); s.setStuNo(rs.getString("stu_no")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setMajor(rs.getString("major")); s.setPhone(rs.getString("phone")); return s; } }

Java 7 的 try-with-resources 写法在这里很关键,连接、PreparedStatement、ResultSet 都实现了 AutoCloseable,方法结束自动关闭,不用写 finally 里那一堆 null 判断。这套写法值得背下来,面试问 JDBC 资源管理时也能直接用上。queryByKeyword里的 LIKE 模糊查询是课设里的加分项,注意这里的%是拼在参数里的,SQL 语句本体仍然是参数化查询,没有注入风险。

3.3 关于数据库连接池:课设阶段真不用上

有些同学看到热门技术里经常出现mysql 数据库连接池,就想往课设里塞一个 Druid 或 HikariCP。我的建议是:除非你的系统里有上千条并发查询的演示场景,否则不要加。连接池的收益在高并发下才明显,课设系统单机单用户,DriverManager.getConnection足够。加了连接池反而要把配置文件、池参数、jar 依赖都引进来,出了问题排查成本更高。

如果确实想体现这块能力,正确的做法是在 DBUtil 里预留接口,不改变 DAO 层的调用方式。把getConnection()内部实现换掉,DAO 代码一行不用动,这个设计本身就比硬塞一个连接池更能体现对分层结构的理解。

4. Swing 界面层:从登录框到主窗体的完整实现

4.1 登录逻辑:admin 表校验和密码输入框

登录界面建议单独做成一个LoginFrame类,主方法直接从这里启动。界面布局用绝对定位还是布局管理器,课设阶段选 FlowLayout 或 GridBagLayout 都行。密码框必须用JPasswordField,不能用 JTextField,这是基本的安全意识,答辩时老师会看。校验逻辑写在一个登录按钮的 ActionListener 里:

import javax.swing.*; import java.awt.*; import java.sql.*; public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { setTitle("学生信息管理系统 - 登录"); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setResizable(false); JPanel panel = new JPanel(new GridLayout(3, 2, 8, 8)); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); panel.add(new JLabel("用户名:")); usernameField = new JTextField(); panel.add(usernameField); panel.add(new JLabel("密码:")); passwordField = new JPasswordField(); panel.add(passwordField); JButton loginBtn = new JButton("登 录"); panel.add(loginBtn); JButton resetBtn = new JButton("重 置"); panel.add(resetBtn); setContentPane(panel); loginBtn.addActionListener(e -> doLogin()); resetBtn.addActionListener(e -> { usernameField.setText(""); passwordField.setText(""); }); } private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()).trim(); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } if (checkAdmin(username, password)) { new MainFrame().setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } private boolean checkAdmin(String username, String password) { String sql = "SELECT * FROM admin 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()) { return rs.next(); } } catch (SQLException e) { e.printStackTrace(); return false; } } public static void main(String[] args) { SwingUtilities.invokeLater(() -> new LoginFrame().setVisible(true)); } }

两个细节:new String(passwordField.getPassword())是因为 JPasswordField 的 getPassword 返回的是 char 数组,不直接给 String;登录成功后dispose()关掉登录窗口,而不是setVisible(false),这样不会残留一个隐藏窗口。SwingUtilities.invokeLater的作用是把 UI 创建动作放到事件分发线程上,这是 Swing 的线程模型要求,虽然不是每次都能察觉到区别,但养成本来就应该这样写的习惯没有坏处。

4.2 主窗体:JTable + DefaultTableModel 的实现和刷新

主窗体是整个系统里代码量最大的部分,核心是一个嵌在JScrollPane里的 JTable,配合上方的按钮栏,完成增删改查和刷新。这里有个新手很容易踩的设计问题:直接改 JTable 里显示的数据,改完发现表格不更新。正确做法是每次操作后重新构建表格模型,把 DAO 查出来的数据重新灌进去。

import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.util.List; public class MainFrame extends JFrame { private JTable table; private DefaultTableModel tableModel; private JTextField keywordField; private StudentDAO dao = new StudentDAO(); public MainFrame() { setTitle("学生信息管理系统"); setSize(800, 500); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10)); JButton addBtn = new JButton("新增"); JButton editBtn = new JButton("修改"); JButton deleteBtn = new JButton("删除"); JButton refreshBtn = new JButton("刷新"); keywordField = new JTextField(12); JButton searchBtn = new JButton("查询"); topPanel.add(addBtn); topPanel.add(editBtn); topPanel.add(deleteBtn); topPanel.add(refreshBtn); topPanel.add(new JLabel("关键字:")); topPanel.add(keywordField); topPanel.add(searchBtn); add(topPanel, BorderLayout.NORTH); tableModel = new DefaultTableModel(new Object[]{"ID", "学号", "姓名", "性别", "年龄", "专业", "电话"}, 0); table = new JTable(tableModel); add(new JScrollPane(table), BorderLayout.CENTER); addBtn.addActionListener(e -> showEditDialog(null)); editBtn.addActionListener(e -> { int row = table.getSelectedRow(); if (row < 0) { JOptionPane.showMessageDialog(this, "请先选中要修改的行"); return; } showEditDialog(getStudentFromRow(row)); }); deleteBtn.addActionListener(e -> doDelete()); refreshBtn.addActionListener(e -> refreshTable()); searchBtn.addActionListener(e -> { String keyword = keywordField.getText().trim(); if (keyword.isEmpty()) { refreshTable(); } else { refreshTable(dao.queryByKeyword(keyword)); } }); refreshTable(); } private void refreshTable() { refreshTable(dao.queryAll()); } private void refreshTable(List<Student> list) { tableModel.setRowCount(0); for (Student s : list) { tableModel.addRow(new Object[]{ s.getId(), s.getStuNo(), s.getName(), s.getGender(), s.getAge(), s.getMajor(), s.getPhone() }); } } private Student getStudentFromRow(int row) { Student s = new Student(); s.setId((Integer) tableModel.getValueAt(row, 0)); s.setStuNo((String) tableModel.getValueAt(row, 1)); s.setName((String) tableModel.getValueAt(row, 2)); s.setGender((String) tableModel.getValueAt(row, 3)); s.setAge((Integer) tableModel.getValueAt(row, 4)); s.setMajor((String) tableModel.getValueAt(row, 5)); s.setPhone((String) tableModel.getValueAt(row, 6)); return s; } private void doDelete() { int row = table.getSelectedRow(); if (row < 0) { JOptionPane.showMessageDialog(this, "请先选中要删除的行"); return; } int id = (Integer) tableModel.getValueAt(row, 0); int confirm = JOptionPane.showConfirmDialog(this, "确认删除该学生记录吗?", "确认", JOptionPane.YES_NO_OPTION); if (confirm == JOptionPane.YES_OPTION && dao.deleteStudent(id)) { refreshTable(); JOptionPane.showMessageDialog(this, "删除成功"); } } private void showEditDialog(Student student) { new StudentEditDialog(this, student, dao, this::refreshTable).setVisible(true); } }

getSelectedRow返回 -1 时先弹出提示而不是直接执行,这是基本的交互保护。表格刷新用setRowCount(0)清空再一行行 addRow,虽然简单粗暴,但对于几百条数据量的课设场景完全够用,不需要引入 TableModel 的事件通知机制。刷新后数据从数据库读一遍,保证看到的是真实落库结果,不会出现界面和数据源不同步的问题。

getStudentFromRow把表格当前行的数据封装回 Student 对象,是为了修改时把原值带进编辑框,这个操作是很多简化版课设没有做但很实用的设计,用户不用重新输入全部字段。

4.3 编辑对话框:新增和修改复用同一个窗体

新增和修改可以复用同一个对话框类,传入的 student 参数为 null 表示新增,非 null 表示修改。这个复用的做法简化了代码量,也让逻辑更集中。

import javax.swing.*; import java.awt.*; public class StudentEditDialog extends JDialog { private JTextField stuNoField, nameField, ageField, majorField, phoneField; private JComboBox<String> genderBox; private StudentDAO dao; private Student student; private Runnable onSuccess; public StudentEditDialog(Frame owner, Student student, StudentDAO dao, Runnable onSuccess) { super(owner, student == null ? "新增学生" : "修改学生", true); this.student = student; this.dao = dao; this.onSuccess = onSuccess; setSize(320, 300); setLocationRelativeTo(owner); JPanel form = new JPanel(new GridLayout(6, 2, 6, 6)); form.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15)); form.add(new JLabel("学号:")); stuNoField = new JTextField(); form.add(stuNoField); form.add(new JLabel("姓名:")); nameField = new JTextField(); form.add(nameField); form.add(new JLabel("性别:")); genderBox = new JComboBox<>(new String[]{"男", "女"}); form.add(genderBox); form.add(new JLabel("年龄:")); ageField = new JTextField(); form.add(ageField); form.add(new JLabel("专业:")); majorField = new JTextField(); form.add(majorField); form.add(new JLabel("电话:")); phoneField = new JTextField(); form.add(phoneField); JButton saveBtn = new JButton("保存"); JButton cancelBtn = new JButton("取消"); form.add(saveBtn); form.add(cancelBtn); setContentPane(form); if (student != null) { stuNoField.setText(student.getStuNo()); nameField.setText(student.getName()); genderBox.setSelectedItem(student.getGender()); ageField.setText(String.valueOf(student.getAge())); majorField.setText(student.getMajor()); phoneField.setText(student.getPhone()); } saveBtn.addActionListener(e -> doSave()); cancelBtn.addActionListener(e -> dispose()); } private void doSave() { String stuNo = stuNoField.getText().trim(); String name = nameField.getText().trim(); String ageText = ageField.getText().trim(); if (stuNo.isEmpty() || name.isEmpty() || ageText.isEmpty()) { JOptionPane.showMessageDialog(this, "学号、姓名、年龄不能为空"); return; } int age; try { age = Integer.parseInt(ageText); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, "年龄必须是数字"); return; } boolean ok; if (student == null) { Student s = new Student(); s.setStuNo(stuNo); s.setName(name); s.setGender((String) genderBox.getSelectedItem()); s.setAge(age); s.setMajor(majorField.getText().trim()); s.setPhone(phoneField.getText().trim()); ok = dao.addStudent(s); } else { student.setStuNo(stuNo); student.setName(name); student.setGender((String) genderBox.getSelectedItem()); student.setAge(age); student.setMajor(majorField.getText().trim()); student.setPhone(phoneField.getText().trim()); ok = dao.updateStudent(student); } if (ok) { JOptionPane.showMessageDialog(this, student == null ? "新增成功" : "修改成功"); if (onSuccess != null) onSuccess.run(); dispose(); } else { JOptionPane.showMessageDialog(this, "保存失败,请检查输入或数据库连接"); } } }

年龄输入用 try-catch 包住Integer.parseInt,这一步把非数字输入挡在数据库之外,数据库层面不会收到错误类型的参数。新增和修改的差异只在 Student 对象有没有 id,onSuccess回调在保存成功后触发主界面的refreshTable,不需要两个窗体之间互相持有引用。

这里需要补一个 Student 实体类,字段对应数据库表,再写上 getter/setter,这是连接数据库和界面的数据载体。类本身不复杂,代码就省略了,但不要省略这个类直接拿散装字符串传参,那会让后面的维护变得很别扭。

4.4 业务代码的分层边界:DAO、Service、UI 各管一段

课程设计里最常见的坏味道,是把 SQL、业务逻辑、界面事件全写在一个按钮监听器里。这个系统的规模不需要引入 Spring,但分层思想可以把代码组织得清楚:UI 层只处理界面展示和用户交互,调用 DAO 拿数据;DAU 层只负责 SQL 和结果集映射;中间如果有业务规则,再插一个 Service 层。

比如查重这个逻辑,新增学生时如果学号已经存在,数据库的唯一索引会直接报错,但报错信息不友好。更好的做法是在 DAO 里加一个isStuNoExists(String stuNo)方法,界面保存前先查一次,给出"该学号已存在"的明确提示。这一层业务判断放 DAO 还是 Service,按你自己的习惯,但不要堆在按钮监听器里。这个分层习惯比这个项目本身更值钱,后面接触 Java Web 和 Spring Boot,你会发现同样的结构到处都是。

5. 避坑与排查:5 个让课设翻车的经典问题

5.1 启动报错:Public Key Retrieval is not allowed

现象:程序启动时第一次查询数据库就抛异常,异常信息里包含Public Key Retrieval is not allowed。

原因:MySQL 8 默认使用 caching_sha2_password 认证插件,在 SSL 关闭的情况下,客户端第一次连接时需要通过 RSA 公钥加密传输密码,而连接串没允许客户端自动获取公钥。

解决:JDBC 连接串上追加allowPublicKeyRetrieval=true&useSSL=false,两个参数一起使用。只加 allowPublicKeyRetrieval 不关 SSL 也能解决,但本地开发建议两个都加上,省去 SSL 握手的额外开销。如果加了参数还报同样的错,确认一下修改后有没有重启程序,驱动类是类加载时读取的,有些 IDE 需要 clean 一下再跑。

5.2 明明建了库,程序却报 database 不存在

现象:代码里 URL 写的jdbc:mysql://localhost:3306/student_info,报错提示Unknown database 'student_info',但命令行SHOW DATABASES能看到这个库。

原因:最常见的是建库语句没执行成功,或者是大小写不一致。MySQL 在 Linux 下对数据库名和表名的大小写敏感,Windows 下不敏感,课程设计如果跨系统演示,这个问题会突然冒出来。

解决:SHOW DATABASES确认库的实际名称,逐个字符核对。另外确认建库时用了USE student_info;,并且建表语句是在这个库下执行的。如果表也建在别的库下面,查询时同样会报 table doesn't exist。统一用小写库名和表名,可以少踩很多跨平台的问题。

5.3 中文乱码:插入和查询显示问号

现象:通过 Java 程序插入的中文,在 MySQL 命令行里显示为??;反过来,命令行里插入的中文,在 Swing 表格里显示为乱码。

原因:这个有两层。第一层是 JDBC 连接串缺characterEncoding=utf8,导致传输层用了默认的 latin1;第二层是数据库或表的字符集不是 utf8mb4。

解决:先用SHOW CREATE TABLE student;看表的默认字符集,如果不是 utf8mb4,执行ALTER TABLE student CONVERT TO CHARACTER SET utf8mb4;。然后确认连接串里带了characterEncoding=utf8。两层都改完后,删除表里已有的乱码数据重新插入一遍,乱码数据本身已经是坏字节,改完字符集后不会自动变好。这个血泪经验值得记住:字符集问题一定要在写入之前确认,数据一旦写坏,只能删掉重来。

5.4 Swing 界面点击按钮后卡住不动

现象:点击"刷新"或"新增"后界面白屏,窗口无响应,过很久才恢复,或者一直卡住。

原因:Swing 是单线程模型,所有 UI 更新必须在事件分发线程(EDT)上执行。如果在事件监听器里执行了耗时操作,比如数据库查询、大文件读写,EDT 被阻塞,界面就卡住了。

解决:课设场景数据量小,通常不会触发这个问题。但如果演示时数据量到了几千条,或者查询语句写得低效,卡顿就会出现。推荐的方案是给耗时操作单独开线程,再把结果通过SwingUtilities.invokeLater交回 EDT 更新界面:

Thread queryThread = new Thread(() -> { List<Student> list = dao.queryAll(); SwingUtilities.invokeLater(() -> refreshTable(list)); }); queryThread.start();

这块属于加分项,不写不影响功能,写了就能在答辩时解释 Swing 的线程模型。核心思路是:DAO 的数据库操作放后台线程,界面的 tableModel 更新放回 EDT。

5.5 运行环境换到别的电脑,数据库连不上

现象:把项目打包带去演示,换一台电脑运行,报Communications link failure或连接超时。

原因:连接串里写死的localhost:3306只在本机有效。换电脑演示时,如果那台机器没装 MySQL,或者端口不一致,连接必定失败。这是课设答辩最典型的翻车现场。

解决:提前准备两个方案。方案一,演示机提前装好 MySQL,导入建表脚本,改一下 DBUtil 里的密码就能跑;方案二,不想装 MySQL,把数据库文件拷过去也不行,MySQL 不能这样迁移。最稳妥的做法是:答辩前一晚在演示机上从头到尾跑一遍建库脚本和程序,确认能通。连接串里把localhost改成127.0.0.1在某些环境下可以绕过 IPv6 解析问题,如果出现连接超时而 MySQL 服务正常,值得试一下。

6. 打包成可执行 jar 和完整验证:从代码到能演示的最后一公里

写完了代码,下一步是把自己这台机器上的工程变成一个能带走的可执行 jar。这里介绍没有 Maven 的情况,直接在 IDE 里操作:IntelliJ IDEA 里打开 Project Structure,选择 Artifacts,添加一个 JAR,类型选 From modules with dependencies,主类选LoginFrame。构建时 IDEA 会把项目依赖的 mysql-connector jar 一起打进去。

如果构建后运行报找不到主类,多半是 Manifest 文件里的 Main-Class 没配对。可以手动写一个 Manifest,注意最后一行必须是空行,这是 Java 打包的老规矩,但这算比较少见的问题。Maven 用户可以用 assembly 插件打 fat jar,这一步跳过不写,直接执行mvn package就行。

运行验证按三层来做。第一层验证是数据库连接,单独写一个测试 main 方法只调DBUtil.getConnection(),能拿到连接再继续,不要跳这步。第二层验证是 DAO 的增删改查,写一个命令行测试类,依次调用新增、查询、修改、删除,每步输出结果确认。这两层跑通后,再启动LoginFrame做界面验证,此时如果出问题,问题一定在界面层,不需要回头查数据库。

验证用例清单建议按这个顺序执行:

  • 登录:输错密码有提示,输对密码能进去
  • 新增:填完整信息保存成功,表里出现新记录
  • 新增(异常):学号重复时给出友好提示,而不是程序报错
  • 修改:选中一行改姓名和专业,保存后表格刷新
  • 删除:选中一行删除,有确认框,删除后表格刷新
  • 查询:输入关键字查学号和姓名,能模糊匹配
  • 刷新:清空关键字后点刷新,恢复全表显示

每个用例跑完,去 MySQL 命令行用SELECT确认数据真的写进了数据库,这能同时验证程序和数据库的一致性。

最后一章给一个实战技巧:给系统加一个启动画面。Swing 有一个SplashScreen类,可以用来在程序加载时显示一张图,对课设答辩观感提升不小。做法是在 Manifest 里配SplashScreen-Image: splash.png,把图片放在 jar 同级目录,启动时 JVM 会自动加载。这个功能不复杂,但有这个细节,整个系统给人的完成度会高一个档次。

回看这套做法,我个人的习惯是把验证脚本留在工程里,不要删掉。下次改别的功能,改完先跑一遍测试类再碰界面,能省下大量反复点按钮的时间。这套流程不一定是最快的,但一定是最稳的。希望帮到你。

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

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

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

立即咨询