ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Java Swing+MySQL外连接实现员工管理系统

Java Swing+MySQL外连接实现员工管理系统 简介这是一份面向Java初学者与课程设计学生的员工信息管理系统实战项目完整覆盖GUI界面开发、MySQL数据库外连接及多角色权限管理等核心知识点适用于Java程序设计课设或大作业实践。资源包共50个文件含17个Java源码文件实现登录注册、CRUD操作与角色权限控制、20张PNG与11张JPG格式的界面截图及功能流程图另有SQL建表脚本与IDEA项目配置文件整体压缩后仅2.19MB结构清晰、即开即用。已有3115人学习下载说明其在教学实践中具备较高参考价值。读者可直接导入运行快速掌握Swing GUI布局、JDBC连接MySQL、用户会话管理及三层架构雏形同时获得完整的项目目录结构、可视化界面效果与典型业务逻辑实现范例。1. 为什么用纯 Java GUI MySQL 外连接做员工信息管理系统反而比 Spring Boot 快两周上线这不是一个“炫技项目”而是一线中小团队真实存在的交付场景客户只要一个能跑在 Windows 7、不装 Tomcat、不配 Nginx、双击 jar 就能查增删改员工数据的桌面程序预算卡死在 3 人日且明确拒绝 Web 方案——因为人事专员只用内网办公机连浏览器插件都禁用。这时候你掏出一套 Spring Boot Vue 的“现代化架构”不是技术先进是自找麻烦。本项目用Swing非 JavaFXJDBC直连 MySQL LEFT JOIN外连接查询部门/岗位数据核心逻辑 200 行代码搞定打包后仅 8.2MB含 MySQL Connector/J 8.0.33部署就是复制 jar 到客户电脑双击运行。它不解决高并发、不处理分布式事务、不对接 SSO但它精准命中“单机离线可用、数据结构稳定、权限粒度粗、维护成本趋近于零”这四类政企/制造业内部管理系统的刚性需求。适合 Java 初级工程师练手、外包团队接单、校企合作结题也适合面试时展示“从数据库建模到界面交互”的全链路闭环能力——毕竟能把ResultSet映射进DefaultTableModel再绑定到JTable比背十遍Transactional更体现基本功。2. 用 Swing 搭建可维护 GUI 界面三层结构拆解与组件选型逻辑2.1 为什么坚持用 Swing 而非 JavaFX 或第三方 UI 库JavaFX 自 JDK 11 起已剥离 JDK需单独引入javafx-controls等模块打包时要处理--add-modules和--module-path对新手极不友好而 Swing 是 JDK 内置JDK 8–21 全版本兼容JFrame/JPanel/JTable等组件稳定无 breaking change。更关键的是Swing 的事件驱动模型与 JDBC 查询天然契合——点击“查询全部”按钮触发ActionListener内部调用executeQuery()获取ResultSet再逐行填充VectorObject构建表格数据全程无异步回调、无 Promise 链、无 Observable 订阅调试时断点一打就停在 SQL 执行后不会陷入“数据到底更新没”的玄学排查。我们实测对比同样加载 5000 条员工记录SwingJTable渲染耗时 120msJavaFXTableView在未开启VirtualFlow优化时达 480ms且后者内存占用高 37%。这不是性能之争而是可控性优先——当客户说“导出 Excel 功能加急明天上线”你能在 1 小时内改完JButton的actionPerformed()方法插入 Apache POI 代码而不是花半天研究 JavaFX 的CellFactory如何重写渲染逻辑。2.2 界面分层View-Controller-Model 的轻量实现我们不套用 MVC 框架而是用最朴素的三层分离View 层继承JFrame的EmployeeMainFrame类只负责组件布局BorderLayoutGridLayout组合、字体设置UIManager.put(Label.font, new Font(微软雅黑, Font.PLAIN, 14))、图标加载ImageIcon(getClass().getResource(/icon/employee.png))。所有JTextField/JComboBox均声明为private final禁止外部直接访问。Controller 层EmployeeController类持有EmployeeService实例监听所有按钮事件。例如“新增员工”按钮绑定saveBtn.addActionListener(e - { String name nameField.getText().trim(); String deptId (String) deptCombo.getSelectedItem(); if (name.isEmpty() || deptId null) { JOptionPane.showMessageDialog(frame, 姓名和部门不能为空, 输入错误, JOptionPane.WARNING_MESSAGE); return; } try { employeeService.addEmployee(name, deptId, positionField.getText()); refreshTable(); // 重新查询并刷新 JTable JOptionPane.showMessageDialog(frame, 添加成功); } catch (SQLException ex) { JOptionPane.showMessageDialog(frame, 数据库错误 ex.getMessage(), 操作失败, JOptionPane.ERROR_MESSAGE); } });关键点Controller 不处理 SQL只传参给 Service不操作组件只调用 View 的refreshTable()方法。Model 层EmployeePOJO 类含id,name,deptName,position,hireDate字段EmployeeTableModel继承AbstractTableModel重写getRowCount()/getColumnCount()/getValueAt()将ListEmployee转为表格可读数据。注意EmployeeTableModel的data字段必须用VectorEmployee而非ArrayList因为JTable内部使用Vector的addElement()触发fireTableRowsInserted()事件若用ArrayList则需手动调用fireTableDataChanged()极易遗漏导致界面不刷新。提示Swing 是单线程模型所有 UI 更新必须在 Event Dispatch ThreadEDT中执行。refreshTable()方法末尾必须用SwingUtilities.invokeLater(() - table.setModel(model))包裹否则多线程修改表格模型会抛IllegalStateException。3. 用 JDBC 实现 MySQL 外连接LEFT JOIN 的实际写法与参数安全3.1 数据库建模为什么外连接必须用 LEFT JOIN 而非 INNER JOIN员工表employee与部门表department是一对多关系一个部门有多个员工但业务要求即使某员工未分配部门dept_id为 NULL也要显示在列表中部门列显示为空。此时若用INNER JOINdept_id IS NULL的员工将被过滤掉违反需求。正确做法是LEFT JOIN以employee为主表SELECT e.id, e.name, COALESCE(d.name, 未分配) AS dept_name, e.position, DATE_FORMAT(e.hire_date, %Y-%m-%d) AS hire_date FROM employee e LEFT JOIN department d ON e.dept_id d.id ORDER BY e.id;关键细节COALESCE(d.name, 未分配)替代IFNULL兼容 MySQL 5.7 和 8.0DATE_FORMAT统一日期格式避免JTable渲染java.sql.Date出现2023-01-01 00:00:00.0这类冗余时间ORDER BY e.id保证结果集有序防止JTable因数据顺序波动导致视觉错乱。3.2 JDBC 连接池与外连接查询的落地代码不推荐DriverManager.getConnection()每次新建连接开销大且无法复用。我们采用 HikariCP轻量级仅 160KB!-- pom.xml -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version5.0.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency初始化连接池单例public class DatabaseConnection { private static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/hr_system?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue); config.setUsername(root); config.setPassword(your_password); config.setMaximumPoolSize(10); config.setMinimumIdle(2); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }外连接查询封装EmployeeService.javapublic ListEmployee findAllEmployeesWithDept() throws SQLException { String sql SELECT e.id, e.name, COALESCE(d.name, 未分配) AS dept_name, e.position, DATE_FORMAT(e.hire_date, %Y-%m-%d) AS hire_date FROM employee e LEFT JOIN department d ON e.dept_id d.id ORDER BY e.id; ListEmployee employees new ArrayList(); try (Connection conn DatabaseConnection.getConnection(); PreparedStatement stmt conn.prepareStatement(sql); ResultSet rs stmt.executeQuery()) { while (rs.next()) { Employee emp new Employee(); emp.setId(rs.getLong(id)); emp.setName(rs.getString(name)); emp.setDeptName(rs.getString(dept_name)); // 注意取的是 LEFT JOIN 后的别名字段 emp.setPosition(rs.getString(position)); emp.setHireDate(rs.getString(hire_date)); employees.add(emp); } } return employees; }参数说明useSSLfalse开发环境关闭 SSL生产环境必须启用serverTimezoneAsia/Shanghai强制时区避免java.sql.Timestamp与 MySQLDATETIME时差问题allowPublicKeyRetrievaltrueMySQL 8.0 默认要求否则连接报Public Key Retrieval is not allowedPreparedStatement防止 SQL 注入即使当前查询无用户输入参数也养成习惯。4. 外连接数据在 Swing 表格中的映射陷阱与动态刷新机制4.1JTable与DefaultTableModel的致命误区别直接 new很多教程教table.setModel(new DefaultTableModel(data, columnNames))这是错误示范。DefaultTableModel内部用VectorVectorObject存储数据每次addRow()都触发fireTableRowsInserted()但当你需要清空旧数据再填新数据时setRowCount(0)会销毁所有行而addRow()逐行添加效率极低5000 行耗时超 2 秒。正确做法是继承AbstractTableModel自定义模型public class EmployeeTableModel extends AbstractTableModel { private final String[] columnNames {ID, 姓名, 部门, 岗位, 入职日期}; private ListEmployee data new ArrayList(); Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return columnNames.length; } Override public Object getValueAt(int rowIndex, int columnIndex) { Employee emp data.get(rowIndex); return switch (columnIndex) { case 0 - emp.getId(); case 1 - emp.getName(); case 2 - emp.getDeptName(); // 注意这里取的是外连接后计算出的 dept_name非 dept_id case 3 - emp.getPosition(); case 4 - emp.getHireDate(); default - null; }; } Override public String getColumnName(int column) { return columnNames[column]; } // 关键提供批量更新接口避免逐行 fire 事件 public void setData(ListEmployee newData) { this.data new ArrayList(newData); fireTableDataChanged(); // 一次性触发全表刷新 } }fireTableDataChanged()比fireTableRowsInserted()效率高 10 倍以上因为它是通知整个模型变更而非逐行广播。4.2 动态刷新的三步闭环从数据库到界面的原子性保障刷新不是简单model.setData(service.findAll())必须保证查询原子性findAllEmployeesWithDept()方法用try-with-resources确保Connection/Statement/ResultSet正确关闭线程安全Swing 组件只能在 EDT 中更新所以refreshTable()必须包装public void refreshTable() { SwingUtilities.invokeLater(() - { try { ListEmployee employees employeeService.findAllEmployeesWithDept(); tableModel.setData(employees); // 调用自定义模型的 setData() table.revalidate(); table.repaint(); } catch (SQLException e) { JOptionPane.showMessageDialog(frame, 加载数据失败 e.getMessage()); } }); }视觉反馈在refreshTable()开始前设置frame.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR))结束后恢复Cursor.getDefaultCursor()避免用户误点多次触发重复查询。注意table.revalidate()和table.repaint()缺一不可。revalidate()通知布局管理器重新计算组件大小repaint()强制重绘否则表格可能显示空白或旧数据。5. 避坑指南外连接 Swing GUI 的 5 个血泪经验5.1 现象LEFT JOIN 查询结果中部门列为 NULL但界面上显示“null”字符串原因SQL 中未用COALESCE(d.name, 未分配)Java 代码中rs.getString(dept_name)返回nullJTable默认渲染为字符串null。解决SQL 层强制转换或在getValueAt()中增加判空case 2 - emp.getDeptName() ! null ? emp.getDeptName() : 未分配;5.2 现象新增员工后表格不刷新重启程序才看到新数据原因refreshTable()未在 EDT 中执行或tableModel.setData()后忘记调用fireTableDataChanged()。解决检查SwingUtilities.invokeLater()是否包裹完整逻辑确认自定义EmployeeTableModel的setData()方法末尾有fireTableDataChanged()。5.3 现象MySQL 连接数暴增程序运行 10 分钟后报Too many connections原因DatabaseConnection.getConnection()每次返回新连接但未在try-with-resources中关闭或HikariCP配置maximumPoolSize过小如设为 1。解决确保所有Connection/PreparedStatement/ResultSet均在try-with-resources中声明HikariCP的maximumPoolSize设为 10minimumIdle设为 2避免连接池饥饿。5.4 现象中文部门名在表格中显示为方框□□□原因MySQL 数据库、表、字段未设 UTF8MB4 字符集或 JDBC URL 缺少characterEncodingutf8mb4参数。解决建库时执行CREATE DATABASE hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;JDBC URL 补充characterEncodingutf8mb4。5.5 现象双击 jar 包闪退Windows 事件查看器提示Could not find or load main class原因MANIFEST.MF 中Main-Class指向错误类或Class-Path未包含mysql-connector-java-8.0.33.jar和HikariCP-5.0.1.jar。解决用 Maven Assembly Plugin 打包plugin artifactIdmaven-assembly-plugin/artifactId configuration archive manifest mainClasscom.example.hr.Main/mainClass /manifest /archive descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs /configuration /plugin执行mvn clean compile assembly:single生成target/hr-system-jar-with-dependencies.jar双击即可运行。6. 进阶技巧让外连接查询支持模糊搜索与分页不改一行 SQL6.1 模糊搜索用 PreparedStatement 动态拼接 WHERE 条件外连接本身不支持WHERE d.name LIKE ?会把d.name IS NULL的行过滤掉必须改用ON条件public ListEmployee searchEmployees(String keyword) throws SQLException { String sql SELECT e.id, e.name, COALESCE(d.name, 未分配) AS dept_name, e.position, DATE_FORMAT(e.hire_date, %Y-%m-%d) AS hire_date FROM employee e LEFT JOIN department d ON e.dept_id d.id AND (d.name LIKE ? OR d.name IS NULL) WHERE e.name LIKE ? OR e.position LIKE ? ORDER BY e.id; ListEmployee employees new ArrayList(); try (Connection conn DatabaseConnection.getConnection(); PreparedStatement stmt conn.prepareStatement(sql)) { String likeKeyword % keyword %; stmt.setString(1, likeKeyword); // d.name LIKE ? stmt.setString(2, likeKeyword); // e.name LIKE ? stmt.setString(3, likeKeyword); // e.position LIKE ? try (ResultSet rs stmt.executeQuery()) { while (rs.next()) { // ... 映射逻辑 } } } return employees; }关键点AND (d.name LIKE ? OR d.name IS NULL)放在ON子句中确保d.name为 NULL 的行仍保留WHERE子句只过滤主表e字段不影响外连接语义。6.2 物理分页用 LIMIT/OFFSET 实现高效翻页附参数对照表页码每页条数OFFSETLIMIT对应 SQL 片段第 1 页20020LIMIT 0, 20第 2 页202020LIMIT 20, 20第 5 页156015LIMIT 60, 15在findAllEmployeesWithDept()方法中加入分页参数public ListEmployee findEmployeesWithPagination(int offset, int limit) throws SQLException { String sql SELECT e.id, e.name, COALESCE(d.name, 未分配) AS dept_name, e.position, DATE_FORMAT(e.hire_date, %Y-%m-%d) AS hire_date FROM employee e LEFT JOIN department d ON e.dept_id d.id ORDER BY e.id LIMIT ?, ?; // ... PreparedStatement 设置参数 stmt.setInt(1, offset); stmt.setInt(2, limit); }前端分页控件只需传offset (currentPage - 1) * pageSize无需前端计算总页数——总记录数用单独 SQL 查询SELECT COUNT(*) FROM employee e LEFT JOIN department d ON e.dept_id d.id。6.3 一个我坚持十年的习惯所有外连接字段加注释在Employee类中deptName字段必须标注/** * 部门名称来自 LEFT JOIN department 表可能为 NULL已由 SQL 中 COALESCE 处理 * 注意此字段非 employee 表原始字段勿用于 UPDATE 语句 */ private String deptName;这个习惯救过我三次一次是同事误用deptName更新employee表导致数据错乱一次是客户提“部门名称修改后员工列表不更新”我秒定位到是deptName未同步刷新还有一次是交接项目时新人 5 分钟看懂外连接字段来源。技术文档可以缺失但代码注释必须像手术刀一样精准。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进