ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Java Swing 人事管理系统实战:从登录到增删改查完整拆解

Java Swing 人事管理系统实战:从登录到增删改查完整拆解

简介:这是一套基于Java Swing开发的桌面人事管理系统源码,面向Java初学者、课程设计学生及需要GUI练手项目的开发者,用于解决企业内部员工信息录入、查询、编辑与数据导入导出等基础管理需求。压缩包共36个文件,包含16个java源文件、16个class编译文件,以及project、prefs、classpath配置和mysql-connector-java驱动jar,整体约771KB,源码与工程配置齐全,可直接导入Eclipse运行调试。系统运用JFrame、JTable、JButton、JTextField、JComboBox、JFileChooser等Swing组件构建界面,通过事件监听实现交互,并可能采用MVC架构分离业务逻辑与视图,配合JDBC连接数据库完成数据持久化,同时涉及异常处理与错误提示机制。目前已有647人学习下载,适合作为Swing界面编程、事件处理与数据库操作的实战参考,帮助读者理解桌面应用的完整开发流程与代码组织方式。

1. 一个能直接跑起来的 Java Swing 人事管理系统,到底长什么样

很多刚接触 Java 桌面开发的朋友,第一次想找一个能跑通、能改的完整项目练手时,往往会被网上那些只有半截代码、缺数据库脚本、跑起来一堆空指针的“半成品”劝退。我这次拆的这个 Java Swing 人事管理系统,就是冲着“能直接跑、能看懂、能改”这三个目标去的。它把员工档案、部门管理、考勤记录、薪资核算这几块人事日常最核心的业务,全部用 Swing 组件搭成了单机桌面应用,数据落在本地数据库里,不需要额外部署服务端,双击就能启动。适合谁?一是正在学 Java GUI 编程、想找一个完整 CRUD 项目练手的学生;二是需要给小型团队做内部人事台账、又不想上重型系统的开发者。下面我就按“先看清结构、再跑起来、最后动手改”的顺序,把这个项目从头到尾拆一遍。

2. 项目结构与技术选型:为什么用 Swing + JDBC 这套组合

2.1 目录结构一览

拿到源码包后,先别急着点运行,花两分钟把目录扫一遍,后面改代码时找文件会快很多。这个项目采用的是典型的 Maven 工程结构,根目录下src/main/java放业务代码,src/main/resources放配置和数据库脚本,lib目录里是几个必须手动引入的第三方 jar。整体分层很清晰,没有把界面和数据库操作揉在一个类里。

hr-swing-system/ ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/ │ │ │ └── hr/ │ │ │ ├── Main.java # 程序入口,启动登录窗口 │ │ │ ├── view/ # 所有 Swing 窗体 │ │ │ │ ├── LoginFrame.java │ │ │ │ ├── MainFrame.java │ │ │ │ ├── EmployeePanel.java │ │ │ │ ├── DeptPanel.java │ │ │ │ └── SalaryPanel.java │ │ │ ├── dao/ # 数据库访问层 │ │ │ │ ├── EmployeeDao.java │ │ │ │ ├── DeptDao.java │ │ │ │ └── SalaryDao.java │ │ │ ├── model/ # 实体类 │ │ │ │ ├── Employee.java │ │ │ │ ├── Department.java │ │ │ │ └── Salary.java │ │ │ └── util/ # 工具类 │ │ │ ├── DBUtil.java # 数据库连接 │ │ │ └── DateUtil.java │ │ └── resources/ │ │ ├── db.properties # 数据库连接参数 │ │ └── init.sql # 建表与初始数据 ├── lib/ │ ├── mysql-connector-java-8.0.28.jar │ └── rs2xml.jar # 结果集转表格模型 └── pom.xml

这个结构里,view只负责画界面和收集用户输入,dao只负责拼 SQL 和读写数据库,model是纯数据载体。三层各管各的,改界面不会碰坏 SQL,换数据库也不用重写窗体。常见做法是再加一个service层做业务校验,但这个项目体量不大,把校验逻辑直接写在dao的方法里也够用,新手读起来反而少绕一层。

2.2 技术选型背后的取舍

Swing 是 JDK 自带的 GUI 库,不用装额外运行时,打包成 jar 后只要有 JRE 就能跑,这对内部小工具来说是最省事的方案。数据库选了 MySQL,通过 JDBC 直连,没有引入 ORM 框架。为什么不用 Hibernate 或 MyBatis?因为人事系统的表结构简单,就员工、部门、薪资三张主表,手写 SQL 反而更直观,也方便新手看清一条数据从界面到数据库的完整链路。rs2xml.jar是个小工具包,作用是把ResultSet直接转成DefaultTableModel,省去手动遍历结果集往表格里塞数据的重复代码。

