
简介这是一份面向Java Web初学者与课程设计学习者的完整项目资料围绕JSP技术构建人事管理系统帮助读者理解从需求分析到编码测试的软件工程全流程。压缩包共1.11MB内含项目报告、任务书、中英文翻译文档及完整源代码报告涵盖背景介绍、需求分析、系统架构与数据库设计、功能模块实现及测试总结任务书则给出目标、任务分配与时间安排源代码包含Servlet、JSP页面及MVC各组件便于对照学习HTTP请求处理与数据库交互。项目涉及JSP基本语法与内置对象、Servlet协同、JDBC连接操作、MVC设计模式以及HTML、CSS、JavaScript前端展示并覆盖数据库ER模型设计与Web应用部署运行等知识点。目前已有54人学习下载适合希望提升Java Web开发理解、完成类似课程设计或企业级应用实践的学生与开发者参考。1. 基于 JSP 的人事管理系统从课程设计到能跑通的企业级 CRUD 实战很多 Java Web 初学者做人事管理系统卡住的地方往往不是业务逻辑而是 JSP 页面怎么把后端查出来的员工列表渲染成表格、表单提交后中文乱码怎么处理、分页查询的 SQL 到底该写在哪一层。这套基于 JSP 的人事管理系统本质上是一个典型的 Servlet JSP JDBC 三层架构 CRUD 项目覆盖员工信息管理、部门管理、考勤记录、薪资查询这几个核心模块适合正在做课程设计或毕业设计的在校生也适合想从纯前端转 Java Web 的开发者拿来练手。它不涉及微服务、不依赖 Spring 全家桶一个 Tomcat 加一个 MySQL 就能跑起来但麻雀虽小该有的分层、该踩的坑一个不少。下面我按实际开发顺序把从环境搭建到部署上线的完整路径拆开讲。2. 环境搭建与项目骨架Tomcat、MySQL 和 JDBC 的版本对齐2.1 为什么这套技术栈至今还在被课程设计选用JSP 本身是 Servlet 的模板化封装浏览器请求先到 TomcatTomcat 根据 web.xml 或注解把请求路由到对应的 ServletServlet 调用 Service 层Service 通过 JDBC 操作 MySQL再把结果集封装成 JavaBean 存进 request 域最后转发到 JSP 页面渲染。这条链路清晰、依赖少、调试直观出问题了看 Tomcat 日志就能定位到具体哪一层抛了异常。相比 Spring Boot 自动装配带来的“黑匣子”感JSP 项目里每个环节都是显式的对理解 Java Web 的请求响应模型反而更友好。选型上JDK 建议用 8 或 11Tomcat 用 8.5 或 9.0MySQL 用 5.7 或 8.0。注意 MySQL 8.0 的 JDBC 驱动类名是com.mysql.cj.jdbc.Driver连接 URL 要加时区参数serverTimezoneAsia/Shanghai否则启动就报时区错误。这是新手翻车率最高的一步没有之一。2.2 数据库建表与连接池配置先建库建表。人事管理系统的核心表至少四张员工表、部门表、考勤表、薪资表。员工表用自增主键部门表用外键关联考勤和薪资表通过员工 ID 关联。CREATE DATABASE hrms DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE hrms; CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, dept_location VARCHAR(100) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(50) NOT NULL, gender CHAR(2), birth_date DATE, phone VARCHAR(20), email VARCHAR(100), dept_id INT, hire_date DATE, position VARCHAR(50), salary DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1在职 0离职, FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字符集统一用utf8mb4不要用utf8否则生僻字和 emoji 存进去会变问号。外键约束在开发阶段建议加上能帮你提前发现数据不一致的问题如果后期要做批量导入再考虑去掉外键改用应用层校验。连接池用 Druid 或 C3P0 都行课程设计里用 Druid 更常见。在src下建druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/hrms?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000initialSize设 5 够用maxActive设 20 是防止并发高了连接耗尽maxWait3000 毫秒表示拿不到连接最多等 3 秒就抛异常避免请求线程无限阻塞。加载配置的代码放在一个静态工具类里public class JDBCUtils { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(JDBCUtils.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 依次关闭注意顺序和空判断 try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这里用静态代码块保证连接池只初始化一次close方法里先关 ResultSet 再关 Statement 最后关 Connection顺序反了在某些驱动下会报错。连接池的 close 实际上是归还连接不是真正断开所以不用担心频繁开关的性能问题。2.3 项目目录结构与 web.xml 路由映射标准 Maven Web 项目结构如下src/main/java/com/hrms/ dao/ -- 数据访问层 service/ -- 业务逻辑层 servlet/ -- 控制层 entity/ -- 实体类 util/ -- 工具类 src/main/resources/ druid.properties src/main/webapp/ WEB-INF/web.xml css/ js/ images/ login.jsp employee_list.jsp employee_edit.jspServlet 用WebServlet(/employee/*)注解映射比 web.xml 配置更简洁。但注意注解需要web.xml的metadata-completefalse或干脆不写 web.xml 的 servlet 节点。如果两个都配了容器会以 web.xml 为准注解被覆盖这是很多人改了注解不生效的原因。提示Tomcat 10 开始包名从javax.servlet变成了jakarta.servlet如果你用的 Tomcat 10 但代码里 import 的是javax.servlet编译能过但运行时报 ClassNotFound。要么降 Tomcat 到 9要么全局替换包名。3. 员工信息模块的增删改查Servlet 分发与 JSP 渲染的配合3.1 用 BaseServlet 做请求分发告别 if-else 堆叠一个模块通常有列表、新增、编辑、删除、批量删除五个操作。如果每个操作写一个 Servlet类会爆炸如果全写在一个 Servlet 的 doGet 里用if (method.equals(list))判断代码会越来越长。常见做法是抽一个 BaseServlet用反射根据请求参数中的method值调用对应的方法。public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String methodName req.getParameter(method); if (methodName null || methodName.isEmpty()) { methodName list; // 默认走列表 } try { Method method this.getClass().getDeclaredMethod( methodName, HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, req, resp); } catch (NoSuchMethodException e) { resp.sendError(404, 方法不存在: methodName); } catch (Exception e) { throw new ServletException(请求处理失败, e); } } }子类EmployeeServlet extends BaseServlet里面写list、add、update、delete方法方法签名统一为(HttpServletRequest, HttpServletResponse)。前端请求写成employee?methodlist、employee?methoddeleteid5。反射调用时setAccessible(true)是为了访问非 public 方法如果方法都声明为 public 可以去掉。异常处理里把原始异常包进 ServletException 抛出Tomcat 日志里能看到完整堆栈方便定位。3.2 分页查询的 SQL 写法与 PageBean 封装员工列表数据量大了必须分页。MySQL 用LIMIT offset, sizeoffset 从 0 开始。先查总数再查当前页数据。public PageBeanEmployee findByPage(int currentPage, int pageSize, String keyword) { PageBeanEmployee pb new PageBean(); pb.setCurrentPage(currentPage); pb.setPageSize(pageSize); StringBuilder countSql new StringBuilder( SELECT COUNT(*) FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE 11); StringBuilder dataSql new StringBuilder( SELECT e.*, d.dept_name FROM employee e LEFT JOIN department d ON e.dept_id d.dept_id WHERE 11); ListObject params new ArrayList(); if (keyword ! null !keyword.trim().isEmpty()) { String like % keyword.trim() %; countSql.append( AND (e.emp_name LIKE ? OR e.phone LIKE ?)); dataSql.append( AND (e.emp_name LIKE ? OR e.phone LIKE ?)); params.add(like); params.add(like); } // 查总数 int totalCount queryForInt(countSql.toString(), params.toArray()); pb.setTotalCount(totalCount); pb.setTotalPage((int) Math.ceil(totalCount * 1.0 / pageSize)); // 查当前页数据 dataSql.append( ORDER BY e.emp_id DESC LIMIT ?, ?); params.add((currentPage - 1) * pageSize); params.add(pageSize); ListEmployee list queryForList(dataSql.toString(), params.toArray()); pb.setList(list); return pb; }PageBean里放currentPage、pageSize、totalCount、totalPage、list五个字段。totalPage用Math.ceil向上取整注意totalCount * 1.0是为了避免整数除法丢精度。ORDER BY emp_id DESC让最新入职的排前面符合使用习惯。LEFT JOIN保证即使部门被删了员工记录还能查出来dept_name显示为空而不是整条记录消失。3.3 JSP 页面用 JSTL 遍历列表并处理中文编码JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %和 JSTL 标签库引入。列表用c:forEach遍历日期用fmt:formatDate格式化。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table classtable table-bordered thead tr th工号/thth姓名/thth性别/thth部门/th th入职日期/thth薪资/thth状态/thth操作/th /tr /thead tbody c:forEach items${pageBean.list} varemp varStatusvs tr td${emp.empId}/td td${emp.empName}/td td${emp.gender M ? 男 : 女}/td td${emp.deptName}/td tdfmt:formatDate value${emp.hireDate} patternyyyy-MM-dd//td tdfmt:formatNumber value${emp.salary} pattern#,##0.00//td td${emp.status 1 ? 在职 : 离职}/td td a hrefemployee?methodtoEditid${emp.empId}编辑/a a hrefjavascript:del(${emp.empId})删除/a /td /tr /c:forEach /tbody /table${emp.gender M ? 男 : 女}用 EL 表达式做三元判断比在 Java 里拼字符串干净。fmt:formatDate需要java.util.Date类型如果实体类里用的是java.time.LocalDateJSTL 默认不支持得自己写转换或改用String接收。删除用 JavaScript 弹确认框再跳转避免误操作function del(id) { if (confirm(确认删除该员工删除后不可恢复)) { location.href employee?methoddeleteid id; } }注意JSP 里${pageBean.list}取不到值先检查 PageBean 有没有放进 request 域再检查 getter 方法名是否规范。EL 表达式对属性名大小写敏感getEmpName()对应${emp.empName}写成${emp.empname}就是空。4. 避坑与排查JSP 人事系统开发中最容易翻车的五个点4.1 中文乱码POST 和 GET 要分开处理现象表单提交后数据库里存的是????或者页面显示乱码。原因POST 请求默认用 ISO-8859-1 解码GET 请求在 Tomcat 8 以后默认 URIEncoding 是 UTF-8 但部分版本仍需确认。解决POST 在 BaseServlet 的 service 方法第一行加req.setCharacterEncoding(UTF-8)GET 在 Tomcat 的server.xml里确认 Connector 有URIEncodingUTF-8。数据库连接 URL 里加useUnicodetruecharacterEncodingutf8。三处都对齐了才不会乱码。4.2 连接池耗尽Connection 忘了关现象系统跑一段时间后报GetConnectionTimeoutException重启 Tomcat 又好了。原因某个 DAO 方法里Connection没在 finally 里关闭连接池的 20 个连接被慢慢耗光。解决所有 JDBC 操作统一用try-finally或 try-with-resources 关闭连接。如果用了事务注意conn.setAutoCommit(false)之后必须commit或rollback否则连接归还时事务没结束下一个拿到这个连接的请求会读到脏数据。4.3 JSP 页面修改后不生效现象改了 JSP 文件浏览器刷新还是旧内容。原因Tomcat 的 work 目录缓存了编译后的 Servlet或者浏览器缓存了静态资源。解决先 CtrlF5 强制刷新不行就删掉 Tomcat 的work/Catalina目录重启。开发阶段可以在server.xml的 Context 里加reloadabletrueTomcat 会监测 class 和 JSP 变化自动重载但生产环境不要开性能损耗明显。4.4 外键约束导致删除失败现象删除部门时抛Cannot delete or update a parent row: a foreign key constraint fails。原因部门下还有员工记录外键约束阻止删除。解决两种方案一是先查该部门下有没有员工有就提示“请先转移员工”二是把外键的删除策略改成ON DELETE SET NULL部门删了员工记录的dept_id置空。课程设计里推荐第一种业务逻辑更清晰。4.5 日期类型转换异常现象表单提交2024-01-15到java.util.Date字段时报IllegalArgumentException。原因request.getParameter拿到的是 String直接赋值给 Date 字段会失败。解决用SimpleDateFormat手动解析或者用 Apache Commons BeanUtils 的ConvertUtils.register注册日期转换器。手动解析的代码String hireDateStr req.getParameter(hireDate); if (hireDateStr ! null !hireDateStr.isEmpty()) { try { emp.setHireDate(new SimpleDateFormat(yyyy-MM-dd).parse(hireDateStr)); } catch (ParseException e) { throw new RuntimeException(日期格式错误: hireDateStr, e); } }SimpleDateFormat不是线程安全的不要定义成静态成员变量每次方法内 new 一个或者用ThreadLocal包一层。5. 从能跑到好用三个让系统更接近生产环境的技巧5.1 用过滤器统一处理编码和登录校验每个 Servlet 都写一遍setCharacterEncoding太啰嗦用 Filter 统一处理。在web.xml或注解里配/*拦截所有请求WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String uri request.getRequestURI(); // 放行登录页和登录请求 if (uri.endsWith(login.jsp) || uri.contains(methodlogin)) { chain.doFilter(req, resp); return; } // 未登录跳回登录页 if (request.getSession().getAttribute(currentUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }登录校验放在 Filter 里比在每个 Servlet 里判断更可靠新增页面时不会漏。request.getContextPath()拿到项目部署路径避免硬编码/hrms导致换项目名就 404。5.2 用 AJAX 做无刷新删除和分页传统表单提交每次操作都整页刷新体验差。用 jQuery 的$.post发异步请求后端返回 JSON前端局部更新表格function del(id) { if (!confirm(确认删除)) return; $.post(employee, {method: delete, id: id}, function(res) { if (res.code 200) { $(#row_ id).remove(); $(#totalCount).text(res.data); } else { alert(res.msg); } }, json); }后端delete方法里用response.getWriter().write({\code\:200,\msg\:\删除成功\})返回。注意手动拼 JSON 容易出转义问题建议引入 Gson 或 Jacksonnew Gson().toJson(resultMap)一行搞定。AJAX 删除后只移除对应行不刷新整页分页组件也不用重新渲染。5.3 用事务保证薪资调整的原子性给员工调薪时既要更新employee表的salary字段又要往salary_log表插一条变更记录。两步操作必须在一个事务里否则第一步成功第二步失败数据就对不上了。public void adjustSalary(int empId, BigDecimal newSalary, String reason) { Connection conn null; try { conn JDBCUtils.getConnection(); conn.setAutoCommit(false); // 更新员工薪资 String updateSql UPDATE employee SET salary ? WHERE emp_id ?; executeUpdate(conn, updateSql, newSalary, empId); // 插入变更日志 String logSql INSERT INTO salary_log(emp_id, old_salary, new_salary, reason, change_time) SELECT emp_id, salary, ?, ?, NOW() FROM employee WHERE emp_id ?; executeUpdate(conn, logSql, newSalary, reason, empId); conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException(调薪失败已回滚, e); } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) {} } } }注意logSql里用SELECT ... FROM employee拿旧薪资这样不用先查一次再插。conn.setAutoCommit(true)在 finally 里恢复因为连接要归还给连接池不恢复的话下一个拿到这个连接的请求会一直处于手动提交模式操作不生效。这个坑我踩过排查了半天才发现是连接池里的连接状态被污染了。提示事务方法里不要吞异常catch之后要么 rollback 要么往上抛最怕的是 catch 了什么都不做commit 照样执行数据错了都不知道。这套 JSP 人事管理系统看起来简单但把分层、连接池、事务、过滤器、AJAX 这些点都串一遍对 Java Web 的理解会比单纯看视频深得多。我自己的习惯是每加一个功能就先想清楚数据从哪来、经过哪几层、最终到哪去画个简单的流向图再动手比上来就写代码少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取