简介:这份资源是一套基于 Java Swing 开发的桌面人事管理系统源码工程,面向正在学习 Java GUI 编程、需要完成课程设计或想积累桌面应用实战经验的学生与开发者,可用于理解界面组件组织、事件响应与数据持久化的完整实现思路。压缩包共 36 个文件,以 16 个 java 源文件和 16 个 class 编译文件为主体,另含工程配置、Eclipse 项目文件及 MySQL 驱动 jar,整体约 771KB,源码与可运行产物齐备,便于直接导入 IDE 对照阅读。系统覆盖员工信息录入、编辑、删除与多条件查询,并支持 CSV 等格式的数据导入导出,界面由 JFrame、JTable、JButton、JTextField、JComboBox 与 JFileChooser 等组件搭建,配合事件监听完成交互,结构上体现 MVC 分层与异常提示处理。目前已有 647 人学习,适合作为 Swing 入门到综合实践的参考案例。
1. 一套能直接跑起来的 Java Swing 人事管理系统,到底值不值得拆
上周有个做企业内训的朋友找我,说想给非计算机专业的行政岗同事做一套「能看得见、点得动」的桌面端练习项目,要求不高:不连数据库也能跑、界面别太丑、代码结构能讲清楚。我翻了几份现成的 Java Swing 人事管理系统源码包,最后挑了一套结构相对规整的给他。这类资源在课程设计和入门实战里出现频率极高,核心价值不在技术多新,而在于它把「窗体布局 + 事件监听 + 数据持久化」这条最容易被跳过的主线完整串了一遍。适合谁?一是刚学完 Java 基础、需要第一个完整 GUI 项目练手的人;二是带课的老师,想要一份能拆开讲、能改参数的骨架;三是想快速验证某个桌面端交互逻辑的开发者。下面我按自己拆包的顺序,把这份资源怎么用、参数怎么调、哪里容易翻车讲透。
2. 拆开源码包先看结构:Swing 三层骨架与数据流向
拿到一个 Java Swing 人事管理系统,别急着点运行。我一般先花十分钟把目录和类名过一遍,这一步决定了后面改代码是「顺藤摸瓜」还是「大海捞针」。这套资源的典型结构是三层:界面层(JFrame、JPanel、JDialog)、业务层(Service 或 Manager 类)、数据层(DAO 或内存集合)。人事管理的核心实体无非是员工、部门、考勤、薪资四类,源码里通常对应 Employee、Department 这样的模型类,加上各自的表格模型和编辑窗体。
2.1 目录与类职责的快速识别法
打开源码根目录,先找带Main或App字样的启动类,它是整个程序的入口,main方法里一般只有一行SwingUtilities.invokeLater。顺着它找到主窗体类,再看主窗体里 new 了哪些面板。我习惯用「一个实体一条线」的方式梳理:员工管理线从EmployeePanel到EmployeeService再到EmployeeDao,部门管理线同理。如果发现某个类同时干了界面渲染和 SQL 拼接两件事,说明这套代码的分层不干净,后面改需求会很难受,但作为学习样本反而能看清「不分层会怎样」。
判断数据流向有个笨办法但很有效:在 DAO 的查询方法里打一行System.out.println,运行程序点一遍菜单,看控制台输出顺序。输出顺序就是真实的调用链,比读代码猜要快得多。常见做法是把员工列表加载放在面板的构造方法里,这意味着窗体一创建就查一次库,切换标签页不会刷新,这个细节后面讲避坑时会再提。
2.2 界面层:JFrame、JPanel 与布局管理器的分工
Swing 的界面代码最容易写成「绝对定位一锅粥」。这套资源里比较规范的部分是主窗体用BorderLayout,顶部放菜单栏和工具栏,中间放JTabbedPane承载各功能面板,底部放状态栏。每个功能面板内部再用BorderLayout或BoxLayout切分「搜索区 + 表格区 + 按钮区」。这种嵌套布局的好处是窗口拉伸时表格能自动填充,不会出现控件飘到屏幕外的情况。
// 主窗体骨架:BorderLayout 分区 + JTabbedPane 承载功能面板 public class MainFrame extends JFrame { public MainFrame() { setTitle("人事管理系统"); setSize(1000, 680); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLayout(new BorderLayout()); // 整体用边界布局 add(buildMenuBar(), BorderLayout.NORTH); // 菜单栏置顶 JTabbedPane tabs = new JTabbedPane(); tabs.addTab("员工管理", new EmployeePanel()); tabs.addTab("部门管理", new DepartmentPanel()); tabs.addTab("考勤统计", new AttendancePanel()); add(tabs, BorderLayout.CENTER); // 标签页占满中间 add(buildStatusBar(), BorderLayout.SOUTH);// 状态栏置底 } }这段代码的关键在BorderLayout的五个区域语义:NORTH 和 SOUTH 高度由组件自身决定,CENTER 会吃掉所有剩余空间。所以表格必须放在 CENTER 里,否则窗口放大时表格不跟着长。JTabbedPane的addTab第二个参数是面板实例,注意这里每次 new 都会触发面板构造方法里的数据加载,如果面板多、查询重,启动会明显变慢,优化思路是把加载挪到标签页的ChangeListener里按需触发。
2.3 数据层:内存集合与 JDBC 两种持久化路径
这套资源通常提供两种数据方案:一种是纯内存的ArrayList模拟,程序一关数据就没了,适合零配置演示;另一种是 JDBC 连 MySQL,需要先建库建表。判断当前用的是哪种,看 DAO 类里有没有DriverManager.getConnection。内存方案的好处是拿到就能跑,坏处是无法演示「重启后数据还在」这个真实需求;JDBC 方案更接近生产,但多了一步环境准备。
// 内存版 DAO:用静态 List 模拟持久化,重启即清空 public class EmployeeDao { private static final List<Employee> STORE = new ArrayList<>(); static { // 初始化几条演示数据,方便一打开就有内容 STORE.add(new Employee("E001", "张工", "研发部", 12000)); STORE.add(new Employee("E002", "李工", "市场部", 9000)); } public List<Employee> findAll() { return new ArrayList<>(STORE); } public void save(Employee e) { STORE.add(e); } public void delete(String id) { STORE.removeIf(e -> e.getId().equals(id)); // 按工号删除 } }findAll返回的是副本而不是STORE本身,这一点很重要:如果直接把内部集合暴露出去,界面层拿到引用后随手clear就会把数据源清空,这是新手改这类代码时的高频翻车点。removeIf配合 lambda 是 Java 8 之后的写法,如果源码包用的是老版本 JDK,这里会编译不过,需要改成迭代器删除。切到 JDBC 方案时,把findAll换成SELECT查询、save换成INSERT,方法签名不变,界面层一行都不用动,这就是分层带来的好处。
3. 跑通第一个界面:环境、编译与事件绑定实操
结构看完,接下来是让它真正跑起来。这一步的坑集中在 JDK 版本和字符编码上,尤其是中文界面,编码不对会满屏问号。我一般按「定 JDK → 编译 → 运行 → 验证交互」四步走,每步都有可观察的成功标志,不靠感觉。
3.1 JDK 版本选择与命令行编译参数
先确认源码用的语言级别。看代码里有没有var、switch表达式、record这些新语法,有就至少 JDK 11 起步;如果只有 lambda 和 Stream,JDK 8 就够。命令行编译时显式指定编码,避免源码里的中文注释和字符串变成乱码:
# 编译:指定源码编码为 UTF-8,输出到 out 目录 javac -encoding UTF-8 -d out -sourcepath src $(find src -name "*.java") # 运行:把 out 加入类路径,指定主类 java -cp out com.example.hr.MainApp-encoding UTF-8必须和源文件实际编码一致,Windows 上用记事本另存过的文件可能是 GBK,这时要么改参数为-encoding GBK,要么用编辑器统一转成 UTF-8。-d out把 class 文件集中输出,避免污染源码目录。find那一段在 Windows 的 cmd 里不可用,换成dir /s /b src\*.java > sources.txt再配合@sources.txt传给 javac。运行阶段如果报NoClassDefFoundError,九成是-cp路径写错或者主类全限定名拼错,用java -cp out加类名逐级试。
3.2 按钮事件与表格刷新的绑定逻辑
界面能弹出来只是第一步,真正要验证的是「点新增按钮 → 弹窗录入 → 表格多一行」这条链路。Swing 的事件模型是监听器模式,按钮的addActionListener里写业务逻辑,表格的数据源是DefaultTableModel,刷新靠fireTableDataChanged或重新setModel。
// 新增按钮:弹录入框,保存后刷新表格 addBtn.addActionListener(e -> { EmployeeForm form = new EmployeeForm(this); // 模态对话框 form.setVisible(true); // 阻塞直到关闭 Employee created = form.getResult(); // 用户确认则非 null if (created != null) { service.save(created); // 先落数据 reloadTable(); // 再刷界面 } }); private void reloadTable() { model.setRowCount(0); // 清空旧行 for (Employee emp : service.findAll()) { model.addRow(new Object[]{emp.getId(), emp.getName(), emp.getDepartment(), emp.getSalary()}); } }顺序不能反:必须先save再reloadTable,否则表格刷出来的是旧数据。setVisible(true)对模态对话框是阻塞调用,下一行代码要等用户关窗才执行,这个特性让「取返回值」的写法成立。setRowCount(0)清空再逐行加,数据量大时会有闪烁,进阶做法是自定义AbstractTableModel只通知变更行,这个放到最后一章讲。验证成功的标志很直观:新增一条后表格立刻多一行,关掉程序重开(JDBC 方案)数据还在。
3.3 中文乱码与字体渲染的两个开关
中文乱码分两种:编译期乱码和运行期乱码。编译期就是上面说的-encoding;运行期常见于从文件或数据库读出的中文显示成方块,原因是 Swing 默认字体不含中文字形。解决办法是在程序启动时统一设置 UI 字体:
// 启动时统一设置全局字体,避免中文显示为方块 UIManager.put("Label.font", new Font("Microsoft YaHei", Font.PLAIN, 13)); UIManager.put("Button.font", new Font("Microsoft YaHei", Font.PLAIN, 13)); UIManager.put("Table.font", new Font("Microsoft YaHei", Font.PLAIN, 13)); UIManager.put("TableHeader.font", new Font("Microsoft YaHei", Font.BOLD, 13));这几行要放在创建任何窗体之前,否则已经创建的组件不受影响。字体名在不同系统上不一样,Linux 上可能没有「微软雅黑」,稳妥做法是用GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames()先探测再选,或者退而求其次用逻辑字体名"SansSerif",它会自动映射到系统默认的无衬线字体。表格行高也要跟着调,table.setRowHeight(26)配合 13 号字看起来才不挤。
4. 避坑与排查:这套源码最容易翻车的五个地方
拆这类项目,代码本身不难,难的是那些「跑起来才发现」的问题。下面五条是我和身边人实际踩过的,按现象、原因、解决三段式记下来,照着排查能省不少时间。
4.1 表格编辑后数据没保存
现象:在表格里双击改了员工姓名,切到别的标签页再切回来,改动没了。原因:JTable的单元格编辑默认只改TableModel里的值,没有触发任何持久化逻辑,而重新加载时又从数据源覆盖了模型。解决:给TableModel加addTableModelListener,在tableChanged回调里判断列号,把新值写回 Service;或者干脆禁用单元格直接编辑(table.setDefaultEditor(Object.class, null)),强制走「选中行 → 点编辑按钮 → 弹窗修改」的流程,后者对新手更友好,也避免了编辑中途数据不一致。
4.2 窗口关闭后进程不退出
现象:点了右上角关闭,窗口消失了,但任务管理器里 java 进程还在。原因:程序里创建了非守护线程(比如定时刷新考勤的Timer),或者有JDialog没释放,AWT 事件分发线程仍在等待。解决:主窗体设setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)只是关窗,不保证 JVM 退出;更彻底的是在WindowListener的windowClosing里显式System.exit(0),并检查所有Timer是否调用了stop()。如果用了数据库连接池,还要在退出前关闭连接。
4.3 JDBC 连接写死在代码里
现象:换一台机器就跑不起来,报Access denied或Communications link failure。原因:连接串、用户名、密码硬编码在 DAO 的静态块里,换环境必须改源码重编译。解决:抽一个db.properties放类路径下,用Properties加载,连接参数外置。同时注意 MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,连接串要带时区和 SSL 参数,老教程里的com.mysql.jdbc.Driver在新驱动上会告警甚至失败。
4.4 大数据量下列表卡死界面
现象:员工数据到几千条时,点「刷新」整个窗口白屏几秒,鼠标转圈。原因:查询和表格填充都在事件分发线程(EDT)上同步执行,Swing 的单线程模型决定了 EDT 一忙,界面就不响应。解决:把耗时查询放到SwingWorker的doInBackground里,done回调里再更新表格模型。注意done方法本身跑在 EDT 上,所以更新 UI 是安全的,但不要在doInBackground里碰任何 Swing 组件。
4.5 打包成 jar 后图片和配置丢失
现象:在 IDE 里图标正常,java -jar运行后图标全空,读配置报FileNotFoundException。原因:代码里用new File("images/logo.png")这种相对路径,打成 jar 后文件在压缩包内,文件系统路径不存在。解决:资源统一放src/main/resources,用getClass().getResourceAsStream("/images/logo.png")读取,图标用new ImageIcon(getClass().getResource("/images/logo.png"))。配置文件同理走类路径加载,不要用File对象。
5. 从能跑到好用:自定义 TableModel 与分页查询的进阶改法
把上面五条坑填完,这套系统就算「能稳定演示」了。但如果想拿它当真正的练习底座,我建议动两处结构性的东西:一是把DefaultTableModel换成自定义的AbstractTableModel,二是给员工列表加分页。这两处改完,你会对 Swing 的「模型-视图」分离有完全不同的理解,也是从「照着抄」到「能改」的分水岭。
先说自定义模型。DefaultTableModel把所有数据塞进Vector,每次fireTableDataChanged都通知整表重绘,数据一多就闪。自定义模型的核心是持有List<Employee>,只暴露必要的列,并在增删改时精确通知:
// 自定义表格模型:持有实体列表,精确通知变更行 public class EmployeeTableModel extends AbstractTableModel { private final String[] cols = {"工号", "姓名", "部门", "薪资"}; private List<Employee> rows = new ArrayList<>(); public void setData(List<Employee> data) { this.rows = data; fireTableDataChanged(); // 整体替换时才全量通知 } public void updateRow(int idx, Employee e) { rows.set(idx, e); fireTableRowsUpdated(idx, idx); // 只通知这一行重绘 } @Override public int getRowCount() { return rows.size(); } @Override public int getColumnCount() { return cols.length; } @Override public String getColumnName(int c) { return cols[c]; } @Override public Object getValueAt(int r, int c) { Employee e = rows.get(r); switch (c) { case 0: return e.getId(); case 1: return e.getName(); case 2: return e.getDepartment(); default: return e.getSalary(); } } }fireTableRowsUpdated只重绘指定行,配合前面的updateRow,编辑单条记录时界面几乎无感。注意getValueAt里不要做耗时计算,它会被频繁调用,薪资格式化这类操作应该在实体里预先算好。
再说分页。内存方案下分页是「假分页」,从全量列表里subList截取;JDBC 方案下用LIMIT offset, size真分页。两种方案对界面层的接口可以统一成Page<Employee> query(int pageNo, int pageSize),界面只认页码和每页条数。参数上,pageSize我一般设 20,太小翻页烦,太大失去分页意义;offset用(pageNo - 1) * pageSize计算,注意页码从 1 开始还是从 0 开始要和界面上的「上一页/下一页」按钮逻辑对齐,这里错一位是最常见的 off-by-one。
| 改造点 | 改造前 | 改造后 | 收益 |
|---|---|---|---|
| 表格模型 | DefaultTableModel | 自定义 AbstractTableModel | 精确刷新,减少闪烁 |
| 数据加载 | 构造时全量查 | 按页查询 | 大数据量不卡界面 |
| 编辑方式 | 单元格直接编辑 | 弹窗编辑 + 行更新 | 数据一致性可控 |
| 资源读取 | File 相对路径 | 类路径流读取 | 打包后不丢资源 |
最后说个验证习惯。每次改完模型层,我不会只看界面,而是先在main里写几行断言式的小测试:造 100 条假数据,调query(2, 20),检查返回条数是不是 20、第一条的工号是不是第 21 条。这个习惯是从一次血泪经验来的——当时分页的 offset 算错,界面上看着正常,翻到最后一页才发现少了一条,排查了半天。从那以后我每次动分页逻辑,都强制先把边界页(第一页、最后一页、空结果页)跑一遍再点界面。希望这套拆解能帮到你,少走几个我走过的弯路。
本文还有配套的精品资源,点击获取