提示:如果你本地没有 MySQL,可以把db.properties里的驱动和 URL 换成 H2 或 SQLite,DBUtil里只改一行加载驱动的代码即可,后面第 5 章会讲具体怎么换。

2.3 数据库表设计

三张表的字段不多,但覆盖了人事管理最基础的关联关系。员工表通过dept_id关联部门表,薪资表通过emp_id关联员工表,删除部门时会先检查下面还有没有员工,避免出现悬空外键。

-- 部门表 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, dept_head VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 员工表 CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(20) NOT NULL, gender CHAR(2), phone VARCHAR(15), dept_id INT, hire_date DATE, status TINYINT DEFAULT 1, -- 1在职 0离职 FOREIGN KEY (dept_id) REFERENCES department(dept_id) ); -- 薪资表 CREATE TABLE salary ( salary_id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT, base_salary DECIMAL(10,2), bonus DECIMAL(10,2), pay_month VARCHAR(7), -- 格式 2024-06 FOREIGN KEY (emp_id) REFERENCES employee(emp_id) );

status字段用 1 和 0 标记在职与离职,查询时默认只查在职员工,离职员工保留在库里方便回溯历史薪资。pay_month用字符串存月份而不是日期,是因为薪资按月发放,不需要精确到日,这样查询某月薪资时直接字符串匹配就行,不用做日期函数转换。

3. 核心模块拆解:从登录到增删改查的完整链路

3.1 数据库连接工具类

整个项目所有数据库操作都走DBUtil这一个入口,连接参数放在db.properties里,改数据库地址不用翻代码。这里用的是每次操作新建连接、用完即关的方式,没有上连接池。对于单机桌面应用,同时只有一个用户在操作,连接池带来的性能提升几乎为零,反而增加配置复杂度。

