☰
JSP+MySQL毕设实战:人事管理系统完整开发与避坑指南
2026/10/6 16:43:32 网站建设 项目流程

简介:这套人事管理系统是基于JSP+Servlet+MySQL的JavaWeb毕业设计项目,也是ERP中的人力资源管理模块实现。系统围绕企业人事信息化场景,提供管理员登录注册、登录校验、员工信息新增等核心功能,并将数据访问、业务处理与页面显示分离,非常适合作为计算机相关专业毕设选题或课程设计参考。资源包共108个文件,压缩后仅2.04MB,内容涵盖12个JSP页面、11个Java源文件及对应class文件、HTML/CSS/JS静态资源、SQL数据库脚本和项目配置;同时附带27张JPG界面截图,便于快速比对页面实现效果,个别DS_Store类垃圾文件不影响使用。项目数据库文件位于lib包下,导入IDE后即可初始化数据并运行,配合源码可完整梳理从前端表单到Servlet控制层、Service业务层和DAO持久层的调用链。目前已有1447人学习/下载,适合需要快速理解JavaWeb分层架构、参考人事管理表设计或进行二次开发的开发者。

1. 人事管理系统毕设选题:JSP+MySQL 这条路还走不走得通

人事管理系统几乎是 JavaWeb 课程设计和毕业设计里最经典的选题,没有之一。它的大小恰好卡在一个舒服的位置:数据表三五张、页面十来页、增删改查齐全,既能完整展示 JSP+Servlet+MySQL 这套传统技术栈,又不会在答辩前把自己逼到通宵。选题本身不新鲜,但很多人真正卡住的地方不是「会不会写」,而是「装不上、连不通、跑起来全是乱码」。这篇文章我把从 MySQL 安装配置到 JSP 页面联调的完整路径拆开写清楚,参数、命令、坑都放在对应位置,顺着走一遍就能交出一套能演示、能答辩、老师愿意往下问的项目。

2. 为什么 2025 年还有人用 JSP 做毕设:这套技术栈的真实收益与边界

2.1 JSP 不是旧,是知识覆盖面大

很多人在选题时会纠结一个很现实的问题:现在校招都在聊 Spring Boot、微服务,我做一个 JSP 项目是不是等于自断前程?这个焦虑我能理解,但你要把它拆开看。毕设和简历项目是两码事。毕设的核心目标是让答辩老师快速确认三件事:你懂 HTTP 请求是怎么被处理的、你懂数据库建模和 SQL、你懂前端页面和后端数据是怎么对接的。JSP 技术栈恰好把这三件事全部暴露在明面上,没有任何封装可以藏住你的不懂。

JSP 页面本质上是一个 Servlet。容器会把.jsp文件翻译成一个 Java 类,再编译成 servlet 执行。这意味着你在 JSP 里写的<% %>脚本片段、${}EL 表达式、<c:forEach>标签,最终都会变成 Java 代码在服务器上跑。理解了这一层,你在排查「为什么页面显示空白」「为什么变量取不到值」的时候,脑子里就会有一个清晰的模型:先看 JSP 翻译出来的 Java 源码,再看编译后的 class 文件,最后看运行时数据。这个排查路径在你以后写任何服务端渲染页面时都通用。

相比之下,Spring Boot 的自动装配把 Tomcat 容器、DispatcherServlet、视图解析器全替你配好了,你写一个return "login"页面就能跳转,但真要问你「这个字符串是怎么变成/login.jsp的」,很多人答不上来。答辩老师往深里问三句就露馅。JSP 项目没有这个问题,因为每一步跳转、每一次转发、每一个数据绑定都是你手写出来的,你能讲清楚的东西远远多于框架帮你代劳的东西。

2.2 JSP+Servlet 与 Spring Boot 的选型界限在哪

我不反对你用 Spring Boot 做毕设,但我反对你在 JSP 和 Spring Boot 之间犹豫不决时选了一个自己说不清的。先把两种方案的差异摆在一张表里,你就知道边界了:

对比维度JSP + Servlet + JDBCSpring Boot + Thymeleaf/模板
学习曲线平缓,知识全透明陡峭,需要理解 IoC/自动装配
答辩深度适合讲清原理适合讲工程化实践
代码量偏多,DAO 层要手写偏少,MyBatis/JPA 接管
排错难度异常栈直观代理对象、切面导致栈很深
环境要求JDK 8 + Tomcat 9 + MySQLJDK 17 + Maven + 大量依赖
适合场景课程设计、本科毕设有实习经验、能讲清框架的人

