JavaWeb员工信息管理系统实战:Servlet+JSP+JDBC从建库到分页查询

发布时间:2026/10/7 17:46:16
JavaWeb员工信息管理系统实战:Servlet+JSP+JDBC从建库到分页查询 简介这是一套面向高校计算机专业学生与JavaWeb初学者、用于课程设计或毕业设计的企业员工信息管理系统完整资料包围绕员工信息管理、请假审批、出勤与工资核算等业务场景展开帮助读者解决从需求分析到系统测试的全流程开发问题。包内共8个文件包含2个doc与1个docx文档、1个ppt答辩演示、1个sql数据库脚本、1个zip源代码压缩包以及2个mp4讲解视频整体约29.64MB覆盖论文、任务书、中期检查表、答辩PPT、数据库与源码等关键材料。已有178人学习下载。读者可借助源码与数据库快速搭建B/S架构系统通过录屏掌握项目部署、数据库创建与启动流程并对照论文理解管理员与普通员工两大模块的功能划分包括部门管理、员工信息管理、出勤管理、工资管理及请假审核等适合需要完整赛题方案与排错思路的开发者参考。1. 从一份“能跑起来”的 JavaWeb 员工信息管理系统说起很多同学做毕设或者公司内部小工具时都会碰到同一个场景需求不复杂就是管管员工档案、部门、入职离职、薪资条但真动手写发现光是把 JavaWeb 这套东西串起来就够呛。企业员工信息管理系统这个题目本质上就是一套典型的 CRUD 后台员工表、部门表、职位表、用户登录、分页查询、条件筛选、Excel 导入导出。它不追求高并发也不玩微服务考的是你对 Servlet、JSP、JDBC、MySQL 这条链路的掌控力。适合谁适合正在做课程设计、毕设或者刚入行想拿一个完整案例练手的人。你把这套跑通数据库增删改查、连接池、事务、分页这些基本功就都落地了。下面我按实际做项目的顺序把选型、建库、编码、避坑、进阶一条线讲清楚。2. 技术选型与工程骨架为什么这套组合至今还能打2.1 为什么是 Servlet JSP JDBC而不是一上来就 Spring Boot先说实话如果今天从零做一个新项目我大概率直接上 Spring Boot。但企业员工信息管理系统这个题目很多场景下要求你展示对 JavaWeb 底层链路的理解尤其是课程设计和答辩环节。Servlet JSP JDBC 这套组合能让你清楚看到一次请求从浏览器到数据库再回来的完整路径没有框架帮你藏黑匣子。具体来说Servlet 负责接收请求、调业务、转发视图JSP 负责渲染JDBC 负责和 MySQL 对话。三层各司其职代码量不大但每一行你都知道它在干嘛。常见做法是用 MVC 手动分层entity 放实体类dao 放数据库操作service 放业务逻辑servlet 放控制层jsp 放页面。这个结构虽然土但它就是后面所有框架的雏形。你把这套写明白了再去学 Spring MVC会发现只是把 new 对象的过程交给了容器。选型上还有几个现实考虑。第一IDEA 运行 JavaWeb 项目配置相对成熟Tomcat 插件一挂就能跑不需要额外装一堆东西。第二MySQL 免费、资料多mysql 数据库常用命令网上一搜一大把遇到问题好查。第三JDBC 直连虽然性能一般但配合连接池足够撑起一个内部管理系统的量级。所以这套组合不是最优解但它是最稳、最容易复现的解。2.2 工程目录结构与依赖清单一个能跑的最小工程目录结构建议这样组织src/ main/ java/ com/company/ems/ entity/ Employee.java, Department.java, User.java dao/ EmployeeDao.java, DepartmentDao.java, UserDao.java service/ EmployeeService.java, UserService.java servlet/ EmployeeServlet.java, LoginServlet.java util/ DBUtil.java resources/ db.properties webapp/ WEB-INF/ web.xml css/ js/ employee_list.jsp employee_edit.jsp login.jsp依赖方面如果不用 Maven就把下面这几个 jar 丢进 WEB-INF/libmysql-connector-java、druid或 c3p0、jstl、servlet-apiTomcat 自带编译时用 provided。用 Maven 的话pom.xml 里加对应坐标即可。注意 mysql 驱动版本要和你本地 MySQL 服务端版本匹配8.x 的服务端配 8.x 的驱动否则时区报错能折腾你半天。数据库连接信息不要硬编码在 Java 里放到 db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/ems?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyourpassword这里 serverTimezone 是 8.x 驱动必须带的不带就抛The server time zone value is unrecognized。characterEncoding 设成 utf8 保证中文不乱码。这两个参数是血泪经验新手十有八九在这翻车。2.3 用 DBUtil 封装连接与连接池接入直接DriverManager.getConnection每次新建连接并发一上来就崩。正确做法是接连接池。我一般用 Druid配置简单监控也方便。DBUtil 负责加载配置、初始化数据源、提供获取连接和关闭资源的方法public class DBUtil { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据源初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 关闭资源顺序不能反ResultSet - Statement - Connection 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) {} } }逻辑说明静态代码块只在类加载时执行一次保证数据源全局唯一。getConnection 从池里借连接用完 close 其实是归还给池不是真关闭。参数说明db.properties 里的 key 名要和 Druid 的约定一致driver 对应 jdbc.driverurl 对应 jdbc.url以此类推。如果你用 c3p0key 名要换成 c3p0.driverClass 那套别混用。提示连接池的初始连接数和最大连接数在 db.properties 里用 initialSize、maxActive 配内部系统 initialSize 设 5、maxActive 设 20 足够别一上来设几百数据库扛不住。3. 数据库设计与增删改查落地从建表到分页查询3.1 员工表、部门表、用户表怎么建才不返工建表是后悔药最难买的一步。字段类型、长度、约束没定好后面改结构能让你把数据迁移脚本写到手软。核心三张表CREATE TABLE department ( dept_id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL UNIQUE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE employee ( emp_id INT PRIMARY KEY AUTO_INCREMENT, emp_name VARCHAR(30) NOT NULL, gender TINYINT DEFAULT 1 COMMENT 1男 2女, phone VARCHAR(20), email VARCHAR(50), dept_id INT, position VARCHAR(30), hire_date DATE, salary DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1在职 0离职, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES department(dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sys_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个关键决策。字符集统一 utf8mb4别用 utf8utf8 存不了 emoji员工备注里万一有个表情就报错。salary 用 DECIMAL 不用 FLOAT金额计算不能有精度丢失。status 用 TINYINT 做逻辑删除离职员工不物理删方便查历史。外键约束加上防止部门删了员工还挂着孤儿记录。但注意如果后面要做批量导入外键检查会拖慢速度导入前可以临时SET FOREIGN_KEY_CHECKS0导完再开。3.2 EmployeeDao 的增删改查与条件分页DAO 层是整个系统干活最多的地方。先看插入和更新再看最容易写错的分页查询。public class EmployeeDao { public int insert(Employee emp) { String sql INSERT INTO employee(emp_name,gender,phone,email,dept_id, position,hire_date,salary,status) VALUES(?,?,?,?,?,?,?,?,?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, emp.getEmpName()); ps.setInt(2, emp.getGender()); ps.setString(3, emp.getPhone()); ps.setString(4, emp.getEmail()); ps.setInt(5, emp.getDeptId()); ps.setString(6, emp.getPosition()); ps.setDate(7, new java.sql.Date(emp.getHireDate().getTime())); ps.setBigDecimal(8, emp.getSalary()); ps.setInt(9, emp.getStatus()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(新增员工失败, e); } } // 条件分页查询keyword 匹配姓名/手机deptId 为 0 表示不限 public ListEmployee queryByPage(String keyword, int deptId, int pageNo, int pageSize) { StringBuilder sql 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.isEmpty()) { sql.append(AND (e.emp_name LIKE ? OR e.phone LIKE ?) ); params.add(% keyword %); params.add(% keyword %); } if (deptId 0) { sql.append(AND e.dept_id ? ); params.add(deptId); } sql.append(ORDER BY e.emp_id DESC LIMIT ?,?); params.add((pageNo - 1) * pageSize); params.add(pageSize); ListEmployee list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i params.size(); i) { ps.setObject(i 1, params.get(i)); } ResultSet rs ps.executeQuery(); while (rs.next()) { list.add(mapRow(rs)); } } catch (SQLException e) { throw new RuntimeException(分页查询失败, e); } return list; } }逻辑说明用 StringBuilder 动态拼 SQL配合 List 收集参数避免手写一堆 if 分支。WHERE 11是个小技巧后面每个条件都能直接AND开头不用判断是不是第一个条件。参数说明pageNo 从 1 开始LIMIT 的偏移量是(pageNo-1)*pageSize这个公式写错会导致第一页数据重复或丢失。keyword 用 LIKE 两边加百分号注意这会让索引失效数据量上万后要改成前缀匹配或全文索引。注意千万不要用字符串拼接把 keyword 直接塞进 SQL那是 SQL 注入的经典入口。PreparedStatement 的占位符是底线没有商量余地。3.3 事务控制员工调岗这种操作必须包在一个事务里有些业务不是单表操作。比如员工调岗既要更新 employee 的 dept_id又要往调岗记录表插一条日志。这两步要么都成功要么都回滚。JDBC 默认自动提交必须手动关掉public void transferDept(int empId, int newDeptId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 String sql1 UPDATE employee SET dept_id? WHERE emp_id?; try (PreparedStatement ps1 conn.prepareStatement(sql1)) { ps1.setInt(1, newDeptId); ps1.setInt(2, empId); ps1.executeUpdate(); } String sql2 INSERT INTO transfer_log(emp_id,new_dept_id,transfer_time) VALUES(?,?,NOW()); try (PreparedStatement ps2 conn.prepareStatement(sql2)) { ps2.setInt(1, empId); ps2.setInt(2, newDeptId); ps2.executeUpdate(); } conn.commit(); // 两步都成功才提交 } catch (SQLException 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) {} } } }逻辑说明setAutoCommit(false) 之后所有操作都在同一个事务里commit 才真正落库出异常就 rollback。参数说明注意 conn 用完要恢复自动提交再归还连接池否则下一个借到这条连接的人会莫名其妙地不提交。这是连接池环境下特有的坑直连时不会遇到。4. 避坑与排查那些让系统跑不起来的常见问题4.1 中文乱码现象、原因、解决现象页面上输入的中文存进数据库变成问号或者查询出来是乱码。原因编码链路有三段——浏览器提交、Tomcat 解码、数据库存储任何一段编码不一致就乱。解决第一JSP 页面头部% page contentTypetext/html;charsetUTF-8 %第二Servlet 里request.setCharacterEncoding(UTF-8)必须在取参数之前调用第三数据库连接 URL 带characterEncodingutf8建表用 utf8mb4。三段都对齐乱码基本绝迹。4.2 数据库连接池耗尽现象、原因、解决现象系统跑一会儿就卡死日志报wait millis 60000, active 20。原因某处代码借了连接没还或者 ResultSet、Statement 没关连接被占满。解决所有数据库操作必须用 try-with-resources 或 finally 里 close顺序是 ResultSet → Statement → Connection。排查时把 Druid 的监控页面打开看 active 连接数是不是只增不减是的话就是泄漏。4.3 时区报错现象、原因、解决现象启动就抛The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因MySQL 8.x 驱动要求明确指定时区系统默认时区它认不出来。解决连接 URL 加serverTimezoneAsia/Shanghai。如果还不行检查 MySQL 服务端的 time_zone 变量必要时在 my.ini 里设 default-time-zone。4.4 外键约束导致删除失败现象、原因、解决现象删除部门时报Cannot delete or update a parent row。原因该部门下还有员工外键挡住了。解决要么先把员工调走或逻辑删除要么用逻辑删除代替物理删除。我一般推荐部门也做逻辑删除加个 status 字段前端只显示有效部门历史数据保留。4.5 IDEA 运行 JavaWeb 项目配置踩坑现象、原因、解决现象项目在 IDEA 里点运行404 或者 ClassNotFound。原因多半是 Artifact 没配好或者 Tomcat 的 Application context 路径不对。解决Project Structure → Artifacts 里确认 output 目录包含编译后的 classes 和 libRun Configuration 里 Deployment 标签确认 Artifact 已添加Application context 设成/或/ems访问路径要和它一致。这个配置是 idea 运行 javaweb 项目配置里最高频的翻车点。5. 进阶技巧Excel 导入导出与登录拦截器5.1 用 POI 做员工 Excel 批量导入内部系统绕不开 Excel 导入。用 Apache POI核心是把每一行映射成 Employee 对象再批量插入。注意两个点一是单元格类型判断数字和字符串要分开取二是导入前做数据校验手机号格式、部门是否存在别一股脑塞进数据库。public int importFromExcel(InputStream in) throws Exception { Workbook wb WorkbookFactory.create(in); Sheet sheet wb.getSheetAt(0); ListEmployee list new ArrayList(); // 第 0 行是表头从第 1 行开始 for (int i 1; i sheet.getLastRowNum(); i) { Row row sheet.getRow(i); if (row null) continue; Employee emp new Employee(); emp.setEmpName(getCellString(row.getCell(0))); emp.setPhone(getCellString(row.getCell(1))); emp.setEmail(getCellString(row.getCell(2))); // 部门名称转 ID查不到就跳过这一行 Integer deptId deptDao.findIdByName(getCellString(row.getCell(3))); if (deptId null) continue; emp.setDeptId(deptId); list.add(emp); } return employeeDao.batchInsert(list); // 批量插入别循环单条插 } private String getCellString(Cell cell) { if (cell null) return ; cell.setCellType(CellType.STRING); // 统一按字符串取避免数字变科学计数 return cell.getStringCellValue().trim(); }逻辑说明WorkbookFactory.create 能自动识别 xls 和 xlsx。getCellString 里强制 setCellType 成 STRING是为了防止手机号被 Excel 当成数字读成 1.38E10。参数说明batchInsert 用addBatchexecuteBatch一千条一批比单条插入快一个数量级。导入前建议先SET FOREIGN_KEY_CHECKS0导完再开避免外键逐行检查拖慢速度。5.2 登录拦截器一个 Filter 挡住所有未登录请求不想在每个 Servlet 里都写登录判断就加一个 Filter拦所有请求放行登录页和静态资源其余检查 session 里有没有用户。WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行登录相关和静态资源 if (uri.endsWith(login.jsp) || uri.endsWith(login) || uri.contains(/css/) || uri.contains(/js/)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); } else { chain.doFilter(req, resp); } } }逻辑说明WebFilter(/*) 拦截全部白名单里的直接放行。参数说明request.getContextPath() 拿到项目上下文路径重定向时拼上否则部署到非根路径会 404。这个 Filter 是系统安全的第一道门别省。5.3 验证系统是否真的跑通三个必测场景写完别急着交按这三个场景过一遍。第一新增一个员工刷新列表能看到重启 Tomcat 后数据还在说明落库成功。第二用错误密码登录应该被挡在登录页直接访问员工列表 URL应该被 Filter 弹回登录页。第三导入一个含 500 行数据的 Excel观察耗时和数据库记录数是否一致中途故意留一行部门不存在确认它被跳过而不是整批失败。我自己的习惯是每加一个功能先在数据库里手动跑一遍对应的 SQL确认语句本身没问题再去调 Java 代码。这样出问题时能快速定位是 SQL 写错还是代码传参错。这套系统不难难的是细节都对齐。希望帮到你。本文还有配套的精品资源点击获取