简介:一套基于Java Swing与MySQL数据库的课程设计人事管理系统完整源码包,专门面向需要完成数据库课程设计或练习JDBC开发的Java初学者。系统自带图形化操作界面,通过JDBC实现与MySQL的交互,覆盖人事管理中的员工信息维护、考勤记录、薪资查询等典型功能模块,能够直观展示桌面程序与数据库协同工作的完整链路。资源压缩包共包含24个文件,其中11个java源文件用于核心业务逻辑与界面实现,12个png图片为系统运行界面截图,另有1个sql文件提供建库建表脚本,整体大小仅143KB,便于快速下载与部署。根据资源描述,运行中可能遇到的报错多为数据库版本差异导致的基础连接或语法问题,读者可结合本地上手自行解决。目前已有3878人学习或下载过该资源,对于希望借鉴完整项目结构、参考界面设计或快速搭建人事管理演示系统的学习者来说,是一份轻量且实用的参考资料。
1. 数据库课程设计选人事管理系统,为什么绕不开 javaswing + mysql + jdbc
每到期末,Java 课程设计题目里出现频率最高的就是人事管理系统。javaswing 负责把数据库里的数据画成窗口,mysql 存数据,jdbc 在两者之间跑 SQL,这个组合几乎成了数据库课程设计的标准答案。理由很直白:Swing 是 JDK 自带的图形界面库,不需要额外装前端环境;MySQL 免费且安装教程遍地都是;JDBC 是 Java 操作数据库的原生接口,老师一眼就能看出你懂没懂原理。整个链路没有任何黑匣子,从建库到跑通最远不过一个下午。
这套组合适合两类人:一类是被课程设计逼着交作业的学生,目标是可运行、有界面、答得上答辩;另一类是刚入职想快速做个内部小工具练手的初级开发。系统要解决的核心问题也很简单——员工的增删改查、按条件找人、部门与员工的关系维护。别小看这个范围,真把它做到不翻车,涉及表结构设计、JDBC 资源管理、Swing 事件线程、中文乱码处理,每一个都是从业者日常要面对的东西。这篇就按我实际做这类系统的顺序讲,先立数据层,再做界面,最后集中说坑。
2. 建库建表与 JDBC 连接:先把数据层独立跑通
2.1 人事管理系统的表结构设计:五张表和一对外键
课程设计的人事管理系统,表数量不用多,但关系要能讲出东西。我一般建议设计五张表:用户表(登录用)、部门表、员工表、工资表、考勤表。其中员工表和部门表通过部门编号建立外键关系,员工表和工资表、考勤表通过员工编号关联。这样的结构在答辩时能讲出「一对多」「一对一」两个关系,而且 SQL 查询里自然会用到 JOIN,比单表增删改查有说服力得多。
CREATE DATABASE personnel DEFAULT CHARACTER SET utf8mb4; USE personnel; CREATE TABLE dept ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, location VARCHAR(100) ); CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) NOT NULL UNIQUE, name VARCHAR(50) NOT NULL, gender CHAR(2), age INT, dept_id INT, hire_date DATE, phone VARCHAR(20), FOREIGN KEY (dept_id) REFERENCES dept(dept_id) ); CREATE TABLE `user` ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL );建库时把DEFAULT CHARACTER SET utf8mb4写进建库语句,是后面少折腾中文乱码的关键。utf8mb4 是 MySQL 8 的默认字符集,能存emoji和生僻字,兼容性比老 utf8 好。员工表里的emp_no是业务编号,比如EMP001,它和自增主键emp_id分开,是为了以后系统对接打卡机之类的外部数据时,业务编号不变、主键随便改也不影响关联。
外键约束我建议加上。有人觉得外键影响插入效率想省掉,但课程设计阶段,外键能在你写错部门编号时立刻报错,帮你守住数据一致性,答辩时也能理直气壮说「数据库层面做了完整性约束」。
2.2 MySQL 8 的驱动与连接串:时区、SSL、编码三个必调参数
表建好之后,下一步不是写界面,而是先写一个能跑通的 JDBC 连接。这步卡住的人最多,十有八九是驱动版本和连接串参数的问题。我用的依赖是mysql-connector-java8.0.x 或com.mysql:mysql-connector-j8.x,后者是 MySQL 官方从 8.0.31 开始用的新坐标。如果你是 Maven 项目,直接引新坐标;如果是手动把 jar 包放进lib目录,就去 MySQL 官网下载对应版本。
import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBConnection { public static void main(String[] args) { String url = "jdbc:mysql://localhost:3306/personnel" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; String user = "root"; String password = "你的密码"; try { Class.forName("com.mysql.cj.jdbc.Driver"); Connection conn = DriverManager.getConnection(url, user, password); System.out.println("连接成功:" + conn.getMetaData().getDatabaseProductName()); conn.close(); } catch (ClassNotFoundException e) { System.err.println("驱动类没找到,检查 jar 包是否引入"); } catch (SQLException e) { System.err.println("连接失败,错误码:" + e.getErrorCode()); e.printStackTrace(); } } }连接串里三个参数是必调的。useSSL=false是关闭 SSL 加密,本地开发库没必要做 TLS 握手,少了它 MySQL 8 会在连接日志里刷大量 warning,个别环境还会直接握手失败;serverTimezone=Asia/Shanghai是告诉驱动服务器时区,不写的话常见报错是The server time zone value '�й���ʱ��' is unrecognized,一串乱码看得人头皮发麻;characterEncoding=utf8指定客户端编码,保证中文写入前编码统一。Class.forName在 8.x 驱动里其实可以省略,但写上没坏处,能让新手看到驱动加载的完整链路。
2.3 第一个 JDBC CRUD:用 PreparedStatement 写员工插入
连接能通之后,立刻写第一个增删改查。这一步不求花哨,但要用对 API。很多人图省事用Statement拼 SQL,一旦员工姓名里出现单引号,SQL 直接语法错误,更别提 SQL 注入,这在课程设计答辩里是被问倒的高频点。所以从一开始就写PreparedStatement,这是 JDBC 里最值得养成的习惯。
public int insertEmployee(Connection conn, Employee emp) throws SQLException { String sql = "INSERT INTO employee (emp_no, name, gender, age, dept_id, hire_date, phone) " + "VALUES (?, ?, ?, ?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, emp.getEmpNo()); ps.setString(2, emp.getName()); ps.setString(3, emp.getGender()); ps.setInt(4, emp.getAge()); ps.setInt(5, emp.getDeptId()); ps.setDate(6, emp.getHireDate() != null ? new java.sql.Date(emp.getHireDate().getTime()) : null); ps.setString(7, emp.getPhone()); return ps.executeUpdate(); } }这里try-with-resources是关键,PreparedStatement 用完自动关闭,不用手动写finally里那一坨close()。setDate那行需要特别说明:实体类里hireDate通常用java.util.Date,但 JDBC 的setDate只认java.sql.Date,所以要做一次转换,不然编译报错。executeUpdate返回的是受影响行数,插入成功返回 1,业务代码可以根据返回值判断操作成败。
3. 用 JDBC 封装数据访问层:连接管理、ORM 映射和 DAO 结构
3.1 连接池还是单连接:课程设计尺度下的选择
到这一步,要不要上数据库连接池,是很多人纠结的第一个问题。常见做法是:课程设计不上连接池,手写一个工具类管理连接。理由很简单——连接池的配置和使用是另一个层面的知识,如果只是为了交作业,引入 HikariCP 会让代码复杂度陡增,答辩时老师追问「连接池原理是什么」反而容易翻车。手写单连接管理足够覆盖课设需求,而且能讲清楚 JDBC 本身。
但如果你心里清楚自己未来要写真正的项目,连接池是躲不掉的。我个人的建议是:课设阶段先手写,等系统跑通了,再花半小时把连接获取改成 HikariCP,体会两种方式的差别。常见做法是:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/personnel" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }static块保证驱动只加载一次,避免每次getConnection重复加载。把连接串、用户名、密码收拢到一个类里,后续换库改密码只改这一个文件,这也是课程设计里「代码整洁」的加分项。
3.2 写一个 DBUtil 工具类:关闭资源的正确姿势
上面只写了获取连接,还缺一个资源关闭的标准姿势。写 JDBC 的老手都知道,Connection、Statement、ResultSet这三个东西哪个没关,都会把数据库连接数占满,轻则应用变慢,重则直接报Too many connections。连接没关这个问题是数据库开发最常见的翻车原因,网上搜「mysql too many connections」能看到大量血泪帖。
public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs != null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (stmt != null) { try { stmt.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn != null) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }关闭顺序不能乱:先关ResultSet,再关Statement,最后关Connection。如果先关连接,Statement 和 ResultSet 的关闭行为就不确定了,底层 socket 可能没完全释放。close方法本身抛SQLException,但在关闭场景下,这个异常通常不影响业务,打印出来就好,不要用它打断主流程。
3.3 DAO 与实体类:ResultSet 到对象的三步映射
表、工具类都有了,还差数据访问层。所谓 DAO(Data Access Object),就是把 SQL 和业务代码隔离开,界面层调 DAO 方法拿到对象,不直接碰ResultSet。这个分层在课设里不是必须的,但写了之后,界面代码会清爽非常多。
public class EmployeeDao { public List<Employee> findByName(String keyword) { String sql = "SELECT e.emp_id, e.emp_no, e.name, e.gender, e.age, " + "e.dept_id, d.dept_name, e.hire_date, e.phone " + "FROM employee e LEFT JOIN dept d ON e.dept_id = d.dept_id " + "WHERE e.name LIKE ?"; List<Employee> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Employee emp = new Employee(); emp.setEmpId(rs.getInt("emp_id")); emp.setEmpNo(rs.getString("emp_no")); emp.setName(rs.getString("name")); emp.setGender(rs.getString("gender")); emp.setAge(rs.getInt("age")); emp.setDeptId(rs.getInt("dept_id")); emp.setDeptName(rs.getString("dept_name")); emp.setHireDate(rs.getDate("hire_date")); emp.setPhone(rs.getString("phone")); list.add(emp); } } } catch (SQLException e) { e.printStackTrace(); } return list; } }这段代码里有三个常见的边界细节。第一个是LEFT JOIN而不是INNER JOIN,这样即使员工还没分配部门,也能查出员工记录,只是dept_name为null,界面展示时补个「未分配」就行。第二个是LIKE ?配合setString(1, "%" + keyword + "%"),通配符在参数里而不在 SQL 里,保持了PreparedStatement的防注入特性。第三个是try-with-resources里连接和语句并发关闭,Connection的关闭会释放底层 socket,不需要额外再写DBUtil.close。
ResultSet转对象的过程中,我建议所有getXxx都用列名而不是列序号。列名可读性强,而且如果 SQL 字段顺序调整,用序号取值就全乱了。
4. javaswing 界面和 JDBC 数据绑定:从 JTable 到增删改查
4.1 JTable 的 TableModel 刷新:为什么查询后表格不更新
Swing 里最常用的数据展示控件是JTable,但新手最容易犯的错是:查完数据库后,直接操作表格控件,结果界面上什么都没变。原因在于JTable本身不存数据,数据都在TableModel里,你往模型里塞数据,表格才会刷新。
private DefaultTableModel model; private JTable table; private void refreshTable(String keyword) { List<Employee> list = new EmployeeDao().findByName(keyword); model.setRowCount(0); // 清空旧数据 for (Employee emp : list) { model.addRow(new Object[]{ emp.getEmpNo(), emp.getName(), emp.getGender(), emp.getAge(), emp.getDeptName(), emp.getHireDate(), emp.getPhone() }); } }setRowCount(0)是把模型里的旧行全部清掉,再逐行addRow。这一步在逻辑上相当于「重新查询 → 填充数据 → 表格自动重绘」。很多教程会教你先table.updateUI(),这其实是走弯路。更新TableModel才是正确路径,因为模型的变化会触发TableModelListener,表格自己会刷新。
4.2 按钮事件与线程阻塞:Swing 的事件分发线程
界面上的「查询」「新增」「删除」按钮都要挂事件监听,这些监听器跑在 Swing 的事件分发线程(EDT)上。如果你在事件里执行一个耗时 SQL,界面会卡死,鼠标点了没反应,看起来像程序崩了。这就是为什么查询按钮的代码里,要把数据库访问单独处理。
btnSearch.addActionListener(e -> { String keyword = txtKeyword.getText().trim(); // 耗时操作放到新线程,避免界面假死 new Thread(() -> { List<Employee> list = new EmployeeDao().findByName(keyword); SwingUtilities.invokeLater(() -> refreshTable(keyword)); }).start(); });注意这里有两个坑。第一个是keyword在 Lambda 里被引用,必须是 effectively final,所以先trim()存到局部变量。第二个是刷新表格的操作要放回事件线程,用SwingUtilities.invokeLater包裹,这是 Swing 的线程规则:数据查询可以在后台线程,但 UI 更新必须回到 EDT。简单系统直接在主事件里跑也死不了,但养成这个习惯后,以后接触真实项目不会因为线程问题踩大坑。
4.3 组合查询:在 WHERE 上拼条件,而不是拼 SQL
人事管理系统几乎都带「按部门 + 按性别 + 按入职时间」这种组合查询。常见的做法是动态拼接 WHERE 子句,但很多人一拼就把PreparedStatement扔掉了,直接用字符串拼接'1=1',这是很烂的写法——既难读又有注入风险。
我一般这么处理:先固定WHERE 1=1保证后续条件前都有AND,然后按需拼条件片段,参数全部用?收集到 List 里,最后循环setObject。
public List<Employee> search(Integer deptId, String gender, String keyword) { StringBuilder sql = new StringBuilder( "SELECT e.*, d.dept_name FROM employee e LEFT JOIN dept d ON e.dept_id = d.dept_id WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (deptId != null && deptId > 0) { sql.append("AND e.dept_id = ? "); params.add(deptId); } if (gender != null && !gender.isEmpty()) { sql.append("AND e.gender = ? "); params.add(gender); } if (keyword != null && !keyword.isEmpty()) { sql.append("AND (e.name LIKE ? OR e.emp_no LIKE ?) "); params.add("%" + keyword + "%"); params.add("%" + keyword + "%"); } try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } ... } }WHERE 1=1从性能角度讲,MySQL 优化器会直接忽略这个恒真条件,不会产生额外开销。从代码角度讲,它让每个条件都能用AND开头,省去判断「是不是第一个条件」的逻辑。setObject会自动匹配类型,比逐个setString或setInt精简很多。
5. 课程设计避坑指南:数据库连不上、中文乱码、驱动冲突与答辩追问
5.1 error 2002 与连接被拒:MySQL 服务没起来还是端口被占
课程设计最常见的第一道坎就是数据库连不上。在 Linux 上装好 MySQL 后,经常遇到ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock'。这个报错有 90% 是服务没启动,而不是密码错误。
现象是客户端连不上,报错提示 socket 文件找不到。先检查服务状态,再检查端口监听。解决步骤是:systemctl status mysql看服务状态,如果显示 inactive,就systemctl start mysql启动。启动后ss -tlnp | grep 3306看端口是否监听。如果服务起了但端口没监听,多半是配置文件里skip-networking被打开了,注释掉这行再重启。
Windows 上的表现不同,通常是com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure。先用netstat -ano | findstr 3306看端口,再用sc query mysql查服务。MySQL 8 安装后默认服务名不一定是 mysql,可能是MySQL80,用管理员身份打开命令行跑net start MySQL80。我见过最多的情况是:装的时候选了不启动服务,第二次开机就忘了开,界面报错各种看不懂,实际就是一句话的事。
5.2 中文乱码:连接串、表编码、IDE 三处对齐
中文乱码是这个系统里绕不开的坑。现象是界面显示「??」或者一串问号,或者往数据库存进去再读出来就变成了乱码。很多人以为只是连接串加个characterEncoding=utf8就完了,实际上要三处对齐。
第一处是数据库表本身:建库时有没有指定utf8mb4。如果建的是默认latin1,后面改就很麻烦。第二处是 JDBC 连接串,必须写characterEncoding=utf8,这个前面说过了。第三处是 IDE 的文件编码,IDEA 里默认file.encoding可能不是 UTF-8,导致 Java 源码里的中文字符在被 javac 编译时已经变成了乱码。
第三种情况是血泪经验。解决方式是在pom.xml里显式指定编译编码:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties>如果不用 Maven,就在 IDEA 的 Settings 里搜File Encodings,把 Global Encoding、Project Encoding、Default encoding for properties files 全部设为 UTF-8,然后重建项目。注意最后一步:改动后要重新Build Project,否则编译产物里还是旧的乱码字节。这个坑往往在把项目从一台电脑拷到另一台机器时爆发,两台机器的默认编码不一样,代码里的中文注释和字符串全花了。
5.3 驱动版本冲突与 Maven 依赖下载失败
用 IDEA 建 Maven 项目时,拉取mysql-connector-j依赖偶尔会报Cannot download from Maven Central或Download from Maven failed。这个报错提示了依赖源连接失败,但真实原因可能是 IDEA 里 Maven 配置的镜像仓库访问慢或者被墙了。
现象是 IDEA 右下角提示依赖下载失败,代码里import com.mysql.cj.jdbc.Driver一直报红。解决方式分三步:第一步,查本机 Maven 的settings.xml,确认mirror配置是否指向了可用镜像;第二步,在 IDEA 的Build Tools -> Maven里勾选Always update snapshots,再点刷新按钮重新拉取;第三步,如果还不行,直接到 MySQL 官方下载页把mysql-connector-j-8.x.x.jar手动下载,放到项目lib目录,然后File -> Project Structure -> Libraries里添加这个 jar。
手动加 jar 是我推荐课设阶段最稳的兜底方案,不依赖网络,也不会因为仓库配置问题浪费两个小时。但你要注意,jar 包必须和 JDK 版本兼容,MySQL Connector/J 8.0.x 支持 JDK 8 及以上,够用。
5.4 答辩追问:外键、事务、注入怎么答
课程设计不只看出不出得了界面,答辩提问才决定分数。最常被问的四个问题,提前准备好答案就不会慌。
第一个是「为什么用外键」。答:外键让数据库层面保证部门编号必须真实存在,防止应用层忘记校验导致孤儿数据。第二个是「事务你怎么理解」。答:一个业务操作要么全部成功要么全部失败,比如入职操作包含插入员工表和更新部门人数,两步要在一个事务里。第三个是「PreparedStatement 和 Statement 的区别」。答:前者预编译、防止 SQL 注入、处理特殊字符更安全,后者只能拼接 SQL。第四个是「索引你建了吗」。这个可以诚实一点:课程设计的数据量不大,索引优化不明显,但我知道emp_no这种频繁查询的字段可以建唯一索引。
5.5 Swing 界面显示不发光的规律:repaint 与 invokeLater
还有一个困扰很多人的现象:窗口弹出来了但是控件全空白,或者表格数据更新后界面没有重绘。原因是 Swing 的美观是在 EDT 上按事件队列处理的,如果在非 EDT 线程里改了模型,Swing 不保证立刻重绘。大家容易出现这个问题的操作是:在ActionListener里调model.addRow后数据不显示,其实是因为你在监听器之外又包了别的线程操作。
这个问题的标准解法就是前面说的:所有 UI 操作放到SwingUtilities.invokeLater里,数据模型更新也走 EDT。另外,首次显示前发生了数据填充(比如构造函数里查库),建议先frame.setVisible(true)再填充数据,否则某些 LookAndFeel 会因为在窗口未显示时绘制而出现空白面板。这条也是我同事趟出来的,属于典型的「怎么写都对但看起来像坏了」。
6. 进阶:让它从「课设」变成「小系统」——事务、级联联动与备份
课设能跑通增删改查已经及格,但如果你想拿高分或者把它作为简历上的项目,还需要补三块:事务控制、界面联动和数据库备份恢复。这三块是真实系统的基本盘。
事务这块用connection.setAutoCommit(false)控制。典型场景是「入职」操作:插入员工表的同时,部门表的人数统计字段要加一。如果两步之间崩了,数据就不一致了。代码结构是:先关掉自动提交,执行两条 SQL,全部成功后commit(),任一步失败就rollback(),最后在finally里恢复自动提交并关闭连接。这个逻辑在答辩时一讲,效果和单纯增删改查完全不一样。
public boolean hireEmployee(Employee emp, int deptId) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 insertEmployee(conn, emp); updateDeptCount(conn, deptId, 1); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }界面联动说的是「选部门 → 员工列表联动更新」这类交互。做法是在部门下拉框的ItemListener里触发员工表的按部门查询,不要等到用户点了「查询」才去查。这样体验更接近真实系统,代码也只是把refreshTable换成searchByDept而已。如果表之间有关联,比如查出的员工列表双击要弹出工资记录,需要保证员工选中时拿到的是emp_id而不是界面上显示的emp_no,可以在TableModel里隐藏一列emp_id,取值时先拿行号再拿隐藏列。
备份恢复属于「关键时刻救命」的功能。课程设计里不需要做增量备份,但做完一个阶段把数据导出来,是防止自己手误删表的后悔药。命令行下就是两条:
mysqldump -u root -p personnel > personnel_backup.sql mysql -u root -p personnel < personnel_backup.sql导出的.sql文件要看一下编码,用UTF-8打开确认没有乱码。如果数据里有新插入的 emoji 字符,而备份文件头部没有SET NAMES utf8mb4,恢复时也会丢字符。恢复前最好先DROP DATABASE再重新建库,避免旧表结构和新表结构不一致导致导入失败。
我个人做项目的习惯是:每写完一个完整功能模块,先导一次备份,改坏了大不了重来。这个习惯在课设后期特别管用——我在调密码加密逻辑时,曾经把 user 表整成了一条数据都查不出来,如果没有备份,那晚就得通宵了。希望这篇能帮你少绕几个弯,把人事管理系统一次跑通。
本文还有配套的精品资源,点击获取