简介:这是一份面向Java Web初学者与课程设计学习者的完整项目资料,围绕JSP技术构建人事管理系统,适合需要完成毕业设计、课程实训或想系统理解Java Web开发流程的读者。压缩包为zip格式,约1.11MB,内含项目报告、任务书、中英文翻译文档及完整源代码,报告覆盖需求分析、系统架构、数据库设计与测试总结,任务书则给出目标与进度安排,源代码包含Servlet、JSP页面及MVC各层组件。项目涉及员工信息管理、考勤记录、薪资计算与绩效评估等核心模块,读者可从中学习JSP内置对象、Servlet协同、JDBC数据库操作、MVC设计模式以及HTML、CSS与JavaScript的前端展示,并理解ER模型与软件工程方法。目前已有54人学习下载,适合作为Java Web入门到实践的参考案例。
1. 从一份 JSP 人事管理系统源码说起:谁还在用它,怎么跑起来
招聘季一过,总有人翻出硬盘里那份基于jsp的人事管理系统设计与实现(源代码+项目报告).zip。它多半是课程设计或毕设产物,技术栈是 JSP + Servlet + JDBC + MySQL,跑在 Tomcat 上,没有 Spring,没有前后端分离,页面里还嵌着<% %>脚本片段。很多人第一反应是"这玩意儿过时了",但真到要交报告、要答辩、要在一个老项目上做二次开发时,它反而是最省事的起点——依赖少、结构直白、改一个字段能立刻看到效果。
这篇笔记不吹这套架构多先进,只讲清楚三件事:这套 JSP 人事管理系统到底由哪些模块组成、怎么在本地把它跑通、以及改需求时哪些地方最容易翻车。适合手里已经拿到源码包、想快速复现的在校生,也适合被派去维护一套老 JSP 系统的 Java 开发。读完你应该能独立完成环境搭建、数据库导入、登录验证和一次完整的员工信息增删改查。
2. 拆开这套 JSP 人事管理系统:模块划分与技术选型
2.1 典型目录结构与各层职责
拿到源码包先别急着导入 IDE,用文件管理器把目录展开看一遍,心里有个地图。JSP 时代的项目结构高度雷同,常见形态是这样:
hrms/ ├── src/ # Java 源码 │ └── com/hrms/ │ ├── dao/ # 数据库访问层,写 JDBC │ ├── entity/ # 实体类,和表字段一一对应 │ ├── servlet/ # 控制器,接收请求、调 DAO、转发 │ └── util/ # DBUtil 等工具类 ├── WebContent/ 或 web/ │ ├── css/ js/ images/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 与过滤器映射 │ │ └── lib/ # mysql-connector、jstl 等 jar │ ├── login.jsp │ ├── index.jsp │ └── emp/ dept/ salary/ # 按模块分的页面 └── sql/hrms.sql # 建库建表脚本dao层是这套系统的核心,所有 SQL 都堆在这里。servlet层负责取参数、做简单校验、调 DAO、把结果塞进 request 再forward到 JSP。JSP 页面既做展示又做表单提交,脚本片段里偶尔还会直接调 DAO——这是老项目的通病,改的时候要留意。
提示:如果
src下没有dao只有servlet,说明 SQL 直接写在 Servlet 里了,这种项目改动量会大很多,先做好心理准备。
2.2 为什么是 JSP + Servlet + JDBC 这套组合
从今天的视角看,这套组合确实笨重,但它有三个绕不开的优点。第一,零框架依赖,只要 JDK 和 Tomcat 就能跑,不需要配 Maven 私服、不需要联网拉依赖,对网络受限的机房环境特别友好。第二,请求流转全在明面上,浏览器发一个请求,web.xml里查映射,进 Servlet,调 DAO,转发 JSP,整条链路没有黑匣子,调试时打断点一目了然。第三,和"项目报告"这种交付物天然契合,报告里画个三层架构图、贴几段 Servlet 代码,逻辑自洽。
选型上真正需要你决策的只有两处:数据库连接方式和页面数据展示方式。连接方式我一般直接用DBUtil手写DriverManager,不引入连接池,因为课程设计级别的并发量根本压不出连接池的价值,反而多一个配置文件多一个出错点。数据展示优先用 JSTL 的<c:forEach>,比脚本片段干净,但如果源码里已经全是<% %>,就别强行重构,保持一致更省事。
2.3 数据库表设计:员工、部门、薪资三张主表怎么关联
人事系统的表设计万变不离其宗,核心是三张表加一张用户表。下面是我从这类项目里总结出的最小可用结构,字段名按常见命名习惯给:
| 表名 | 关键字段 | 说明 |
|---|---|---|
t_user | id, username, password, role | 登录账号,role 区分管理员/普通用户 |
t_dept | dept_id, dept_name, dept_head | 部门表,dept_head 存负责人姓名 |
t_employee | emp_id, emp_name, gender, birthday, dept_id, position, phone, entry_date | 员工主表,dept_id 外键关联部门 |
t_salary | salary_id, emp_id, base_salary, bonus, pay_month | 薪资表,emp_id 关联员工 |
关联关系是t_employee.dept_id → t_dept.dept_id,t_salary.emp_id → t_employee.emp_id。查询员工列表时要LEFT JOIN t_dept把部门名带出来,否则页面上只能显示一个数字 ID,体验很差。这里有个容易忽略的点:t_employee的dept_id建议允许为 NULL,因为新员工入职时可能还没分配部门,如果设成 NOT NULL,插入就会失败。
CREATE TABLE t_employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(50) NOT NULL, gender CHAR(2) DEFAULT '男', birthday DATE, dept_id INT, position VARCHAR(50), phone VARCHAR(20), entry_date DATE, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES t_dept(dept_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;建表语句里utf8mb4别写成utf8,否则员工姓名里出现生僻字或 emoji 会插入失败。外键约束在课程设计里可以保留,显得规范;但如果后续要做批量导入,外键会拖慢速度,那时再考虑去掉。
3. 本地跑通的最小步骤:从建库到登录成功
3.1 环境准备与版本匹配
跑这套系统需要三样东西:JDK、Tomcat、MySQL。版本搭配是新手最容易翻车的地方,我踩过的坑是 JDK 17 配 Tomcat 8 直接启动报错。稳妥组合如下:
| 组件 | 推荐版本 | 备注 |
|---|---|---|
| JDK | 8 或 11 | JSP 老项目对高版本 JDK 兼容差 |
| Tomcat | 8.5 或 9.0 | 不要用 Tomcat 10,包名从 javax 变 jakarta,全崩 |
| MySQL | 5.7 或 8.0 | 8.0 要改驱动类和连接串 |
| 驱动 jar | mysql-connector-java 5.1.x 或 8.0.x | 与 MySQL 版本对应 |
Tomcat 10 的坑必须单独强调:它把javax.servlet.*全部换成了jakarta.servlet.*,而这份源码里的 import 语句几乎不可能已经改过,部署上去就是一片ClassNotFoundException。所以要么用 Tomcat 9,要么手动全局替换包名,后者工作量大且容易漏。
3.2 导入数据库并修改连接配置
第一步,用命令行或 Navicat 把hrms.sql导进去:
mysql -u root -p -e "CREATE DATABASE hrms DEFAULT CHARSET utf8mb4;" mysql -u root -p hrms < sql/hrms.sql第二步,找到DBUtil.java,改连接参数。这是全项目唯一需要改配置的地方:
public class DBUtil { // MySQL 8.0 用 com.mysql.cj.jdbc.Driver,5.7 用 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // 8.0 必须带时区和 SSL 参数,否则连接报错 private static final String URL = "jdbc:mysql://localhost:3306/hrms?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PWD = "你的密码"; public static Connection getConn() throws Exception { Class.forName(DRIVER); return DriverManager.getConnection(URL, USER, PWD); } }serverTimezone不写会报The server time zone value is unrecognized,这是 MySQL 8.0 最经典的报错。allowPublicKeyRetrieval=true是 8.0 默认加密方式导致的,不加会提示Public Key Retrieval is not allowed。这两个参数记住就行,不用深究原理。
3.3 部署到 Tomcat 并验证登录
把项目导入 Eclipse 或 IDEA,确认WebContent/WEB-INF/lib下有mysql-connector-java-x.x.x.jar和jstl.jar,然后配置 Tomcat 运行。启动后访问http://localhost:8080/hrms/login.jsp,用 SQL 脚本里预置的账号登录,常见是admin/123456。
登录逻辑在LoginServlet里,核心就一句查询:
String sql = "SELECT * FROM t_user WHERE username=? AND password=?";如果登录后跳转 404,先看web.xml里<url-pattern>和表单action是否一致;如果提示账号密码错误但数据库里明明有数据,检查密码字段是不是被 MD5 加密过而表单传的是明文。这类项目一半用明文一半用 MD5,没有统一标准,只能打开表看实际存的值。
注意:登录成功后如果页面样式全丢,是 CSS 路径问题。JSP 里写相对路径
css/style.css,在/emp/list.jsp这种二级目录下会解析成/emp/css/style.css,改成${pageContext.request.contextPath}/css/style.css即可。
4. 增删改查落地:Servlet 与 JSP 的配合细节
4.1 员工列表分页查询的实现
列表页是使用频率最高的功能,也是最容易写崩的地方。不分页的话,几百条数据一次性渲染,页面卡到怀疑人生。分页的核心是LIMIT加两个参数:
// pageNo 当前页码,pageSize 每页条数 public List<Employee> listByPage(int pageNo, int pageSize) throws Exception { List<Employee> list = new ArrayList<>(); String sql = "SELECT e.*, d.dept_name FROM t_employee e " + "LEFT JOIN t_dept d ON e.dept_id = d.dept_id " + "ORDER BY e.emp_id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (pageNo - 1) * pageSize); // 偏移量从 0 开始 ps.setInt(2, pageSize); ResultSet rs = ps.executeQuery(); while (rs.next()) { Employee emp = new Employee(); emp.setEmpId(rs.getInt("emp_id")); emp.setEmpName(rs.getString("emp_name")); emp.setDeptName(rs.getString("dept_name")); // 关联出来的部门名 list.add(emp); } } return list; }偏移量公式(pageNo - 1) * pageSize必须记牢,写成pageNo * pageSize会漏掉第一页数据。ORDER BY一定要加,否则 MySQL 返回顺序不保证,翻页时会出现同一条记录重复出现或消失,这种玄学问题排查起来很费时间。总记录数用单独的SELECT COUNT(*)查,别想着一次查询搞定。
4.2 新增与修改共用一个表单页
新增和修改的字段完全一样,没必要写两个 JSP。常见做法是共用一个empForm.jsp,靠 URL 上有没有empId参数区分:
// Servlet 中判断 String empId = request.getParameter("empId"); if (empId != null && !empId.isEmpty()) { // 有 id,是修改,先查出来回填 Employee emp = empDao.getById(Integer.parseInt(empId)); request.setAttribute("emp", emp); } request.getRequestDispatcher("/emp/empForm.jsp").forward(request, response);JSP 里用 EL 表达式回填:value="${emp.empName}"。当emp为 null 时 EL 会输出空字符串,不会报错,正好满足新增场景。这个技巧省掉一个页面,也省掉两份校验逻辑。
4.3 删除操作为什么要用 POST 而不是 GET
很多源码里删除写成<a href="empDelete?id=5">删除</a>,点一下就把数据删了。这在功能上没问题,但有两个隐患:一是浏览器预加载或爬虫可能误触发删除,二是没法加二次确认。正确做法是改成表单提交:
<form action="empDelete" method="post" onsubmit="return confirm('确认删除该员工?');"> <input type="hidden" name="empId" value="${emp.empId}"> <button type="submit">删除</button> </form>onsubmit返回 false 时表单不提交,这是最轻量的二次确认方案,不需要引入任何弹窗库。删除时还要注意外键:如果该员工在t_salary里有薪资记录,直接删t_employee会因外键约束失败。要么先删薪资记录,要么把外键改成ON DELETE CASCADE,前者更安全,后者更省事,看你的数据重要性决定。
5. 避坑与排查:这套 JSP 人事系统最常见的 5 个翻车点
5.1 中文乱码:POST 和 GET 要分开治
现象:表单提交后数据库里存的是????,或者页面显示问号。原因:JSP 默认用 ISO-8859-1 解码。解决:POST 请求在 Servlet 第一行加request.setCharacterEncoding("UTF-8");GET 请求改 Tomcat 的server.xml,在 Connector 上加URIEncoding="UTF-8"。数据库连接串里的characterEncoding=utf8和建表的utf8mb4也要对齐,三处缺一不可。
5.2 空指针异常:DAO 返回 null 没判空
现象:点开员工详情页直接 500,日志里NullPointerException。原因:getById查不到记录返回 null,Servlet 里直接emp.getEmpName()。解决:拿到对象先判空,为空就转发到错误页或列表页。更稳妥的做法是 DAO 层查不到时返回一个空对象而不是 null,但老项目里改这个动静大,先在调用处判空更实际。
5.3 修改后数据没变:事务没提交
现象:页面提示修改成功,刷新数据库还是旧值。原因:JDBC 默认自动提交,但如果代码里手动开了conn.setAutoCommit(false)却忘了conn.commit(),改动就丢了。解决:检查 DAO 里有没有手动事务,有就补上 commit,异常分支里补 rollback。用 try-with-resources 时连接会自动关闭,未提交的事务会被回滚,所以这个坑特别隐蔽。
5.4 部署后 404:web.xml 映射和访问路径对不上
现象:Tomcat 启动正常,访问任何页面都 404。原因:web.xml里<url-pattern>写的是/login,浏览器访问的是/LoginServlet,或者项目 context path 不是hrms。解决:看 Tomcat 启动日志里项目部署的 context path,再看web.xml的映射,两边对齐。IDEA 里可以在 Run Configuration 的 Deployment 页签确认 Application context。
5.5 日期格式报错:String 和 Date 转换失败
现象:提交生日或入职日期时抛IllegalArgumentException。原因:表单传的是2024-01-01字符串,代码里直接new java.sql.Date(...)或SimpleDateFormat格式不匹配。解决:统一用SimpleDateFormat("yyyy-MM-dd")解析,注意parse会抛ParseException,要 try-catch。如果表单用的是<input type="date">,提交格式固定是yyyy-MM-dd,按这个格式解析就不会错。
6. 二次开发进阶:给这套系统加一个部门人数统计
跑通之后,真正体现价值的是按需扩展。我拿"部门人数统计"举例,这是人事系统里最常被要求加的功能,也能顺带把 JSTL 和 SQL 聚合练一遍。
需求是在部门列表页每个部门后面显示当前人数。最笨的做法是循环里再查一次数据库,N 个部门查 N+1 次,部门一多就慢。正确做法是一条 SQL 用GROUP BY搞定:
SELECT d.dept_id, d.dept_name, COUNT(e.emp_id) AS emp_count FROM t_dept d LEFT JOIN t_employee e ON d.dept_id = e.dept_id GROUP BY d.dept_id, d.dept_name ORDER BY emp_count DESC;这里必须用LEFT JOIN而不是INNER JOIN,否则人数为 0 的部门会直接从列表里消失,这是统计类需求最常见的逻辑错误。COUNT(e.emp_id)而不是COUNT(*),因为COUNT(*)会把 LEFT JOIN 产生的 NULL 行也算成 1,空部门会显示 1 人。
DAO 里把emp_count读进一个扩展字段,JSP 用<c:forEach>渲染:
<table> <tr><th>部门</th><th>人数</th></tr> <c:forEach items="${deptList}" var="d"> <tr> <td>${d.deptName}</td> <td>${d.empCount}</td> </tr> </c:forEach> </table>验证方法很直接:先在数据库里手动删掉某个部门的所有员工,刷新页面看该部门是否还显示且人数为 0;再新增一个员工,看对应部门人数是否加 1。两步都通过,说明 JOIN 和聚合逻辑没问题。
再往深一层,如果要做"按部门筛选员工",别在 JSP 里用 JavaScript 过滤,数据量大时前端扛不住。正确做法是给列表 Servlet 加一个deptId参数,SQL 里动态拼WHERE:
StringBuilder sql = new StringBuilder( "SELECT e.*, d.dept_name FROM t_employee e " + "LEFT JOIN t_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); } sql.append(" ORDER BY e.emp_id DESC LIMIT ?, ?");WHERE 1=1是个老技巧,为的是后续条件都能以AND开头,不用判断是不是第一个条件。参数用List收集再统一setObject,避免下标错乱。这套写法在 JSP 项目里足够用,不需要引入 MyBatis 动态 SQL。
最后说个习惯:每次改完 DAO,我都会先在数据库客户端把 SQL 单独跑一遍,确认结果集对了再回代码里调。JSP 项目没有单元测试的传统,这个手动验证就是最后的后悔药。这套系统技术不新,但把上面这些细节吃透,改需求时能少熬好几个夜。希望帮到你。
本文还有配套的精品资源,点击获取