简介:这是一套基于JSP与MySQL开发的人事管理系统毕业设计项目,面向计算机专业本科生及Java Web初学者,聚焦企业人力资源信息的信息化集成管理,适用于课程设计、毕设选题与Servlet/JSP技术实践。资源包共108个文件,包含12个JSP页面(实现用户交互逻辑)、11个Java源码与11个Class字节码(涵盖AdminService、StaffInfoAdd等核心业务类)、10个HTML静态页、6个CSS样式文件、1个SQL数据库脚本及27张界面截图(JPG),整体压缩后仅2.04MB,轻量易部署。已有1447人学习下载,资源结构完整,含lib目录下的数据库配置与DAO层实现(如BaseDao、AdminDaoImpl),并体现典型MVC分层设计思想。读者可直接导入运行,快速掌握登录校验、员工信息增删改查、管理员权限控制等关键功能模块的代码组织与数据库联动逻辑。
1. 为什么用 JSP + MySQL 做毕设人事管理系统,反而比 Spring Boot 更稳、更易过答辩?
很多同学一听说“毕设要做人事系统”,第一反应是去搜 Spring Boot + Vue 全栈模板,结果两周搭环境、三天调跨域、答辩前夜还在修 MyBatis-Plus 的@TableField注解——这真不是玄学,而是技术选型错位。JSP + MySQL 这套组合,在高校毕设场景里,恰恰是经过十年验证的「低风险高完成度」方案:它不追求工业级解耦,但胜在路径清晰、依赖极简、本地一键可跑;Tomcat 8.5 内置 JSTL,MySQL 5.7 安装包不到 300MB,连虚拟机都不用开;所有代码写在.jsp和.java里,导师查重、调试、改需求时,直接打开记事本就能定位逻辑。这不是倒退,而是对毕设本质的尊重——它要验证的是你对 Web 请求生命周期、数据库事务边界、MVC 分层意识的理解,而不是你能不能把 Spring Security 的 RBAC 权限模型配对。如果你的目标是:两周内交付可演示、可调试、可解释每行代码作用的完整系统,并顺利通过中期检查和终期答辩,那么 JSP + MySQL 不是备选,就是正解。
2. 从零建库到首页渲染:用最简 JSP + Servlet 流程跑通人事管理最小闭环
2.1 创建 MySQL 人事数据库与核心表结构(含字段注释与约束逻辑)
我们不搞复杂范式,按毕设实际需求设计三张表:employee(员工主表)、department(部门表)、position(岗位表)。重点不是 E-R 图多漂亮,而是字段类型选得准、外键有依据、注释能讲清业务含义。
-- 创建数据库(字符集必须为 utf8mb4,否则中文姓名/备注会乱码) CREATE DATABASE IF NOT EXISTS hr_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hr_system; -- 部门表:部门编号为主键,名称唯一且非空 CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE COMMENT '部门名称,如:技术部、人事部', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 岗位表:岗位编号为主键,关联部门,状态字段用于软删除 CREATE TABLE position ( pos_id INT PRIMARY KEY AUTO_INCREMENT, pos_name VARCHAR(50) NOT NULL COMMENT '岗位名称,如:Java 开发工程师、HRBP', dept_id INT NOT NULL COMMENT '所属部门ID,外键关联 department.dept_id', status TINYINT DEFAULT 1 COMMENT '状态:1-启用,0-停用', FOREIGN KEY (dept_id) REFERENCES department(dept_id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 员工主表:核心业务表,含外键、索引、默认值 CREATE TABLE employee ( emp_id VARCHAR(20) PRIMARY KEY COMMENT '员工工号,业务主键,如 EMP2024001', name VARCHAR(20) NOT NULL COMMENT '真实姓名', gender ENUM('男','女') DEFAULT '男' COMMENT '性别,用 ENUM 比 VARCHAR 更安全', birth_date DATE COMMENT '出生日期', phone VARCHAR(15) COMMENT '手机号,允许为空', email VARCHAR(100) UNIQUE COMMENT '邮箱,唯一约束防重复注册', dept_id INT NOT NULL COMMENT '所属部门ID', pos_id INT NOT NULL COMMENT '所属岗位ID', hire_date DATE NOT NULL COMMENT '入职日期', salary DECIMAL(10,2) DEFAULT 0.00 COMMENT '月薪,精确到分', remark TEXT COMMENT '备注信息,支持长文本', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (dept_id) REFERENCES department(dept_id), FOREIGN KEY (pos_id) REFERENCES position(pos_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;关键说明:
VARCHAR(20)作emp_id主键,比INT AUTO_INCREMENT更符合企业工号习惯,也避免答辩时被问“为什么不用自增?”。ENUM('男','女')替代VARCHAR,既防止输入“Male”“female”等不一致值,又省去前端下拉选项与后端校验的映射成本。ON UPDATE CURRENT_TIMESTAMP自动更新update_time,后续做数据变更审计时直接可用,不用在 Java 层手动 set。- 所有
COMMENT必须写,答辩时导师翻源码看到字段注释,会立刻判断你是否真正理解每个字段的业务语义。
2.2 配置 Tomcat + JDBC 连接池(DBCP2),让数据库操作不卡死、不报空指针
JSP 项目最容易翻车的地方,不是 SQL 写错,而是连接没关、池子没配、驱动没加载。我们跳过web.xml里繁琐的<resource-ref>配置,直接用 DBCP2 在ServletContextListener中初始化全局数据源——这是我带过的某高校毕设组统一采用的稳定做法。
第一步:将commons-dbcp2-2.9.0.jar和mysql-connector-java-8.0.33.jar放入WEB-INF/lib/目录(注意版本兼容性:DBCP2 ≥ 2.8 要求 JDK 8+,MySQL 驱动 8.x 必须用com.mysql.cj.jdbc.Driver)。
第二步:新建src/com/hr/listener/DBCPInitListener.java
package com.hr.listener; import org.apache.commons.dbcp2.BasicDataSource; import javax.servlet.ServletContextEvent; import javax.servlet.ServletContextListener; import javax.servlet.annotation.WebListener; @WebListener public class DBCPInitListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { BasicDataSource ds = new BasicDataSource(); ds.setUrl("jdbc:mysql://localhost:3306/hr_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"); ds.setUsername("root"); ds.setPassword("123456"); // 实际使用请改密,毕设演示可保留 ds.setDriverClassName("com.mysql.cj.jdbc.Driver"); ds.setInitialSize(5); // 初始连接数 ds.setMaxTotal(20); // 最大连接数,毕设 20 足够 ds.setMaxIdle(10); // 最大空闲连接 ds.setMinIdle(5); // 最小空闲连接 ds.setTestOnBorrow(true); // 取连接前检测有效性 ds.setValidationQuery("SELECT 1"); // MySQL 心跳检测 SQL ds.setTimeBetweenEvictionRunsMillis(30000); // 每30秒检测空闲连接 // 将数据源存入 ServletContext,全局可取 sce.getServletContext().setAttribute("dataSource", ds); System.out.println("✅ DBCP2 数据源初始化成功,最大连接数:" + ds.getMaxTotal()); } @Override public void contextDestroyed(ServletContextEvent sce) { BasicDataSource ds = (BasicDataSource) sce.getServletContext().getAttribute("dataSource"); if (ds != null) { try { ds.close(); System.out.println("✅ DBCP2 数据源已关闭"); } catch (Exception e) { e.printStackTrace(); } } } }参数说明与血泪经验:
useSSL=false是必须加的,MySQL 8.0 默认强制 SSL,本地开发不配证书必报Communications link failure;serverTimezone=Asia/Shanghai解决The server time zone value 'XXX' is unrecognized错误,这是毕设答辩现场最高频报错之一;testOnBorrow=true+validationQuery="SELECT 1"是防“连接超时自动断开后仍返回失效连接”的后悔药,不加这个,系统空闲 8 小时后首次操作必 500;@WebListener注解替代web.xml配置,更简洁,且 Tomcat 7+ 原生支持,无需担心容器兼容性。
2.3 编写通用 DAO 基类与 EmployeeDAO 实现类(手写 JDBC,不依赖框架)
毕设不鼓励用 Hibernate 或 MyBatis,因为导师想看的是你对PreparedStatement、ResultSet、Connection生命周期的真实掌控力。我们封装一个BaseDAO<T>,用反射处理泛型实体映射,再写EmployeeDAO继承它——这样既体现代码复用能力,又完全暴露底层细节。
src/com/hr/dao/BaseDAO.java:
package com.hr.dao; import com.hr.util.JDBCUtil; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import java.lang.reflect.Field; import java.sql.*; import java.util.ArrayList; import java.util.List; public abstract class BaseDAO<T> { // 从 ServletContext 获取数据源(避免每次 new Connection) protected Connection getConnection(HttpServletRequest request) throws SQLException { ServletContext ctx = request.getServletContext(); DataSource ds = (DataSource) ctx.getAttribute("dataSource"); return ds.getConnection(); } // 通用查询:返回单个对象(如根据 emp_id 查员工) protected T findOne(String sql, Class<T> clazz, Object... params) { try (Connection conn = getConnection(null); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rowToEntity(rs, clazz); } } } catch (Exception e) { e.printStackTrace(); } return null; } // 通用查询:返回列表(如查全部员工) protected List<T> findList(String sql, Class<T> clazz, Object... params) { List<T> list = new ArrayList<>(); try (Connection conn = getConnection(null); PreparedStatement ps = conn.prepareStatement(sql)) { for (int i = 0; i < params.length; i++) { ps.setObject(i + 1, params[i]); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { list.add(rowToEntity(rs, clazz)); } } } catch (Exception e) { e.printStackTrace(); } return list; } // ResultSet → 实体对象(核心反射逻辑,答辩时可逐行讲解) private T rowToEntity(ResultSet rs, Class<T> clazz) throws Exception { T obj = clazz.getDeclaredConstructor().newInstance(); Field[] fields = clazz.getDeclaredFields(); for (Field f : fields) { f.setAccessible(true); // 突破 private 限制 String colName = f.getName(); // 默认列名 = 字段名(要求数据库字段与 Java 字段同名) Object value = rs.getObject(colName); if (value != null) { // 处理日期类型自动转换(避免 getString 后 parse) if (f.getType() == java.util.Date.class || f.getType() == java.time.LocalDate.class) { if (value instanceof Timestamp) { f.set(obj, new java.util.Date(((Timestamp) value).getTime())); } else if (value instanceof Date) { f.set(obj, value); } } else { f.set(obj, value); } } } return obj; } }src/com/hr/dao/EmployeeDAO.java:
package com.hr.dao; import com.hr.entity.Employee; import javax.servlet.http.HttpServletRequest; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.List; public class EmployeeDAO extends BaseDAO<Employee> { // 新增员工(含工号生成逻辑:EMP2024001 格式) public boolean addEmployee(Employee emp, HttpServletRequest request) { String sql = "INSERT INTO employee(emp_id, name, gender, birth_date, phone, email, dept_id, pos_id, hire_date, salary, remark) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; try (Connection conn = getConnection(request); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, generateEmpId(conn)); // 工号自动生成 ps.setString(2, emp.getName()); ps.setString(3, emp.getGender()); ps.setDate(4, emp.getBirthDate() != null ? new java.sql.Date(emp.getBirthDate().getTime()) : null); ps.setString(5, emp.getPhone()); ps.setString(6, emp.getEmail()); ps.setInt(7, emp.getDeptId()); ps.setInt(8, emp.getPosId()); ps.setDate(9, new java.sql.Date(emp.getHireDate().getTime())); ps.setBigDecimal(10, emp.getSalary()); ps.setString(11, emp.getRemark()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } // 生成工号:查询当前最大工号,+1 后格式化(如 EMP2024001 → EMP2024002) private String generateEmpId(Connection conn) throws SQLException { String sql = "SELECT MAX(emp_id) FROM employee WHERE emp_id LIKE 'EMP2024%'"; try (PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { if (rs.next()) { String maxId = rs.getString(1); if (maxId != null && maxId.length() == 10) { int num = Integer.parseInt(maxId.substring(6)) + 1; return String.format("EMP2024%03d", num); } } } return "EMP2024001"; } // 按姓名模糊查询(支持中文搜索) public List<Employee> searchByName(String keyword, HttpServletRequest request) { String sql = "SELECT * FROM employee WHERE name LIKE ?"; return findList(sql, Employee.class, "%" + keyword + "%"); } // 删除员工(软删除:更新 status 字段,此处简化为物理删除,毕设可接受) public boolean deleteById(String empId, HttpServletRequest request) { String sql = "DELETE FROM employee WHERE emp_id = ?"; try (Connection conn = getConnection(request); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, empId); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }为什么坚持手写 JDBC?
- 导师提问“怎么防止 SQL 注入?”——你指着
ps.setString(1, empId)就能答:预编译参数化,不是字符串拼接;- 提问“事务怎么控制?”——你可以说:“新增员工时,如果插入失败,catch 里不 commit,Connection 自动回滚”,并指出
try-with-resources保证资源释放;- 提问“工号怎么保证唯一?”——你展示
generateEmpId()里先查后插的原子性逻辑,并承认“高并发下可能冲突,但毕设单机演示无此压力”,坦诚且专业。
3. JSP 页面分层与 MVC 落地:从 login.jsp 到 employee_list.jsp 的请求链路拆解
3.1 登录流程:Servlet 接收表单 → Service 校验 → 转发到主页(拒绝 scriptlet,用 JSTL + EL)
很多毕设项目把 Java 代码全塞进 JSP,导致页面臃肿、难以维护、答辩时被批“违反 MVC”。正确做法是:JSP 只负责展示,逻辑全在 Servlet 和 Service 层。我们以登录为例,展示标准三层流转。
login.jsp(纯展示层,无 Java 代码):
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>人事系统登录</title> <style> .error { color: red; font-size: 14px; margin-top: 5px; } </style> </head> <body> <div style="width:400px; margin:100px auto; padding:20px; border:1px solid #ccc;"> <h2>人事管理系统登录</h2> <form action="LoginServlet" method="post"> <label>用户名:<input type="text" name="username" required></label><br><br> <label>密码:<input type="password" name="password" required></label><br><br> <c:if test="${not empty errorMsg}"> <div class="error">${errorMsg}</div> </c:if> <input type="submit" value="登录"> </form> </div> </body> </html>src/com/hr/servlet/LoginServlet.java(控制层,只做调度):
package com.hr.servlet; import com.hr.service.UserService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/LoginServlet") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); // 调用 Service 层校验(此处简化为固定账号,实际可查库) if ("admin".equals(username) && "123456".equals(password)) { req.getSession().setAttribute("user", username); resp.sendRedirect("main.jsp"); // 登录成功跳转主页 } else { req.setAttribute("errorMsg", "用户名或密码错误!"); req.getRequestDispatcher("login.jsp").forward(req, resp); // 失败留在登录页 } } }src/com/hr/service/UserService.java(服务层,封装业务规则):
package com.hr.service; // 此处可扩展为查 user 表、校验密码加密、记录登录日志等 public class UserService { // 模拟校验逻辑,毕设可接受;若需真实校验,可在此处调用 UserDAO public boolean validate(String username, String password) { // TODO:查数据库比对加密密码(如 BCrypt) return "admin".equals(username) && "123456".equals(password); } }关键设计点:
login.jsp用<c:if>和${errorMsg}显示错误,而非<% if(...) out.print(...) %>,彻底分离逻辑与视图;LoginServlet不处理任何数据库操作,只调UserService.validate(),体现“控制层不碰数据”的分层意识;resp.sendRedirect("main.jsp")是客户端跳转,URL 变为/main.jsp;而req.getRequestDispatcher(...).forward()是服务端转发,URL 不变——答辩时被问“重定向和转发区别”,这就是最佳答案。
3.2 员工列表页:JSTL 循环 + 分页链接 + 搜索框联动(一行代码实现动态表格)
employee_list.jsp是毕设最常被要求演示的功能页。我们用 JSTLc:forEach渲染列表,用隐藏域传当前页码,用 GET 参数支持搜索,全程无 scriptlet。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>员工列表</title> </head> <body> <h2>员工管理</h2> <!-- 搜索表单 --> <form method="get" style="margin-bottom:15px;"> <input type="text" name="keyword" value="${param.keyword}" placeholder="请输入姓名搜索" style="width:200px;"> <input type="submit" value="搜索"> <a href="employee_list.jsp" style="margin-left:10px;">显示全部</a> </form> <!-- 员工表格 --> <table border="1" width="100%" style="border-collapse:collapse;"> <tr> <th>工号</th><th>姓名</th><th>性别</th><th>部门</th><th>岗位</th><th>入职日期</th><th>月薪</th><th>操作</th> </tr> <c:choose> <c:when test="${empty employeeList}"> <tr><td colspan="8" align="center">暂无员工数据</td></tr> </c:when> <c:otherwise> <c:forEach items="${employeeList}" var="emp"> <tr> <td>${emp.empId}</td> <td>${emp.name}</td> <td>${emp.gender}</td> <td>${emp.deptName}</td> <!-- 注意:deptName 是在 Servlet 中查出并 set 到 request 的 --> <td>${emp.posName}</td> <td><fmt:formatDate value="${emp.hireDate}" pattern="yyyy-MM-dd"/></td> <td>¥${emp.salary}</td> <td> <a href="EditEmployeeServlet?empId=${emp.empId}">编辑</a> | <a href="DeleteEmployeeServlet?empId=${emp.empId}" onclick="return confirm('确定删除?')">删除</a> </td> </tr> </c:forEach> </c:otherwise> </c:choose> </table> <!-- 分页导航(简化版,仅上一页/下一页) --> <div style="margin-top:15px;"> <c:if test="${currentPage > 1}"> <a href="employee_list.jsp?page=${currentPage-1}&keyword=${param.keyword}">上一页</a> </c:if> <c:if test="${hasNextPage}"> <a href="employee_list.jsp?page=${currentPage+1}&keyword=${param.keyword}">下一页</a> </c:if> </div> </body> </html>src/com/hr/servlet/EmployeeListServlet.java(支撑列表页的数据准备):
package com.hr.servlet; import com.hr.dao.DepartmentDAO; import com.hr.dao.EmployeeDAO; import com.hr.dao.PositionDAO; import com.hr.entity.Employee; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; @WebServlet("/employee_list.jsp") public class EmployeeListServlet extends HttpServlet { private EmployeeDAO empDAO = new EmployeeDAO(); private DepartmentDAO deptDAO = new DepartmentDAO(); private PositionDAO posDAO = new PositionDAO(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String keyword = req.getParameter("keyword"); String pageStr = req.getParameter("page"); int currentPage = 1; if (pageStr != null && !pageStr.trim().isEmpty()) { currentPage = Integer.parseInt(pageStr); } // 查询员工列表(带搜索) List<Employee> empList; if (keyword != null && !keyword.trim().isEmpty()) { empList = empDAO.searchByName(keyword, req); } else { empList = empDAO.findAll(req); // findAll 方法需在 EmployeeDAO 中补充 } // 关联查询部门名、岗位名(为 JSP 渲染准备) for (Employee emp : empList) { emp.setDeptName(deptDAO.findById(emp.getDeptId(), req).getDeptName()); emp.setPosName(posDAO.findById(emp.getPosId(), req).getPosName()); } // 设置分页参数(简化:不查总数,只判断是否有下一页) final int PAGE_SIZE = 10; req.setAttribute("employeeList", empList.subList(0, Math.min(PAGE_SIZE, empList.size()))); req.setAttribute("currentPage", currentPage); req.setAttribute("hasNextPage", empList.size() > PAGE_SIZE); req.getRequestDispatcher("employee_list.jsp").forward(req, resp); } }为什么这个写法能过答辩?
- 表格中
${emp.deptName}不是数据库字段,而是 Servlet 中主动查出并 set 的,证明你理解“关联查询要在 Service/Servlet 层做,不能靠前端拼接”;<fmt:formatDate>格式化日期,避免new Date().toString()输出冗长字符串,体现细节把控;onclick="return confirm(...)"是最朴素有效的删除确认,比弹窗 JS 插件更易解释、更少出错;- 分页虽简化,但
currentPage和hasNextPage两个变量已构成分页核心要素,导师不会苛求 PageHelper。
4. 常见问题排查:JSP + MySQL 毕设项目上线即崩的 4 个真实翻车现场
4.1 现象:页面显示HTTP Status 500 – Internal Server Error,控制台报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:MySQL 驱动 JAR 包未放入WEB-INF/lib/,或放入了但版本不匹配(如用了 5.1 驱动连 MySQL 8.0)。更隐蔽的情况是:IDEA 中Project Structure → Modules → Dependencies里勾选了 JAR,但Artifact打包时未勾选Include in project build,导致 WAR 包里实际没有该 JAR。
解决:
- 确认
WEB-INF/lib/下存在mysql-connector-java-8.0.33.jar(不要用 5.x); - 在 IDEA 中打开
File → Project Structure → Artifacts,选中你的 Web Application:explode,检查右侧Output Layout树形结构中WEB-INF/lib/下是否包含该 JAR; - 若缺失,点击
+ → Library Files,添加对应 JAR,并勾选Include in project build; - 重新
Build → Build Artifacts → Rebuild。
4.2 现象:登录成功后跳转main.jsp,但地址栏仍是/LoginServlet,且刷新页面提示 404
原因:LoginServlet中用了req.getRequestDispatcher("main.jsp").forward(req, resp)(服务端转发),而非resp.sendRedirect("main.jsp")(客户端重定向)。转发时 URL 不变,浏览器地址栏仍是原 Servlet 路径;一旦刷新,会再次提交 POST 请求,而main.jsp并不处理 POST,故 404。
解决:
- 登录成功必须用
sendRedirect,确保 URL 变为/main.jsp; - 若需在
main.jsp中显示欢迎信息,可在重定向前req.getSession().setAttribute("user", username),然后在main.jsp中用${sessionScope.user}读取。
4.3 现象:新增员工时,中文姓名、备注显示为??,数据库里存的是乱码
原因:四个环节任一未设 UTF-8:① JSP 页面<%@ page contentType="text/html;charset=UTF-8" %>缺失;② MySQL 数据库/表字符集不是utf8mb4;③ JDBC URL 未加characterEncoding=utf8;④ Tomcatconf/server.xml中Connector未设URIEncoding="UTF-8"。
解决(四步全做):
- 所有 JSP 顶部加
<%@ page contentType="text/html;charset=UTF-8" %>; - 创建库时执行
CREATE DATABASE ... CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - JDBC URL 改为:
jdbc:mysql://localhost:3306/hr_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4; - 修改
conf/server.xml,找到<Connector port="8080" ... />,添加属性URIEncoding="UTF-8"。
4.4 现象:点击“编辑”链接进入EditEmployeeServlet,页面报NullPointerException,堆栈指向empDAO.findOne(...)返回 null
原因:EditEmployeeServlet中req.getParameter("empId")获取的值为 null,因为链接写成了<a href="EditEmployeeServlet?empId=${emp.empId}">,但emp.empId是String类型,JSTL 会自动转义特殊字符(如&),若工号含&会被截断。更常见的是:employee_list.jsp的href路径写错,如写成EditEmployeeServlet?empId=${emp.empId(漏了}),导致 URL 参数无效。
解决:
- 在
EditEmployeeServlet开头加日志:System.out.println("Received empId: [" + empId + "]");,确认是否为 null; - 检查 JSP 中所有
href,确保${emp.empId}完整闭合,且empId字段值不含 URL 特殊字符(毕设工号用EMP2024001格式可规避); - 在 Servlet 中增加判空:
if (empId == null || empId.trim().isEmpty()) { req.setAttribute("errorMsg", "员工ID不能为空"); req.getRequestDispatcher("error.jsp").forward(req, resp); return; }。
5. 让毕设答辩加分的 3 个实战技巧:从“能跑”到“像模像样”
5.1 用 CSS + Bootstrap 4 快速美化界面,不写一行 JS 却显专业
毕设答辩不是前端大赛,但一个干净的界面能让导师多看你两眼。别碰 Vue 或 React,就用 Bootstrap 4 的 CDN,5 分钟升级颜值。
在main.jsp<head>中加入:
<!-- Bootstrap 4.6.2 CSS CDN --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"> <!-- 可选:字体图标 --> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.0/font/bootstrap-icons.css">然后把原始表格改成 Bootstrap 样式:
<table class="table table-striped table-hover"> <thead class="thead-dark"> <tr> <th scope="col">工号</th> <th scope="col">姓名</th> <th scope="col">性别</th> <th scope="col">部门</th> <th scope="col">岗位</th> <th scope="col">入职日期</th> <th scope="col">月薪</th> <th scope="col">操作</th> </tr> </thead> <tbody> <c:forEach items="${employeeList}" var="emp"> <tr> <td>${emp.empId}</td> <td>${emp.name}</td> <td><span class="badge badge-primary">${emp.gender}</span></td> <td>${emp.deptName}</td> <td>${emp.posName}</td> <td><fmt:formatDate value="${emp.hireDate}" pattern="yyyy-MM-dd"/></td> <td class="text-success">¥${emp.salary}</td> <td> <a href="EditEmployeeServlet?empId=${emp.empId}" class="btn btn-sm btn-outline-primary"><i class="bi bi-pencil"></i> 编辑</a> <a href="DeleteEmployeeServlet?empId=${emp.empId}" class="btn btn-sm btn-outline-danger" onclick="return confirm('确定删除 ${emp.name}?')"><i class="bi bi-trash"></i> 删除</a> </td> </tr> </c:forEach> </tbody> </table>效果与价值:
table-striped带斑马纹,table-hover悬停高亮,badge标签突出性别,btn-sm控制按钮大小——这些全是 Bootstrap 内置类,无需写 CSS;bi-pencil和bi-trash是图标,比文字链接更直观;onclick中的提示语带上${emp.name},体现你对 EL 表达式的熟练运用;- 导师看到
class="text-success"给月薪加绿色,会认为你注重用户体验细节,而非只会堆功能。
5.2 在web.xml中配置全局错误页,把 404/500 变成定制化提示
默认的 Tomcat 错误页太生硬,一个定制页能体现工程素养。在WEB-INF/web.xml中添加:
<error-page> <error-code>404</error-code> <location>/error_404.jsp</location> </error-page> <error-page> <error-code>500</error-code> <location>/error_500.jsp</location> </error-page>error_404.jsp示例:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>页面未找到</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css"> </head> <body class="bg-light"> <div class="container mt-5"> <div class="row justify-content-center"> <div class="col-md-6 text-center"> <h1 class="display-1 text-danger">404</h1> <h2 class="mt-3">页面未找到</h2> <p class="lead text-muted">您访问的地址可能已删除或输入有误。</p> <a href="employee_list.jsp" class="btn btn-primary btn-lg"><i class="bi bi-arrow-left"></i> 返回员工列表</a> </div> </div> </div> </body> </html>本文还有配套的精品资源,点击获取