我的建议很直接:如果你现在还在问「JSP 和 MySQL 怎么连起来」,那就选 JSP,因为你现在最重要的不是项目有多新,而是把 JavaWeb 这条主链路彻底走通一次。如果你已经独立写过三五个成套项目,再拿 JSP 做毕设确实有点浪费时间,那就去写 Spring Boot 加 Vue 分离式。毕设题目从来不是越新越好,是越你能讲清楚越好。

从技术演进的角度看,JSP 确实已经不是主流方案,但它在教学体系里依然是理解 MVC 最直接的载体。SUN 公司当年设计 JSP 的初衷就是让页面开发和业务逻辑分离——JSP 管展示,Servlet 管控制,JavaBean 管数据。这个 MVC 思想不会过时,你日后写任何后端框架都能感受到它的影子。你做 JSP 毕设,买的不是未来三五年能用在生产环境的技术,而是这套 MVC 思维和一次完整的开发演练,这个账要算清楚。

3. 把环境立住:MySQL 8.0 安装配置与人事系统三张表设计

3.1 MySQL 8.0 安装的四个关键参数,省得后面返工

环境问题占了 JSP 毕设排障的至少一半比例。我接手过好几个卡在「MySQL 装好了但项目连不上」的求助,追下去全是安装阶段埋的雷。先从下载说起:不要下.msi图形化安装版本,去官网下 ZIP 压缩包版本。ZIP 包干净、可重复、卸载时删文件夹就行,对毕设这种一次性的环境足够了。你搜「mysql 安装教程」能看到各种版本,认准 8.0.x 的 ZIP 包。

解压之后在根目录新建my.ini,这是我每次都会用的一套最小配置:

[mysqld] # 端口号,3306 被占用时再改这里 port=3306 # 安装目录,改成你自己的实际路径 basedir=D:/mysql-8.0.40-winx64 # 数据目录,不要放在 C 盘,避免权限问题 datadir=D:/mysql-8.0.40-winx64/data # 字符集必须整套指定,否则建表默认 latin1 character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci # 时区问题:JDBC 连接串里也要保持一致 default-time-zone=+08:00 # 本地开发关闭严格模式,避免 date 字段默认值报错 sql_mode=STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION max_connections=200 [client] default-character-set=utf8mb4

这段配置里character-set-server和collation-server是最容易踩坑的地方。很多人装完 MySQL 不去动字符集,结果建表默认latin1,JSP 页面往数据库里插中文变成???,然后到处找「中文乱码」的解决办法。其实根就在这个 ini 文件里。default-time-zone=+08:00也是个大坑,如果你不写,MySQL 8.0 默认用 UTC 时区,你now()出来的时间比北京时间少 8 小时,而你 JDBC 连接串里如果再传一个serverTimezone=UTC,两边一致倒是没问题,但一旦混用就全乱了。

接下来是初始化和管理命令,用管理员身份打开 CMD,顺序执行:

# 进入 MySQL 的 bin 目录 cd /d D:\mysql-8.0.40-winx64\bin # 初始化数据目录,8.0 必须做这一步,否则服务起不来 mysqld --initialize-insecure # 安装 Windows 服务,服务名建议用 mysql80,避免和你以后装的版本冲突 mysqld --install mysql80 # 启动服务 net start mysql80 # 登录,--initialize-insecure 生成了空密码 root 账号 mysql -uroot -p

--initialize-insecure的含义是初始化数据目录并且 root 账号为空密码。千万别用--initialize,那个会生成一个随机密码,第一次登录还要去data目录下的.err日志里翻,对毕设来说没必要。执行完net start mysql80如果报「服务无法启动」,先去data目录下的主机名.err文件看错误,最常见的两个原因一个是my.ini路径写错导致 basedir 找不到,另一个是data目录已经被初始化过,删掉data文件夹重新执行初始化即可。

连接上以后记得改密码和允许远程访问,这一步平时没人提,等你写完项目发现在另一台电脑上连不上自己的库就晚了:

-- 把 root 密码改成你项目里要用的,比如 123456 ALTER USER 'root'@'localhost' IDENTIFIED BY '123456'; -- 创建远程访问账号,方便你从别的机器连数据库调试 CREATE USER 'root'@'%' IDENTIFIED BY '123456'; GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION; FLUSH PRIVILEGES;

