简介:这是一套面向Java Web初学者与进阶开发者的员工管理系统实战源码资源,聚焦Servlet、JSP、JDBC及MVC分层架构的综合应用,帮助开发者掌握企业级Web应用从数据库设计到前后端交互的完整开发流程。压缩包共246个文件,包含50个核心Java源文件(如Emploe、DeptDaoImpl等业务类)、28个JSP页面(实现员工信息增删改查、部门管理等前端视图)、102个编译后Class文件,以及SQL建库脚本、web.xml配置、项目元数据(.project/.classpath)和静态资源(PNG/JPG/GIF),整体仅2.45MB,轻量易部署。已有1086人学习下载,资源结构清晰:src目录按包组织业务逻辑,WebContent承载视图与配置,database提供可直接导入的MySQL脚本,配合完整DAO层实现与Manager/EmploeUpdate等典型控制器类,便于理解分层协作与CRUD实践。
1. 为什么一个“Java Web员工管理系统”压缩包,至今仍是校招面试官手里的压轴题?
你打开这个名为java web员工管理系统(源码+数据库).zip的压缩包时,看到的不只是几个.java文件和一个.sql脚本——它是一套被反复验证过的、能跑通「增删改查→权限控制→页面跳转→数据持久化」全链路的最小可行闭环。不是 Spring Boot 自动装配的黑匣子,不是 Vue + Axios 的前端胶水逻辑,而是用 Servlet + JSP + JDBC + MySQL 原生堆出来的、每行代码都暴露在请求生命周期里的老派扎实感。它解决的不是“高并发”或“分布式事务”,而是:当一个刚学完 JDBC 连接池、还没搞懂 Filter 和 Listener 区别的学生,如何在 3 天内把课本上的 DAO 模式真正焊进浏览器地址栏里?适合 Java 初学者做课程设计、转岗者补 Web 开发手感、面试前复盘 MVC 分层逻辑——尤其当你被问到“请手写一个用户登录校验流程”时,这个项目里LoginServlet.doPost()里那 12 行带request.getSession().setAttribute()的代码,就是你的后悔药。
2. 从解压到启动:用最原始的方式跑通这个系统(不依赖任何 IDE 插件)
这个压缩包的结构非常典型:src/下是 Java 源码,WebContent/(或webapp/)下是 JSP 和静态资源,db/或根目录下有.sql文件。它默认面向 Tomcat 7–9,不带 Maven 依赖声明,意味着你要亲手把 jar 包塞进WEB-INF/lib/。这不是缺陷,是刻意为之——它逼你直面 ClassLoader 加载顺序、web.xml的<servlet-mapping>如何劫持 URL、以及为什么jdbc:mysql://localhost:3306/emp?useSSL=false&serverTimezone=UTC里那个serverTimezone=UTC少了就会报java.sql.SQLException: The server time zone value 'XXX' is unrecognized。
2.1 解压与目录结构确认:先看清“战场”
解压后,务必确认以下三个核心目录是否存在且内容合理:
src/:应包含com.emp.dao/(DAO 层)、com.emp.servlet/(Servlet 层)、com.emp.entity/(实体类)等标准包结构;WebContent/或webapp/:必须有index.jsp(首页)、login.jsp(登录页)、list.jsp(员工列表页),以及WEB-INF/web.xml;db/或根目录:应有一个.sql文件,如emp_system.sql,内容以CREATE DATABASE IF NOT EXISTS emp_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;开头。
提示:如果
src/下全是.class文件而非.java,说明你拿到的是编译后版本——立刻放弃,这不是你要的“源码”。真正的源码包,src/里每个.java文件都该有package com.emp.xxx;声明和清晰的import列表。
2.2 数据库初始化:别让ClassNotFoundException出现在建表之前
先启动 MySQL(5.7 或 8.0),然后执行 SQL 脚本。关键动作不是直接source emp_system.sql,而是先创建数据库并指定编码:
mysql -u root -pCREATE DATABASE emp_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE emp_system; SOURCE /path/to/emp_system.sql;为什么强调
utf8mb4?因为员工姓名可能含 emoji 或生僻字(如“䶮”、“堃”),utf8在 MySQL 8.0+ 中已被标记为 deprecated,且实际只支持 3 字节 UTF-8;utf8mb4才是完整四字节支持。若建库时漏掉,后续插入中文会报Incorrect string value,而错误堆栈里根本不会提示编码问题,只会卡在PreparedStatement.executeUpdate()。
执行后,用SELECT * FROM employee;确认有测试数据(通常含id=1, name=张三, dept=技术部, salary=8000等)。这步验证通过,才代表数据库层已就绪。
2.3 Tomcat 部署:手动拷贝 WAR 包比 IDE 一键部署更能暴露问题
不要用 Eclipse/IntelliJ 的 “Run on Server” 功能——它会自动帮你生成context.xml、注入 JVM 参数、甚至悄悄替换web.xml版本号。我们要的是“裸机部署”:
- 将整个项目文件夹(如
emp-system/)重命名为emp-system(不能含中文、空格、特殊符号); - 拷贝到
TOMCAT_HOME/webapps/目录下; - 启动 Tomcat:
TOMCAT_HOME/bin/startup.sh(Linux/Mac)或startup.bat(Windows); - 观察
TOMCAT_HOME/logs/catalina.out,等待出现INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deploying web application directory。
逻辑说明:Tomcat 启动时会扫描
webapps/,将文件夹名作为 Context Path。emp-system/部署后,访问地址就是http://localhost:8080/emp-system/。如果首页打不开,第一反应不是查 JSP 语法,而是看 catalina.out 里有没有SEVERE: Error filterStart或Caused by: java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver——这说明 MySQL 驱动没放对位置。
2.4 驱动 Jar 包落位:WEB-INF/lib/是唯一合法的“藏身之处”
下载mysql-connector-java-8.0.33.jar(适配 MySQL 8.0+)或mysql-connector-java-5.1.47.jar(适配 MySQL 5.7),必须放入emp-system/WEB-INF/lib/目录下,而不是TOMCAT_HOME/lib/。原因:Servlet 规范要求 Web 应用的类加载器(WebAppClassLoader)优先从WEB-INF/lib/加载,这是应用级隔离的基础。若放错位置,会出现两种诡异现象:
Class.forName("com.mysql.cj.jdbc.Driver")成功,但DriverManager.getConnection(...)报No suitable driver found(驱动类被加载,但 DriverManager 未注册);- 或者更隐蔽的:
Connection对象能获取,但执行executeQuery("SELECT * FROM employee")时抛SQLException: Unknown system variable 'query_cache_size'(驱动版本与 MySQL 版本不匹配)。
验证方式:启动后访问http://localhost:8080/emp-system/login.jsp,输入默认账号(常见为admin/123456),若跳转到list.jsp且显示员工表格,则 JDBC 层打通。
3. 代码层拆解:看懂这个系统如何用原生 Servlet 实现 MVC 分层
这个项目的价值不在功能多炫酷,而在它用最朴素的方式实现了教科书级的分层:entity存数据结构,dao封装 SQL,servlet当控制器,jsp做视图。没有 Spring 的@Autowired注入,所有对象靠new创建;没有 MyBatis 的 XML 映射,SQL 字符串硬编码在EmployeeDao.java里。这种“笨功夫”,恰恰是理解 Web 容器工作原理的必经之路。
3.1 Entity 层:为什么private Integer id;比private int id;更安全?
查看src/com/emp/entity/Employee.java,你会发现主键字段声明为:
private Integer id; private String name; private String department; private Double salary;而非int id、double salary。原因在于:数据库中id允许为 NULL(新增员工时主键由数据库自增,Java 对象初始值应为 null,而非 0)。若用基本类型,new Employee()后id=0,插入时会违反主键约束或覆盖已有 ID。同理,salary可能为空(如实习生未定薪),Double可为null,而double强制为0.0,导致业务语义丢失。
参数说明:
Integer是int的包装类,支持null值;JDBC 的rs.getInt("id")若数据库值为 NULL,会返回0,但rs.getObject("id", Integer.class)则返回null——所以 DAO 层读取时必须用后者,否则空值会被误转为0。
3.2 DAO 层:PreparedStatement的占位符不是摆设,是防 SQL 注入的最后防线
打开src/com/emp/dao/EmployeeDao.java,查找insert(Employee emp)方法:
public boolean insert(Employee emp) { String sql = "INSERT INTO employee(name, department, salary) VALUES(?, ?, ?)"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, emp.getName()); ps.setString(2, emp.getDepartment()); ps.setDouble(3, emp.getSalary()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }注意三点:
- SQL 字符串里用
?占位,而非"'" + emp.getName() + "'"拼接——这是防 SQL 注入的基石; ps.setString(1, ...)的参数索引从 1 开始(不是 0),且类型严格匹配(name是VARCHAR,必须用setString);try-with-resources自动关闭Connection和PreparedStatement,避免连接泄漏。
逻辑说明:若此处写成
sql = "INSERT INTO employee(name,...) VALUES('" + emp.getName() + "',...)",当emp.getName()为张三'; DROP TABLE employee; --时,整条 SQL 变成INSERT ... VALUES('张三'; DROP TABLE employee; --',...),数据库会执行恶意语句。而PreparedStatement会将?参数作为纯数据传给 MySQL Server,Server 不会将其解析为 SQL 语法。
3.3 Servlet 层:request.getParameter()的返回值永远要判空,这是血泪经验
LoginServlet.java的doPost方法里,必然有:
String username = request.getParameter("username"); String password = request.getParameter("password"); if (username == null || username.trim().isEmpty() || password == null || password.trim().isEmpty()) { request.setAttribute("msg", "用户名或密码不能为空"); request.getRequestDispatcher("login.jsp").forward(request, response); return; }为什么username.trim().isEmpty()不可省略?因为 HTML 表单提交时,若用户只敲空格再提交,getParameter返回" "(含空格字符串),== null为 false,但业务上它仍是无效输入。若不trim(),会导致username.equals("admin")为 false,登录失败却无提示。
参数说明:
request.getParameter()在参数不存在时返回null,存在但为空字符串时返回"",存在且含空格时返回" "。三者必须分别处理,否则NullPointerException或逻辑漏洞必然出现。
3.4 JSP 层:EL 表达式${employee.name}背后是pageContext.findAttribute()的查找链
list.jsp中遍历员工列表:
<c:forEach items="${requestScope.employees}" var="emp"> <tr> <td>${emp.id}</td> <td>${emp.name}</td> <td>${emp.department}</td> <td>${emp.salary}</td> </tr> </c:forEach>这里${emp.name}能工作,是因为:
- Servlet 中
request.setAttribute("employees", empList)将 List 放入 request 域; - JSP 容器在解析 EL 时,对
emp变量调用pageContext.findAttribute("emp"),该方法按page → request → session → application顺序查找; emp是<c:forEach>的var,属于 page 域,所以能直接访问其 getter 方法(getname()→getName())。
注意:若
Employee类中getName()方法抛出异常(如 NPE),JSP 页面会显示???而非堆栈,调试困难。因此 DAO 层返回的Employee对象,所有字段必须确保非 null(如department为null时,emp.getDepartment()返回null,${emp.department}输出空字符串,安全)。
4. 避坑指南:那些让新手调试到凌晨三点的“玄学”错误
这个项目看似简单,但因技术栈古老、环境敏感、错误信息晦涩,极易陷入“改了十处,只有一处生效”的泥潭。以下是我在带实习生时记录的真实踩坑清单,每一条都附带现象 → 原因 → 解决的闭环。
4.1 现象:登录成功后跳转list.jsp,页面显示空白,查看源码只有<html><body></body></html>
原因:list.jsp中使用了 JSTL 标签(如<c:forEach>),但WEB-INF/lib/下缺少jstl.jar和standard.jar(JSTL 1.1)或jakarta.servlet.jsp.jstl-api-2.0.0.jar+tomcat-jasper-el-9.0.83.jar(JSTL 2.0)。JSP 编译时无法解析<c:前缀,直接忽略标签,导致循环体不渲染。
解决:下载对应 JSTL 版本 jar 包,放入WEB-INF/lib/;同时检查list.jsp顶部是否有<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>(JSTL 1.1)或<%@ taglib prefix="c" uri="http://xmlns.jcp.org/jsp/jstl/core" %>(JSTL 2.0),URI 必须与 jar 包匹配。
4.2 现象:添加新员工时,中文姓名存入数据库后变成??,但数据库和表字符集已设为utf8mb4
原因:JDBC URL 缺少characterEncoding=utf8mb4参数,或 Tomcat 的server.xml中Connector未配置URIEncoding="UTF-8"。前者导致 MySQL Server 接收字节流时按 latin1 解码,后者导致 GET 请求参数(如?name=张三)在 Tomcat 解码时乱码。
解决:
- JDBC URL 改为:
jdbc:mysql://localhost:3306/emp_system?useSSL=false&serverTimezone=UTC&characterEncoding=utf8mb4; TOMCAT_HOME/conf/server.xml中<Connector port="8080" ... />添加属性:URIEncoding="UTF-8";- 重启 Tomcat。
4.3 现象:修改员工信息后,页面跳回list.jsp,但数据未更新,刷新后才显示新值
原因:浏览器缓存了list.jsp的响应。Servlet 跳转用的是response.sendRedirect("list.jsp")(302 重定向),但list.jsp本身未设置缓存控制头,浏览器可能复用旧缓存。
解决:在list.jsp顶部添加缓存禁用指令:
<% response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); response.setHeader("Pragma", "no-cache"); response.setDateHeader("Expires", 0); %>4.4 现象:点击“删除”按钮,URL 变为delete?id=5,但数据库无变化,控制台无任何日志
原因:delete.jsp或DeleteServlet.java中,request.getParameter("id")返回null,因为 HTML 表单的method="get"时,<a href="delete?id=${emp.id}">的链接参数名是id,但 Servlet 中request.getParameter("id")取不到——因为delete是 JSP 文件名,Tomcat 默认将其映射为delete.jsp,而delete.jsp里没写 Java 代码处理删除逻辑,它只是个静态页面。真实删除逻辑应在DeleteServlet.java中,且web.xml必须有<servlet-mapping>将/deleteURL 映射到该 Servlet。
解决:检查web.xml是否有:
<servlet> <servlet-name>DeleteServlet</servlet-name> <servlet-class>com.emp.servlet.DeleteServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>DeleteServlet</servlet-name> <url-pattern>/delete</url-pattern> </servlet-mapping>确保链接是<a href="delete?id=${emp.id}">(指向/delete),而非<a href="delete.jsp?id=${emp.id}">(指向静态 JSP)。
4.5 现象:部署到 Tomcat 9 后,web.xml报错The content of element type "web-app" must match "(icon?,display-name?,description?,distributable?,context-param*,filter*,filter-mapping*,listener*,servlet*,servlet-mapping*,session-config?,mime-mapping*,welcome-file-list?,error-page*,taglib*,resource-env-ref*,security-constraint*,login-config?,security-role*,env-entry*,ejb-ref*,ejb-local-ref*)"
原因:web.xml的 DTD 或 schema 声明版本过低。Tomcat 9 要求web-app版本为4.0,而老项目常写2.3或2.4。
解决:将web.xml顶部声明替换为:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">5. 进阶改造:把“教学项目”变成“可交付原型”的 3 个关键动作
跑通只是起点。这个系统真正的价值,在于它提供了一个极简但完整的骨架,让你能快速验证业务想法。我带团队做内部工具时,常以它为基线,三天内完成需求落地。以下是三个最实用、零成本的改造点,每个都附具体代码和效果说明。
5.1 给员工列表加搜索框:10 行代码实现模糊查询
原始list.jsp是全量展示,加搜索只需两步:
Step 1:在list.jsp表格上方加表单
<form action="list" method="post"> <input type="text" name="keyword" placeholder="按姓名或部门搜索" value="${param.keyword}"> <button type="submit">搜索</button> <a href="list">清空搜索</a> </form>Step 2:修改ListServlet.java的doPost方法
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String keyword = request.getParameter("keyword"); List<Employee> employees; if (keyword != null && !keyword.trim().isEmpty()) { employees = new EmployeeDao().search(keyword.trim()); // 新增 search 方法 } else { employees = new EmployeeDao().findAll(); } request.setAttribute("employees", employees); request.getRequestDispatcher("list.jsp").forward(request, response); }Step 3:在EmployeeDao.java中新增search方法
public List<Employee> search(String keyword) { String sql = "SELECT * FROM employee WHERE name LIKE ? OR department LIKE ?"; try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String likeKeyword = "%" + keyword + "%"; ps.setString(1, likeKeyword); ps.setString(2, likeKeyword); 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.setDepartment(rs.getString("department")); emp.setSalary(rs.getDouble("salary")); list.add(emp); } return list; } catch (SQLException e) { e.printStackTrace(); return new ArrayList<>(); } }效果:输入“技术”,返回所有
name或department含“技术”的员工。无需改数据库、不引入新框架,纯 JDBC 原生实现。
5.2 用 Cookie 记住用户名:让用户下次打开登录页时自动填充
LoginServlet.java登录成功后,添加 Cookie 写入逻辑:
// 登录验证通过后 Cookie userCookie = new Cookie("username", username); userCookie.setMaxAge(60 * 60 * 24 * 7); // 7 天 userCookie.setPath("/"); // 作用域为整个应用 response.addCookie(userCookie);然后在login.jsp的<input>中读取:
<input type="text" name="username" value="${cookie.username.value}" />注意:
cookie.username.value是 EL 表达式,cookie是隐式对象,username是 Cookie 名。若用户清除 Cookie,value为 null,输入框为空,安全无副作用。
5.3 导出 Excel:用 Apache POI 一行代码生成可下载文件
在ListServlet.java中新增导出逻辑(需引入poi-5.2.4.jar):
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { if ("export".equals(request.getParameter("action"))) { List<Employee> employees = new EmployeeDao().findAll(); HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("员工列表"); String[] headers = {"ID", "姓名", "部门", "薪资"}; HSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } int rowNum = 1; for (Employee emp : employees) { HSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(emp.getId()); row.createCell(1).setCellValue(emp.getName()); row.createCell(2).setCellValue(emp.getDepartment()); row.createCell(3).setCellValue(emp.getSalary()); } response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment; filename=employees.xls"); workbook.write(response.getOutputStream()); return; } // ... 原有 list 逻辑 }在list.jsp加导出链接:
<a href="list?action=export">导出 Excel</a>效果:点击即下载
employees.xls,Excel 打开后格式规整。POI 4.x+ 支持.xlsx,但老项目用.xls兼容性更好,无需额外依赖。
我习惯在接手一个新项目时,先用这个员工管理系统搭起本地开发环境,再逐步替换为真实业务模块——它像一把瑞士军刀,不锋利,但每个刃口都磨得恰到好处。希望帮到你。
本文还有配套的精品资源,点击获取