简介这是一套面向高校计算机相关专业课程设计场景的实验室管理系统完整源码包采用Java Swing构建带图形界面的桌面应用并配套数据库文件适合正在做课程设计、毕业设计或需要Java桌面项目练手的同学参考与二次开发。系统按功能划分为文件、维修管理、管理员助理信息、实验室课程查询预约、系统设置与帮助信息六大模块其中维修管理细分为设备报修、故障表、设备状况登记与运行状况查询管理员助理信息则包含楼层分工、管理规章制度与值日排班表业务逻辑贴近真实实验室日常运维。压缩包共1344个文件以htm页面、class字节码、gif与jpg图片、java源码为主另含js脚本、dll动态库、jar包、css样式、wav音频以及mdf、ldf数据库文件和sql脚本整体约6.35MB源码、资源与数据库结构齐全。目前已有123人学习下载可帮助读者快速理解Swing界面布局、事件处理与数据库连接方式并在此基础上完成功能扩展与答辩准备。1. 从一份课程设计说起Java Swing 实验室管理系统到底解决什么问题每年毕业季实验室管理相关的课程设计都是热门选题。原因很直接高校实验室的设备预约、耗材领用、人员登记、使用记录长期靠纸质表格和 Excel 维护数据分散、查询困难、权限混乱。用 Java Swing 搭一个带 GUI 界面的实验室管理系统既能覆盖数据库增删改查的完整链路又能展示桌面端界面开发能力是课程设计里少见的“业务真实、技术闭环”的题目。这份源码加数据库的打包方案核心价值在于它把实验室管理中最常见的四类操作——设备信息录入、预约记录管理、用户权限区分、数据统计查询——用 Swing 组件和 JDBC 串了起来。适合正在做课程设计的学生、刚接触 Java 桌面开发的新手以及需要快速搭一个内部管理工具原型的一线工程师。你拿到手后最需要搞清楚的是数据库表怎么设计、Swing 界面怎么和 JDBC 对接、权限控制放在哪一层。下面按实际落地顺序拆开讲。2. 数据库设计五张表撑起实验室管理的增删改查2.1 为什么是这五张表实验室管理系统的数据模型不复杂但表设计不合理后面写 Swing 界面时会反复返工。常见做法是围绕“人、设备、预约、记录、权限”五个实体建表。我一般会先画一遍 ER 图确认外键关系再动手写 SQL。核心表结构如下表名用途关键字段关联关系lab_user用户信息id, username, password, role被预约表、记录表引用lab_device设备信息id, device_name, model, status被预约表引用lab_booking预约记录id, user_id, device_id, start_time, end_time, status关联 user 和 devicelab_record使用记录id, booking_id, operate_time, remark关联 bookinglab_role角色权限id, role_name, permission_level被 user 表引用建表 SQL 用 MySQL 写字符集统一utf8mb4时间字段用datetimeCREATE DATABASE lab_manage DEFAULT CHARACTER SET utf8mb4; USE lab_manage; CREATE TABLE lab_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role_id INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE lab_device ( id INT PRIMARY KEY AUTO_INCREMENT, device_name VARCHAR(100) NOT NULL, model VARCHAR(50), status TINYINT DEFAULT 0 COMMENT 0可用 1占用 2维修, lab_room VARCHAR(50) ); CREATE TABLE lab_booking ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, device_id INT NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待审 1通过 2驳回, FOREIGN KEY (user_id) REFERENCES lab_user(id), FOREIGN KEY (device_id) REFERENCES lab_device(id) ); CREATE TABLE lab_record ( id INT PRIMARY KEY AUTO_INCREMENT, booking_id INT NOT NULL, operate_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255), FOREIGN KEY (booking_id) REFERENCES lab_booking(id) ); CREATE TABLE lab_role ( id INT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(30) NOT NULL, permission_level INT DEFAULT 1 );这段 SQL 的逻辑说明lab_user和lab_role通过role_id关联权限级别用整数区分1 是普通学生、2 是教师、3 是管理员。lab_booking同时挂user_id和device_id两个外键保证预约记录不会指向不存在的用户或设备。lab_record只挂booking_id因为使用记录一定是从某次预约产生的。参数说明status字段用TINYINT而不是BOOLEAN因为后续可能扩展更多状态值。start_time和end_time用DATETIME而不是DATE实验室预约通常精确到小时。外键约束建议在开发阶段保留方便发现数据不一致上线前如果性能敏感可以去掉改由应用层保证。2.2 初始化数据与连接配置建完表后插入几条测试数据方便后面调 Swing 界面时能看到效果INSERT INTO lab_role (role_name, permission_level) VALUES (学生, 1), (教师, 2), (管理员, 3); INSERT INTO lab_user (username, password, role_id) VALUES (admin, 123456, 3), (student01, 123456, 1); INSERT INTO lab_device (device_name, model, status, lab_room) VALUES (示波器, Tektronix TBS1102, 0, A101), (万用表, Fluke 15B, 0, A101), (信号发生器, RIGOL DG1022, 1, A102);Java 侧连接 MySQL 的配置我一般单独抽一个DBUtil类避免每个 Swing 面板里都写一遍DriverManager.getConnectionimport java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/lab_manage?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_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); } }逻辑说明静态代码块里加载驱动保证类第一次被使用时驱动已经注册。getConnection每次返回新连接Swing 桌面应用并发量低不需要连接池但用完必须关闭否则连接数会涨。参数说明serverTimezone必须设否则 MySQL 8 会报时区错误。characterEncodingutf8配合数据库的utf8mb4避免中文设备名乱码。useSSLfalse在本地开发环境可以关掉减少连接开销。提示如果导入 SQL 后中文显示问号先检查数据库和表的字符集是否为utf8mb4再检查 JDBC URL 里的characterEncoding参数。3. Swing 界面搭建从登录窗口到主面板的完整链路3.1 登录窗口与权限分流Swing 做 GUI 的核心思路是每个功能模块用一个JPanel主窗口用JFrame加CardLayout或JTabbedPane切换。登录窗口是入口验证成功后根据角色决定显示哪些面板。登录窗口代码import javax.swing.*; import java.awt.*; import java.awt.event.ActionEvent; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; public LoginFrame() { setTitle(实验室管理系统 - 登录); setSize(350, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel new JPanel(new GridLayout(3, 2, 10, 10)); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); panel.add(new JLabel(用户名)); userField new JTextField(); panel.add(userField); panel.add(new JLabel(密码)); passField new JPasswordField(); panel.add(passField); JButton loginBtn new JButton(登录); JButton resetBtn new JButton(重置); panel.add(loginBtn); panel.add(resetBtn); loginBtn.addActionListener(this::doLogin); resetBtn.addActionListener(e - { userField.setText(); passField.setText(); }); add(panel); setVisible(true); } private void doLogin(ActionEvent e) { String username userField.getText().trim(); String password new String(passField.getPassword()).trim(); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } String sql SELECT u.id, u.username, r.permission_level FROM lab_user u JOIN lab_role r ON u.role_id r.id WHERE u.username ? AND u.password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery(); if (rs.next()) { int level rs.getInt(permission_level); dispose(); new MainFrame(username, level); } else { JOptionPane.showMessageDialog(this, 用户名或密码错误); } } catch (Exception ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, 数据库连接失败); } } }逻辑说明登录按钮触发doLogin先做空值校验再用PreparedStatement查库。查询语句关联了lab_role表一次拿到权限级别。验证通过后关闭登录窗口把用户名和权限级别传给主窗口。参数说明setLocationRelativeTo(null)让窗口居中显示。GridLayout(3, 2, 10, 10)表示三行两列水平和垂直间距各 10 像素。PreparedStatement的?占位符必须按顺序setString这里 1 对应用户名、2 对应密码。3.2 主面板与设备管理表格主窗口根据权限级别决定显示哪些标签页。管理员能看到全部功能学生只能看到设备查询和预约申请。import javax.swing.*; import javax.swing.table.DefaultTableModel; import java.awt.*; import java.sql.*; public class MainFrame extends JFrame { private String username; private int permissionLevel; public MainFrame(String username, int permissionLevel) { this.username username; this.permissionLevel permissionLevel; setTitle(实验室管理系统 - username); setSize(900, 600); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabbedPane new JTabbedPane(); tabbedPane.addTab(设备管理, createDevicePanel()); if (permissionLevel 2) { tabbedPane.addTab(预约审批, createBookingPanel()); } if (permissionLevel 3) { tabbedPane.addTab(用户管理, createUserPanel()); } add(tabbedPane); setVisible(true); } private JPanel createDevicePanel() { JPanel panel new JPanel(new BorderLayout()); String[] columns {ID, 设备名称, 型号, 状态, 实验室}; DefaultTableModel model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; JTable table new JTable(model); JScrollPane scrollPane new JScrollPane(table); JPanel btnPanel new JPanel(); JButton refreshBtn new JButton(刷新); JButton addBtn new JButton(新增设备); refreshBtn.addActionListener(e - loadDevices(model)); addBtn.addActionListener(e - showAddDeviceDialog(model)); btnPanel.add(refreshBtn); btnPanel.add(addBtn); panel.add(scrollPane, BorderLayout.CENTER); panel.add(btnPanel, BorderLayout.SOUTH); loadDevices(model); return panel; } private void loadDevices(DefaultTableModel model) { model.setRowCount(0); String sql SELECT id, device_name, model, status, lab_room FROM lab_device; try (Connection conn DBUtil.getConnection(); Statement stmt conn.createStatement(); ResultSet rs stmt.executeQuery(sql)) { while (rs.next()) { String statusText switch (rs.getInt(status)) { case 0 - 可用; case 1 - 占用; case 2 - 维修; default - 未知; }; model.addRow(new Object[]{ rs.getInt(id), rs.getString(device_name), rs.getString(model), statusText, rs.getString(lab_room) }); } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, 加载设备列表失败); } } private void showAddDeviceDialog(DefaultTableModel model) { JTextField nameField new JTextField(); JTextField modelField new JTextField(); JTextField roomField new JTextField(); Object[] message { 设备名称, nameField, 型号, modelField, 实验室, roomField }; int option JOptionPane.showConfirmDialog(this, message, 新增设备, JOptionPane.OK_CANCEL_OPTION); if (option JOptionPane.OK_OPTION) { String sql INSERT INTO lab_device (device_name, model, status, lab_room) VALUES (?, ?, 0, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, nameField.getText().trim()); ps.setString(2, modelField.getText().trim()); ps.setString(3, roomField.getText().trim()); ps.executeUpdate(); loadDevices(model); } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, 新增设备失败); } } } private JPanel createBookingPanel() { return new JPanel(); } private JPanel createUserPanel() { return new JPanel(); } }逻辑说明MainFrame构造时根据permissionLevel动态添加标签页。设备面板用DefaultTableModel承载数据重写isCellEditable返回false防止用户直接在表格里改数据。刷新按钮重新查库并清空旧行新增按钮弹出一个JOptionPane输入框确认后执行INSERT再刷新。参数说明model.setRowCount(0)是清空表格的常用写法比逐行删除高效。switch表达式里的-是 Java 14 的语法如果项目用 Java 8改成传统的switch-case加break。JOptionPane.showConfirmDialog返回OK_OPTION时才执行插入取消则什么都不做。注意Swing 的所有界面更新必须在事件调度线程EDT上执行。如果你在子线程里查完数据库直接调model.addRow界面可能不刷新甚至抛异常。稳妥做法是用SwingUtilities.invokeLater包一层。4. 避坑与排查Swing JDBC 组合最容易翻车的五个地方4.1 中文乱码从数据库到界面的全链路排查现象设备名称在数据库里正常但 Swing 表格里显示成问号或方块。原因三个环节都可能出问题——数据库字符集不是utf8mb4、JDBC URL 没带characterEncoding、Swing 组件用了不支持中文的字体。解决先执行SHOW VARIABLES LIKE character%;确认数据库字符集。再检查 JDBC URL 是否包含characterEncodingutf8。最后在main方法开头设置全局字体UIManager.put(Label.font, new Font(微软雅黑, Font.PLAIN, 13)); UIManager.put(Table.font, new Font(微软雅黑, Font.PLAIN, 13));4.2 连接泄漏查完数据忘了关跑半天就卡死现象程序运行一段时间后所有数据库操作都超时重启才恢复。原因Connection、Statement、ResultSet没有关闭。Swing 桌面应用虽然并发低但每次刷新表格都开新连接积累几百次后 MySQL 的max_connections就被占满。解决统一用 try-with-resources把Connection、PreparedStatement、ResultSet都放在 try 的括号里。上面所有代码示例都用了这种写法照着抄就不会漏。4.3 表格数据不刷新改了库但界面还是旧的现象新增设备后数据库里有了但表格没变化。原因DefaultTableModel的数据是内存里的副本不会自动感知数据库变化。解决每次增删改之后手动调一次loadDevices(model)。如果数据量大可以只更新受影响的行但课程设计场景直接全量刷新最省事。4.4 密码明文存储课程设计也要注意的基本问题现象lab_user表里密码字段直接存123456。原因图省事登录时直接拿明文比对。解决至少用MessageDigest做一次 MD5 或 SHA-256。注册时存哈希值登录时把输入密码哈希后比对。课程设计不要求加盐但明文存储是答辩时容易被问住的点。import java.security.MessageDigest; public static String hashPassword(String plain) throws Exception { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] digest md.digest(plain.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); }4.5 窗口关闭后进程不退出现象点了关闭按钮窗口消失了但 Java 进程还在任务管理器里。原因某处创建了非守护线程或者setDefaultCloseOperation设成了DO_NOTHING_ON_CLOSE但没加退出逻辑。解决主窗口用EXIT_ON_CLOSE子窗口用DISPOSE_ON_CLOSE。如果确实有后台线程在windowClosing里手动System.exit(0)。5. 进阶技巧用表格行选中联动预约面板5.1 从设备表格直接发起预约设备管理面板和预约面板如果各自独立用户得手动输入设备 ID体验很差。一个实用技巧是在设备表格上监听行选中事件把选中的设备信息带到预约面板。table.getSelectionModel().addListSelectionListener(e - { if (!e.getValueIsAdjusting()) { int row table.getSelectedRow(); if (row 0) { int deviceId (int) model.getValueAt(row, 0); String deviceName (String) model.getValueAt(row, 1); // 把 deviceId 和 deviceName 传给预约面板 bookingPanel.setSelectedDevice(deviceId, deviceName); } } });逻辑说明getValueIsAdjusting返回true时表示用户还在拖动选择此时不处理避免触发多次。getSelectedRow返回 -1 表示没有选中行需要判断。拿到设备 ID 和名称后调用预约面板的 setter 方法填充字段。参数说明model.getValueAt(row, 0)取第一列即 IDgetValueAt(row, 1)取第二列即设备名称。列索引从 0 开始和columns数组的顺序一致。5.2 预约时间冲突检测预约功能最容易出的问题是同一设备同一时段被重复预约。在插入lab_booking之前先查一次冲突String checkSql SELECT COUNT(*) FROM lab_booking WHERE device_id ? AND status 1 AND start_time ? AND end_time ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setInt(1, deviceId); ps.setTimestamp(2, Timestamp.valueOf(endTime)); ps.setTimestamp(3, Timestamp.valueOf(startTime)); ResultSet rs ps.executeQuery(); rs.next(); if (rs.getInt(1) 0) { JOptionPane.showMessageDialog(this, 该时段设备已被预约); return; } }逻辑说明冲突判断的条件是“已有预约的开始时间早于新预约的结束时间且已有预约的结束时间晚于新预约的开始时间”。两个时间区间有重叠就会命中。status 1表示只检查已通过的预约待审和驳回的不算。参数说明Timestamp.valueOf把LocalDateTime转成 JDBC 能识别的Timestamp。时间格式统一用yyyy-MM-dd HH:mm在界面输入框加SimpleDateFormat校验。5.3 用 JTable 渲染器给状态列上色状态列显示“可用/占用/维修”纯文字不够直观。自定义一个DefaultTableCellRenderer根据状态值设置背景色table.getColumnModel().getColumn(3).setCellRenderer(new DefaultTableCellRenderer() { Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); String status (String) value; if (可用.equals(status)) { c.setBackground(new Color(200, 255, 200)); } else if (占用.equals(status)) { c.setBackground(new Color(255, 230, 200)); } else { c.setBackground(new Color(255, 200, 200)); } return c; } });逻辑说明重写getTableCellRendererComponent先调父类拿到默认组件再根据单元格值改背景色。isSelected为true时选中行的颜色会覆盖自定义色这是 Swing 的默认行为不影响使用。参数说明getColumnModel().getColumn(3)里的 3 是状态列在表格中的索引从 0 开始数。颜色用new Color(r, g, b)构造浅绿表示可用、浅橙表示占用、浅红表示维修。5.4 打包成可执行 JAR 的注意事项课程设计最后要交一个能双击运行的文件。用 Maven 的maven-shade-plugin或maven-assembly-plugin把依赖打进去plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-shade-plugin/artifactId version3.4.1/version executions execution phasepackage/phase goalsgoalshade/goal/goals configuration transformers transformer implementationorg.apache.maven.plugins.shade.resource.ManifestResourceTransformer mainClasscom.lab.Main/mainClass /transformer /transformers /configuration /execution /executions /plugin逻辑说明shade插件把所有依赖打进一个 JARManifestResourceTransformer指定主类这样java -jar才能找到入口。phase设为package执行mvn package时自动触发。参数说明mainClass填包含public static void main的完整类名。如果项目用 Java 8maven-shade-plugin版本降到 3.2.4 更稳。打包后 JAR 体积会比较大因为 MySQL 驱动也进去了这是正常的。我自己的习惯是每次改完数据库操作代码先单独跑一遍main方法验证增删改查确认没问题再接 Swing 界面。这样出问题时能快速定位是 SQL 写错了还是界面逻辑写错了。另外课程设计答辩前一定要把数据库导出成.sql文件随源码一起交只交 Java 代码不交建表语句别人跑不起来。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?