3.2 人事系统的表设计:三张表建好,后面少写一半重复代码

人事管理系统的业务其实就两块:管人和管部门。绝大多数增删改查都围绕这两块展开。所以核心表设计我建议控制在三张表:部门表、员工表、用户表。

CREATE DATABASE hrms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hrms; -- 部门表 CREATE TABLE dept ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '部门ID', name VARCHAR(50) NOT NULL UNIQUE COMMENT '部门名称', manager VARCHAR(20) DEFAULT NULL COMMENT '负责人', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB COMMENT='部门表'; -- 员工表 CREATE TABLE employee ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '员工ID', emp_no VARCHAR(20) NOT NULL UNIQUE COMMENT '工号', name VARCHAR(20) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT '男' COMMENT '性别', birthday DATE DEFAULT NULL COMMENT '出生日期', dept_id INT COMMENT '所属部门ID', position VARCHAR(30) DEFAULT NULL COMMENT '职位', salary DECIMAL(10,2) DEFAULT 0.00 COMMENT '薪资', entry_date DATE DEFAULT NULL COMMENT '入职日期', status TINYINT DEFAULT 1 COMMENT '1在职 0离职', CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(id) ON DELETE SET NULL ) ENGINE=InnoDB COMMENT='员工表'; -- 用户表,登录功能用的 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE COMMENT '登录名', password CHAR(32) NOT NULL COMMENT 'MD5密码', real_name VARCHAR(20) DEFAULT NULL COMMENT '真实姓名', role TINYINT DEFAULT 1 COMMENT '1管理员 2普通操作员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='系统用户表';

三张表的设计是有讲究的。employee.dept_id外键指向dept.id,并且ON DELETE SET NULL,意思是删除一个部门后,该部门下的员工dept_id变为 NULL,员工数据不丢失。这个设计在答辩时是个加分点,老师问「删部门时员工怎么办」,你能答出SET NULL而不是「删除部门前手动清员工」,这体现的是你对引用完整性的理解。

常用的排序和查询场景我也提前给你铺好。员工表查询最常用的两个条件是按部门筛选和按入职日期排序,以及查「某部门薪资最高的员工」。这些语句建议现在就在纳品里练熟,后面写 DAO 层时不用现想:

-- 按部门分组查平均薪资 SELECT dept_id, AVG(salary) AS avg_salary FROM employee GROUP BY dept_id; -- 查薪资最高的前 5 名员工 SELECT * FROM employee ORDER BY salary DESC LIMIT 5;

MySQL 的ORDER BY遇到 NULL 值要特别注意。默认排序规则里,ASC时 NULL 排最前,DESC时 NULL 排最后。这个规则在 MySQL 8.0 里是固定的,如果你需要把 NULL 强制排到最后,用ORDER BY ISNULL(salary), salary ASC这种写法。答辩的时候冷不丁问一句排序边界,你能答出这个细节会非常加分。

表建完之后,往里插几条测试数据。注意员工表的emp_no是唯一键,测试数据别用重复工号。我一般会插两条中文姓名、三个部门、两个用户,确保登录、查询、删除都有数据可演示。不要插太多造数据,两三行足够你在页面上看到效果了。

4. 把这套骨架跑起来:登录拦截、JDBC 连接池与分页查询的完整代码

4.1 从登录页到 LoginServlet:一次请求的完整生命周期

技术栈选定之后,代码从哪下手?我建议先做登录功能,因为它串起了浏览器、Servlet、JDBC、数据库四层,是整条主链路的微缩版。登录页用一个简洁的 JSP,注意表单提交方式必须是 POST,action 指向 Servlet 的 URL 映射:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>人事管理系统登录</title> </head> <body> <h2>人事管理系统</h2> <form action="${pageContext.request.contextPath}/login" method="post"> 用户名:<input type="text" name="username"><br> 密 码:<input type="password" name="password"><br> <span style="color:red">${msg}</span><br> <input type="submit" value="登录"> </form> </body> </html>

这里action路径用${pageContext.request.contextPath}拼接项目上下文路径,用 EL 表达式取出来,这样项目部署后改名也不会把表单请求路径写死。${msg}是后面 Servlet 里request.setAttribute("msg", "...")存进去的错误提示。POST提交保证了密码不在地址栏明文暴露——虽然这个项目密码用了 MD5 存储,但传输层没有加密,毕设层面够用就行。