public class DBUtil { private static String url; private static String user; private static String password; // 静态块在类加载时读取配置文件,只执行一次 static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); user = props.getProperty("jdbc.user"); password = props.getProperty("jdbc.password"); Class.forName(props.getProperty("jdbc.driver")); } catch (Exception e) { e.printStackTrace(); } } // 获取连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } // 统一关闭资源,顺序不能反:先关结果集,再关语句,最后关连接 public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

db.properties里四个键分别对应驱动类名、连接地址、用户名和密码。连接地址后面要带上useSSL=false&serverTimezone=Asia/Shanghai这两个参数,否则 MySQL 8 会报时区错误和 SSL 警告,这是新手最容易翻车的地方之一。close方法的关闭顺序必须和打开顺序相反,先关ResultSet再关Statement最后关Connection,顺序写反在某些驱动下会抛异常。

3.2 员工管理面板:表格 + 表单的经典布局

员工管理是使用频率最高的模块,界面左边是员工列表表格,右边是编辑表单,顶部一排操作按钮。这种布局在 Swing 里用BorderLayout套JSplitPane就能实现,左边表格放JScrollPane里自动出滚动条。

public class EmployeePanel extends JPanel { private JTable table; private DefaultTableModel tableModel; private JTextField nameField, phoneField; private JComboBox<String> deptBox, genderBox; private EmployeeDao employeeDao = new EmployeeDao(); public EmployeePanel() { setLayout(new BorderLayout()); // 顶部按钮栏 JPanel topPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); JButton addBtn = new JButton("新增"); JButton updateBtn = new JButton("修改"); JButton deleteBtn = new JButton("删除"); JButton refreshBtn = new JButton("刷新"); topPanel.add(addBtn); topPanel.add(updateBtn); topPanel.add(deleteBtn); topPanel.add(refreshBtn); // 中间表格,列名与数据库字段对应 String[] columns = {"工号", "姓名", "性别", "电话", "部门", "入职日期", "状态"}; tableModel = new DefaultTableModel(columns, 0) { @Override public boolean isCellEditable(int row, int col) { return false; // 表格只读,编辑走右侧表单 } }; table = new JTable(tableModel); table.setRowHeight(28); JScrollPane scrollPane = new JScrollPane(table); add(topPanel, BorderLayout.NORTH); add(scrollPane, BorderLayout.CENTER); // 绑定事件 refreshBtn.addActionListener(e -> loadEmployees()); deleteBtn.addActionListener(e -> deleteSelected()); table.getSelectionModel().addListSelectionListener(e -> fillFormFromTable()); loadEmployees(); // 构造时加载一次数据 } // 从数据库拉取全部在职员工,填充表格 private void loadEmployees() { tableModel.setRowCount(0); // 先清空旧数据 List<Employee> list = employeeDao.findAll(); for (Employee emp : list) { tableModel.addRow(new Object[]{ emp.getEmpId(), emp.getEmpName(), emp.getGender(), emp.getPhone(), emp.getDeptName(), emp.getHireDate(), emp.getStatus() == 1 ? "在职" : "离职" }); } } }

isCellEditable返回false是关键,否则用户双击表格就能改数据,但改完不会同步到数据库,造成界面和库里的数据不一致。所有修改必须走右侧表单和“修改”按钮,保证每次变更都经过dao层。loadEmployees里先setRowCount(0)再重新添加行,这是刷新表格的标准做法,比逐行删除快,也不会残留旧数据。

3.3 数据访问层:手写 SQL 的增删改查

EmployeeDao里每个方法都遵循“获取连接 → 准备语句 → 执行 → 处理结果 → 关闭资源”的固定套路。查询返回List<Employee>,增删改返回受影响行数。

public class EmployeeDao { // 查询所有在职员工,关联部门表取部门名称 public List<Employee> findAll() { List<Employee> list = new ArrayList<>(); String sql = "SELECT e.*, d.dept_name FROM employee e " + "LEFT JOIN department d ON e.dept_id = d.dept_id " + "WHERE e.status = 1 ORDER BY e.emp_id"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Employee emp = new Employee(); emp.setEmpId(rs.getInt("emp_id")); emp.setEmpName(rs.getString("emp_name")); emp.setGender(rs.getString("gender")); emp.setPhone(rs.getString("phone")); emp.setDeptName(rs.getString("dept_name")); emp.setHireDate(rs.getDate("hire_date")); emp.setStatus(rs.getInt("status")); list.add(emp); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 新增员工,返回自增主键 public int insert(Employee emp) { String sql = "INSERT INTO employee(emp_name, gender, phone, dept_id, hire_date, status) " + "VALUES(?, ?, ?, ?, ?, 1)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, emp.getEmpName()); ps.setString(2, emp.getGender()); ps.setString(3, emp.getPhone()); ps.setInt(4, emp.getDeptId()); ps.setDate(5, new java.sql.Date(emp.getHireDate().getTime())); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }

这里用了 try-with-resources 语法,连接、语句、结果集会在代码块结束时自动关闭,不用再手动调DBUtil.close。PreparedStatement的占位符从 1 开始编号,和数组下标从 0 开始不一样,写的时候容易数错。LEFT JOIN保证即使员工没有分配部门,员工记录也能查出来,部门名称显示为空而不是整条记录消失。

3.4 登录与权限控制

登录窗口校验用户名密码后,把当前登录用户存到一个静态变量里,主窗口根据角色决定哪些按钮可见。这个项目只做了简单的管理员和普通用户两级,没有引入复杂的权限框架。

public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; private void doLogin() { String username = userField.getText().trim(); String password = new String(passField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } UserDao userDao = new UserDao(); User user = userDao.login(username, password); if (user != null) { CurrentUser.set(user); // 存入全局静态变量 dispose(); // 关闭登录窗 new MainFrame().setVisible(true); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } }

密码在数据库里存的是 MD5 摘要,登录时把用户输入的密码同样做 MD5 再比对,不存明文。CurrentUser是一个只有静态字段的类,主窗口构造时读它的role字段,如果是普通用户就把“删除”和“薪资核算”按钮设为不可见。这种权限控制很粗,但胜在简单,改起来也快,后面第 5 章会讲怎么把它换成更细的按钮级控制。

4. 避坑与常见问题排查

4.1 启动报“找不到或无法加载主类”

现象是双击 jar 或者命令行java -jar时直接报这个错。原因通常是打包时没有指定主类,或者pom.xml里maven-jar-plugin的mainClass配置写错了。解决方法是检查pom.xml中manifest配置,确保mainClass指向com.hr.Main,然后重新执行mvn clean package。如果用的是 IDE 直接运行,检查运行配置里的主类是否选对。

4.2 中文乱码

现象是界面上的中文显示成方块,或者数据库里的中文变成问号。原因有两个层面:一是 JVM 启动参数没指定字符集,二是数据库连接 URL 没带字符集参数。解决方法是在启动命令里加-Dfile.encoding=UTF-8,同时在db.properties的 URL 后面追加characterEncoding=utf8。如果数据库已经存了乱码数据,需要先把表字符集改成utf8mb4再重新导入init.sql。

4.3 表格数据不刷新

现象是新增或修改员工后,表格里还是旧数据。原因是loadEmployees没有被重新调用,或者调用前没有清空tableModel。解决方法是在每个增删改操作成功后,显式调用一次loadEmployees(),并且确保方法开头有tableModel.setRowCount(0)。如果表格绑定了RowSorter,刷新后排序会重置,这是正常现象,需要重新点列头排序。

4.4 删除部门时抛外键约束异常

现象是删除一个还有员工的部门时,控制台报Cannot delete or update a parent row。原因是员工表的dept_id外键指向了该部门,数据库不允许直接删。解决方法是在DeptDao.delete里先查该部门下的员工数量,如果大于零就弹窗提示“该部门下还有员工,无法删除”,而不是直接把异常抛给用户看。

4.5 打包后图片和配置文件丢失

现象是 IDE 里跑得好好的,打成 jar 后图标不显示、数据库连不上。原因是src/main/resources下的文件默认会被打进 jar,但如果你把图片放在项目根目录而不是 resources 下,打包时就不会被包含。解决方法是把所有资源文件统一放到src/main/resources对应子目录下,代码里用getClass().getResourceAsStream("/xxx")读取,不要用new File("xxx")这种相对路径。

5. 二次开发与改造思路

5.1 换数据库:从 MySQL 迁到 SQLite

有些场景下不想装 MySQL,比如给非技术同事用的单机工具,直接换成 SQLite 最省事。改动集中在三处:第一,把lib里的 MySQL 驱动换成sqlite-jdbc.jar;第二,改db.properties的驱动类名为org.sqlite.JDBC,URL 改成jdbc:sqlite:hr.db;第三,init.sql里的AUTO_INCREMENT改成AUTOINCREMENT,DATETIME DEFAULT CURRENT_TIMESTAMP语法 SQLite 也支持,不用动。改完跑一遍,如果建表报错,多半是字段类型不兼容,把TINYINT换成INTEGER即可。

5.2 加一个导出 Excel 的功能

人事同事最常提的需求就是“能不能把员工列表导出来”。在EmployeePanel顶部加一个“导出”按钮,点击后把tableModel里的数据写进 Excel。用 Apache POI 库,核心代码不到二十行:

private void exportExcel() { try (XSSFWorkbook workbook = new XSSFWorkbook()) { XSSFSheet sheet = workbook.createSheet("员工列表"); // 写表头 Row header = sheet.createRow(0); for (int i = 0; i < tableModel.getColumnCount(); i++) { header.createCell(i).setCellValue(tableModel.getColumnName(i)); } // 写数据行 for (int i = 0; i < tableModel.getRowCount(); i++) { Row row = sheet.createRow(i + 1); for (int j = 0; j < tableModel.getColumnCount(); j++) { Object val = tableModel.getValueAt(i, j); row.createCell(j).setCellValue(val == null ? "" : val.toString()); } } // 弹出保存对话框 JFileChooser chooser = new JFileChooser(); chooser.setSelectedFile(new File("员工列表.xlsx")); if (chooser.showSaveDialog(this) == JFileChooser.APPROVE_OPTION) { try (FileOutputStream out = new FileOutputStream(chooser.getSelectedFile())) { workbook.write(out); } JOptionPane.showMessageDialog(this, "导出成功"); } } catch (IOException e) { JOptionPane.showMessageDialog(this, "导出失败:" + e.getMessage()); } }

XSSFWorkbook对应.xlsx格式,如果要用.xls就换成HSSFWorkbook。JFileChooser的setSelectedFile只是给个默认文件名,用户仍然可以改路径。导出前最好检查一下tableModel.getRowCount()是否为零,零行导出会得到一个只有表头的空文件,体验不好。

5.3 把硬编码的权限改成配置化

现在的权限判断散落在各个按钮的setVisible调用里,加一个角色就要改好几处。我一般会抽一个PermissionConfig类,用Map<String, List<String>>存角色和可访问模块的对应关系,主窗口初始化时遍历所有按钮,根据当前用户角色统一设置可见性。这样以后加角色只需要改配置,不用动界面代码。改完之后记得写一个简单的验证:用管理员登录看所有按钮是否都在,用普通用户登录看受限按钮是否隐藏,两个角色各跑一遍增删改查,确认没有漏网的入口。

5.4 一个我踩过的坑

早些年我改这类 Swing 项目时,习惯直接在dao里写System.out.println打日志,觉得方便。结果有一次打包给同事用,同事反馈“点一下卡三秒”,我查了半天才发现是某条 SQL 在循环里被反复执行,控制台疯狂输出把 IO 拖慢了。从那以后我每次改dao层,都强制走一遍“先看 SQL 有没有在循环里、再看有没有不必要的全表查询、最后确认日志级别”这三步。桌面应用虽然并发低,但界面线程被阻塞一样会卡死,这个习惯帮我省了不少后悔药。希望帮到你。

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

RELATED READING

延伸阅读

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