对应的 LoginServlet 处理逻辑如下:

@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 用 MD5 加密后再查库,避免明文密码直接参与 SQL 拼接 String md5Pwd = MD5Util.md5(password); // 这里使用连接池,Servlet 里不手动打开/关闭 Connection try (Connection conn = DBUtil.getConnection()) { String sql = "SELECT * FROM sys_user WHERE username=? AND password=?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, md5Pwd); ResultSet rs = ps.executeQuery(); if (rs.next()) { // 登录成功,把用户信息放进 session,后续 Filter 靠它判断是否放行 request.getSession().setAttribute("loginUser", username); request.getSession().setAttribute("realName", rs.getString("real_name")); response.sendRedirect(request.getContextPath() + "/employee/list"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } catch (Exception e) { e.printStackTrace(); request.setAttribute("msg", "系统异常,请稍后重试"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

这套代码的核心是PreparedStatement的使用。用占位符?传参数,而不是把用户输入直接拼进 SQL 字符串,这是最基础的 SQL 注入防线。很多毕设项目死在答辩老师随口一句「你能防 SQL 注入吗」,你写出PreparedStatement就能答上来。另外try-with-resources语法保证了Connection、PreparedStatement、ResultSet三层资源自动关闭,不写finally手动关,代码少四五行且不会漏关连接。

连接池这一层我强烈建议你用 Druid 或者 C3P0,不要再用DriverManager每次手动创建连接。别小看这个选择,Druid 自带监控页面,答辩时打开监控页展示 SQL 执行次数和慢查询统计,这属于「超出毕设预期」的亮点,老师会眼前一亮。最小配置用druid-1.2.8.jar加一个druid.properties文件:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/hrms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username=root password=123456 initialSize=5 maxActive=20 maxWait=3000

注意连接串里的每个参数都有用。useUnicode=true&characterEncoding=utf8让 JDBC 传输层用 UTF-8 编码,配合 MySQL 服务端utf8mb4才是完整链路。serverTimezone=Asia/Shanghai对应之前 my.ini 里的default-time-zone,两边必须一致,差一点就会出现日期字段偏移 8 小时。useSSL=false和allowPublicKeyRetrieval=true是用来规避 MySQL 8.0 的 SSL 连接报错的——你搜「mysql ssl连接错误」能搜出几百个帖子,本质就是本地没配 SSL 证书,关掉就行。maxWait=3000是拿连接的超时时间,如果设成 0,连接池耗尽的瞬间线程会无限期阻塞,页面卡死。设一个明确的超时时间,失败时会给异常而不是死等。

4.2 用 Filter 把登录拦截和字符集统一一起解决

登录做完之后紧接着要解决的是「没登录能不能直接访问页面」。这个场景天然属于 Filter 的职责范围。同时把请求和响应的字符集编码统一放在 Filter 里做,比在每个 Servlet 里重复写两行要干净得多:

@WebFilter("/*") public class CharsetAndAuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 统一字符集,放在 Filter 里就不用每个 Servlet 再重复 set 了 request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); // 放行登录页、登录接口和静态资源 String uri = request.getRequestURI(); if (uri.endsWith("/login.jsp") || uri.endsWith("/login") || uri.contains("/static/") || uri.endsWith(".css") || uri.endsWith(".js")) { chain.doFilter(request, response); return; } // 检查 session 里有没有登录标记 Object loginUser = request.getSession().getAttribute("loginUser"); if (loginUser == null) { // 未登录跳回登录页 response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

Filter 的拦截逻辑里,放行条件一定要写全。常见翻车现场是把.css和.js也拦了,导致登录页样式全丢,只剩纯 HTML 元素排版。判断路径时用endsWith而不是contains,因为contains会把/login和/login.jsp都拦下来,而且如果项目里还有其他含login字样的路径也会被误伤。这个细节虽然小,但反映的是你对 URL 语义的理解。

Filter 还有个容易被忽略的坑:如果你在 Filter 里做了response.sendRedirect,那之后就不要继续调用chain.doFilter了,因为响应已经提交,继续调用会导致IllegalStateException。很多新手在这上面翻车,现象是「有时能跳转,有时报错」,排查半天发现是 if-else 结构没写对,return 被吞了。

4.3 员工列表:分页查询和搜索条件的 SQL 拼接

员工列表页是人事管理系统的主界面,也是 DAO 层最值得做厚的地方。分页查询的 SQL 用LIMIT语法,但要注意LIMIT的两个参数含义:第一个是偏移量,第二个是条数。页码从 1 开始的话,偏移量计算公式是(pageNum - 1) * pageSize。

public List<Employee> selectByPage(int pageNum, int pageSize, String keyword) { String sql = "SELECT e.*, d.name AS dept_name FROM employee e " + "LEFT JOIN dept d ON e.dept_id = d.id " + "WHERE e.name LIKE ? OR e.emp_no LIKE ? " + "ORDER BY e.entry_date DESC, e.id DESC " + "LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, (pageNum - 1) * pageSize); ps.setInt(4, pageSize); ResultSet rs = ps.executeQuery(); List<Employee> list = new ArrayList<>(); while (rs.next()) { Employee emp = new Employee(); emp.setId(rs.getInt("id")); emp.setName(rs.getString("name")); emp.setEmpNo(rs.getString("emp_no")); emp.setDeptName(rs.getString("dept_name")); emp.setSalary(rs.getBigDecimal("salary")); emp.setEntryDate(rs.getDate("entry_date")); list.add(emp); } return list; } catch (Exception e) { throw new RuntimeException("分页查询失败", e); } }

这个 SQL 里有两个细节值得在答辩时主动讲。第一个是LEFT JOIN的选择——用LEFT JOIN而不是INNER JOIN,是因为员工表里可能有dept_id为 NULL 的离职员工,内连接会把这些人过滤掉,页面数据不完整。第二个是排序字段的选择——ORDER BY e.entry_date DESC, e.id DESC用了两个字段,因为同一天入职的人可能很多,只按日期排无法保证顺序稳定,追加id DESC才保证分页时每页数据不重不漏。

LIKE 查询的模糊匹配走不了索引,这点也是答辩高频题。你的关键词搜索只在员工表和工号里做,数据量撑死几百条,全表扫描完全没压力,但如果数据量到了几万条,就要考虑用全文索引或改前缀匹配。你主动说出来这段话,老师会认为你不只写了代码,还想过性能边界。

5. 避坑指南:JSP+MySQL 项目里高频踩到的 6 个具体问题

5.1 JDBC 驱动类名与版本错位导致 ClassNotFoundException

现象:项目在别人电脑上能跑,自己电脑上启动 Tomcat 后访问页面直接报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。

原因:你用的 MySQL 驱动 JAR 包是 8.x 版本,但代码里还写着 5.x 时代的驱动类名com.mysql.jdbc.Driver。MySQL 官方在 Connector/J 8.0 里把驱动类改名成了com.mysql.cj.jdbc.Driver,老名字在 8.x 里默认不加载。还有一种情况是驱动 JAR 没放进WEB-INF/lib目录,而是放在了 Tomcat 的 lib 下。Tomcat 的类加载机制是「应用优先加载自己 lib 下的类」,不会去找全局 lib,所以放在全局目录里应用一样报类找不到。

解决:优先检查两处——第一,WEB-INF/lib下有没有 mysql-connector-java 的 jar 包;第二,驱动类名和驱动 JAR 版本是否匹配。8.x 连接串写法里的com.mysql.cj.jdbc.Driver且连接串必须有serverTimezone参数。5.x 时代可以不写时区,8.x 不写给报错The server time zone value is unrecognized。

5.2 中文乱码:三层要一起设,少一层都是问号

现象:JSP 页面显示正常,插入数据库后中文变???。

原因:乱码从来不是单一环节的问题,而是请求参数编码、数据库表字符集、JDBC 连接串编码三层中某一层没对齐。浏览器提交表单时用页面编码,Tomcat 接收请求参数时默认用 ISO-8859-1,MySQL 存储时看表和库的字符集,JDBC 传输时看连接串的characterEncoding。任何一层不一致,中文就出问题。

解决:三层全部统一到 UTF-8。第一层,JSP 页面头部写contentType="text/html;charset=UTF-8",POST请求在 Filter 或 Servlet 里执行request.setCharacterEncoding("UTF-8")。第二层,建库语句指定DEFAULT CHARACTER SET utf8mb4,检查现有表用SHOW CREATE TABLE employee看默认字符集。第三层,JDBC 连接串里加useUnicode=true&characterEncoding=utf8。排查顺序从数据库开始:先在命令行用INSERT中文,查出来如果是问号,说明是 MySQL 侧问题;命令行正常但 JSP 插入是问号,就是 JDBC 连接串问题。

有的朋友会在这时候去改my.ini加上init_connect='SET NAMES utf8mb4',对于本地单应用来说没有必要,固定连接串参数就够,改 init_connect 影响的是所有连接,反而容易造成时区和字符集的隐性不一致。

5.3 Tomcat 10 的 jakarta 包名与老代码的水土不服

现象:把一套之前在 Tomcat 9 下能跑的项目放进 Tomcat 10,启动后报错java.lang.ClassNotFoundException: javax.servlet.http.HttpServlet之类。

原因:Tomcat 10 开始把 Servlet API 从javax.servlet迁移到了jakarta.servlet,旧代码里import javax.servlet.*的类全部找不到。这是 Oracle 把 Java EE 捐给 Eclipse 基金会后的品牌切割,代码层面是包名整体更名,底层类和 API 接口几乎没有区别。

解决:毕设阶段最省事的办法是不要碰 Tomcat 10。下载 Tomcat 9 版本,保持在javax.servlet包名体系下,和你的教材、网上大部分 JSP 教程保持一致。如果你已经装了 Tomcat 10,不想卸重装,那就要把项目里所有import javax.servlet改成import jakarta.servlet,同时 Tomcat 的web.xml,还有你依赖的第三方库(比如 JSTL)也要换成jakarta命名空间版本。对毕设来说这个纯属浪费时间,直接换 Tomcat 9 是正经答案。下载时留意官网目录里的Tomcat 9和Tomcat 10标签,别一眼晃过去下错。

5.4 MySQL 排序 NULL 值:你以为按薪资降序,结果 NULL 排最前

现象:员工列表按薪资排序,离职员工或薪资字段没填的记录 NULL 全堆在最上面,把正常数据挤下去了。

原因:MySQL 的ORDER BY默认把 NULL 当作最小值。ASC时 NULL 排最前,DESC时 NULL 排最后。这个规则其实和大多数人的直觉相反——直觉上 NULL 是「空」,应该排最后,但 MySQL 的定义是 NULL 最小,所以降序时 NULL 反而在末尾。区分一下:DESC时 NULL 排最后,正合你意;ASC时 NULL 排最前,往往不合你意。

解决:如果你要强制 NULL 到最底部,不改变其他字段排序规则,可以这样写:ORDER BY (salary IS NULL), salary DESC。salary IS NULL这个表达式的结果是 0(非空)或 1(NULL),0会排在1前面,这就是「把 NULL 强制沉底」的底层逻辑。同理,要把 NULL 强制放最上面用ORDER BY (salary IS NOT NULL), salary DESC。这种写法在 Oracle、PostgreSQL 里同样适用,SQL 标准里你还可以用NULLS FIRST/LAST,但 MySQL 直到 8.0.30 还不支持这个子句,所以只能用上面的表达式写法。

5.5 JSP 图片坐标定位:相对路径在转发表单里失效

现象:JSP 页面里<img src="images/logo.png">在登录页显示正常,跳转到子目录页面或者经过 Servlet 转发后的页面上图片裂了。

原因:src="images/logo.png"是相对路径,浏览器解析时是以当前访问的 URL 路径为基准去拼的。如果当前 URL 是/employee/list,浏览器会把它解析成/employee/images/logo.png,而你的图片实际在项目根目录的/images/logo.png。登录页通常直接注册为根路径,所以正好对上;经过 Servlet 转发或 Filter 拦截的 URL 路径一变,相对路径就错位。

解决:JSP 页面里不要写相对路径,全部使用${pageContext.request.contextPath}拼绝对路径:<img src="${pageContext.request.contextPath}/images/logo.png">。涉及表单提交后的重定向跳转也一样。另外要注意,如果你的页面中有<base>标签,它能改变所有相对路径的基准,但用得不熟容易把已经正确的绝对路径也带歪,风险更大,我一般建议直接用 EL 前缀,不引入 base 标签。图片坐标定位这个需求在人事系统里常见于「部门架构图」或「员工照片墙」页面,HTML 里对图片定位可以用 CSSposition: relative加top/left偏移,但前提是图片路径先解决,否则定位好了也显示不出来。

5.6 事务忘了提交:删除部门后员工表出现悬挂引用

现象:删掉一个部门,部门列表没了,但员工列表里员工还挂在已删除部门的名下,部门名称显示为空。

原因:删除部门的外键策略是ON DELETE SET NULL,这个约束在 MySQL InnoDB 里只要执行 DELETE 就会自动生效。如果伸出问题,多半是你自定义的删除逻辑里先删部门再更新员工,或者用多条 SQL 在一个事务里处理了删除和回填,但没有COMMIT,导致约束层面的自动动作没有持久化。另一种更隐蔽的情况是 JDBC 连接默认是自动提交,但你在代码里开启了事务conn.setAutoCommit(false)后只做了 DELETE,忘了commit(),直接关了连接,事务回滚,部门删除了但外键动作没生效。

解决:多表操作的业务一律显式控制事务,三行代码不能少:

conn.setAutoCommit(false); try { // 执行删除部门等操作 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); }

判断是不是这个原因有个笨办法:在 MySQL 命令行手动执行DELETE FROM dept WHERE id=1,然后查询employee.dept_id,如果外键生效,关联员工会自动变 NULL。命令行下正常而程序里不正常,那就去代码里检查setAutoCommit(false)和commit()之间的逻辑,是否有异常分支把 commit 跳过了。MySQL 事务这一块答好了,在答辩时跟老师聊锁分类——共享锁、排他锁、意向锁的区别——都是加分项,至少说明你读过 InnoDB 的锁粒度。

6. 从「能跑」到「能答辩」:压测、索引与事务的验证方法

项目联调完之后,不要急着打包交差。用这三个方法把项目从「能跑」推到「能讲」的层次,同时也帮你自己在答辩前把系统真实情况摸清楚。

第一个是压测登录接口。用 JMeter 建一个线程组,10 个线程循环 20 次打/login这个 POST 接口,观察平均响应时间和错误率。本地单机 MySQL 不做任何优化时,这个接口的响应时间正常应该在 50ms 到 150ms 之间。如果你的结果超过 500ms 甚至上千毫秒,说明 SQL 有问题:检查是否全表扫描、连接池是否没生效、是否存在 N+1 查询。你带着一份压测报告去答辩,比任何口头描述都有说服力。JMeter 的聚合报告可以截图插到毕设论文的测试章节,这是很多论文里「性能测试」一节的真实来源。

第二个是检查慢查询日志。执行下面两条命令,把 MySQL 的慢查询阈值设为 1 秒,跑一遍核心操作,然后看日志里有没有超过阈值的 SQL:

SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1; SHOW VARIABLES LIKE 'slow_query_log_file';

如果你发现某条 SQL 进了慢查询日志,最常规的优化是在employee.name和employee.emp_no上建普通索引。注意LIKE '%关键字%'这种模糊匹配不加索引,但WHERE emp_no = 'EMP001'这种等值查询可以走索引。面试时聊 MySQL 排序和索引,能说出「等值走索引、模糊前导通吃」的人,比死背八股文的印象分高不少。如果你按标题搜过「mysql创建索引」和「mysql排序」相关的内容,正好可以把这些积累整理成答辩素材。

第三个是事务演示。故意做一个操作性演示:在员工管理页面同时提交两个操作——新插入一条员工记录,同时更新一个部门的负责人。把 SESSION A 里的插入操作开启事务但不要提交,然后在命令行里到 SESSION B 查这条数据是否存在,让老师亲眼看到「读不到未提交数据」;提交后再查,数据可见。这个演示直接对应 ACID 的隔离性,造价低、效果直观。你给老师讲什么是「不可重复读」和「幻读」,用这个现场实验比背定义强得多。

回到最初的问题:JSP+MySQL 做人事管理系统到底值不值得。我的结论是值得,且性价比很高。这套题能覆盖 JavaWeb 的全部主干知识,排坑过程本身就是在训练你排查问题的方法论——先把环境立对,再把链路走通,最后才是功能堆积。我当年做这套题时在 MySQL 8.0 的时区参数上栽过跟头,页面里全乱码的日子现在想起来也谈不上愉快,但正是这些坑让我后来用 Spring Boot 时格外注意连接串里的每一个参数。把这些经验留在你的毕设记录里,答辩时你会讲得有底气,做的过程也能真的学到东西。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询