尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java Web员工管理系统实战:Servlet+JSP+JDBC从表设计到分页事务

Java Web员工管理系统实战:Servlet+JSP+JDBC从表设计到分页事务 简介基于Java的企业员工信息管理系统设计与实现文档面向计算机相关专业毕业生、Java Web初学者及课程设计在校学生。文档按毕业设计规范结构展开覆盖需求分析、系统概要设计、系统功能实现与系统测试等环节可帮助读者理顺从业务调研到系统落地的完整流程。系统采用B/S架构使用MyEclipse编写程序前台基于JSP数据库使用MySQL服务器采用Tomcat功能按管理员与普通员工两类角色划分管理员可操作系统全部功能涵盖部门管理、员工信息管理、出勤管理、工资管理及请假审核普通员工仅能查看工资和请假兼顾功能完整性与安全性。资源包共1个文件为docx格式文档大小约369KB内含中英文摘要、目录及详细正文章节结构清晰便于对照修改。目前已有500人学习下载适合需要梳理系统设计思路、撰写论文或搭建同类员工信息管理系统的读者。1. 基于Java企业员工信息管理系统代码之外的第一个决定每当在课程设计题目里看到“基于Java企业员工信息管理系统”这几个字我就知道又有一个同学要开始跟 Tomcat 和 MySQL 搏斗了。这个系统几乎是 Java Web 课程设计的常青树登录会话、部门与员工一对多、分页搜索、权限区分每个点都是日后面试常考的基础。把它吃透比背十道 java 面试八股文更有用。它适合三类人正在赶课程设计的学生、想练手 Java Web 的转行者以及要给公司内部做小工具的一线开发。我不建议你一开始就扑向 Spring Boot先把 Servlet JSP JDBC 这条老路走通你才算真正见过 Java Web 的底层长相。这个标题真正考验你的不是会不会写“增删改查”而是能不能把数据关系理清楚、把代码分层分明白。2. 选型与设计为什么我建议用 Servlet JSP JDBC 而不是一上来就 Spring Boot2.1 课程设计与生产项目的分界线先看你的交付物是什么标题里只写了“基于Java”没指定框架这是很多人的第一个自由选择题。常见的做法有三套JSP Servlet JDBC、SSMSpring MVC MyBatis、Spring Boot MyBatis/JPA。不同方案的学习曲线和交付痕迹差别很大先看下表方案学习曲线部署难度答辩友好度简历价值JSP Servlet JDBC陡但要手写请求/响应链路简单WAR包丢进Tomcat高能讲清底层原理中等SSM中等配置多中等中但配置文件容易背锅高Spring Boot平缓自动配置简单内嵌Tomcat低封装太黑容易一问三不知高我的建议很直接如果课程设计评分标准里有“必须使用 JSP”或者答辩老师喜欢追着原理问那就乖乖用 JSP Servlet。与其用 Spring Boot 写一个你讲不清楚启动流程的项目不如用传统方案把它做透。如果没有任何框架限制而且你想把它作为找工作时的项目写进简历那用 Spring Boot 更合适但这类方案不在标题默认的“基于Java”语境里也不是本文展开的重点。这里先立住一个原则这个系统无论用什么框架数据关系是核心。我在同类项目里见过太多人把部门名称直接写在员工表里导致部门改名时只能批量 update。正确做法是拆表让数据关系本身成为业务的约束。下面就开始做这件事。2.2 数据库表设计五张表撑起整个员工管理系统员工管理系统不是只有“员工”一张表。我会拆出 sys_user、dept、emp、job、attendance 这五张表分别管登录用户、部门、员工、职位、考勤。员工和部门是多对一员工和职位是多对一用户和员工是逻辑上的 1 对 1也就是说员工表里不放登录密码。建表语句是整个系统的地基下面这段 SQL 可以直接执行CREATE DATABASE employee_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL COMMENT 存MD5或BCrypt后的值, real_name VARCHAR(50), role VARCHAR(20) DEFAULT EMPLOYEE COMMENT ADMIN/EMPLOYEE ); CREATE TABLE dept ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL, manager_id INT COMMENT 部门经理员工ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE job ( id INT PRIMARY KEY AUTO_INCREMENT, job_name VARCHAR(50) NOT NULL, job_level INT COMMENT 职级用于排序 ); CREATE TABLE emp ( id INT PRIMARY KEY AUTO_INCREMENT, emp_no VARCHAR(20) UNIQUE COMMENT 工号业务上唯一, name VARCHAR(50) NOT NULL, gender CHAR(1), phone VARCHAR(20), dept_id INT, job_id INT, hire_date DATE, salary DECIMAL(10,2), status TINYINT DEFAULT 1 COMMENT 1在职0离职, CONSTRAINT fk_emp_dept FOREIGN KEY (dept_id) REFERENCES dept(id), CONSTRAINT fk_emp_job FOREIGN KEY (job_id) REFERENCES job(id) ); CREATE TABLE attendance ( id INT PRIMARY KEY AUTO_INCREMENT, emp_id INT, work_date DATE, check_in TIME, check_out TIME, CONSTRAINT fk_att_emp FOREIGN KEY (emp_id) REFERENCES emp(id) );这段 DDL 里三个容易被忽略的点。第一字符集用 utf8mb4 而不是 utf8因为 MySQL 的 utf8 最多只能存 3 字节员工备注里万一有个生僻字或者表情符号插入直接报错。第二外键一定要建答辩时老师问“数据一致性怎么保证”外键是你能拿得出的第一个理由。第三工号 emp_no 加了 UNIQUE业务上员工编号必须唯一不要拿自增主键当工号用否则换系统或者数据迁移时你会后悔。可能有人会问为什么不把部门名称冗余到 emp 表里查询时少一次 JOIN确实读列表时 JOIN 会多一点代码但换来的是一致性和可维护性。部门名称只在 dept 表维护一处员工表只存 dept_id这是经典范式设计。等到查列表时用 LEFT JOIN 把 dept_name 带出来即可反正 SQL 不复杂。下面这段查询是列表页最常用的SELECT e.*, d.dept_name FROM emp e LEFT JOIN dept d ON e.dept_id d.id WHERE e.status 1 ORDER BY e.id DESC我用 LEFT JOIN 而不是 INNER JOIN是因为员工可能还没分配部门但列表里依然要把这个人显示出来。答辩时你能主动说出这个 JOIN 的选择理由比被老师问出来再解释要好得多。2.3 数据模型与Java实体类的映射字段类型、包装类、时间精度数据库表建好后接着就是把它映射成 Java 实体类。这里有个非常高频的坑数据库的 INT 对应 Java 的 int但当查询结果里这个字段是 NULL 时基本类型 int 会自动拆箱直接抛空指针。所以实体类里我都用 Integer、Long、BigDecimal 这类包装类配合 ResultSet 的 getObject() 或者 getInt() 都能安全处理 null。时间字段也是一样的逻辑。别再用 java.util.Date 和 SimpleDateFormat 了Java 8 开始有 java.time 包LocalDate 对应 DATELocalDateTime 对应 DATETIME。一旦用上 LocalDate格式化就交给 DateTimeFormatter不再有线程安全问题代码也干净得多。员工实体的典型写法如下public class Employee { private Integer id; private String empNo; private String name; private String gender; private String phone; private Integer deptId; private Integer jobId; private LocalDate hireDate; private BigDecimal salary; private Integer status; private String deptName; // 联查出来的冗余字段只读 // 省略getter/setter }额外加了一个 deptName 字段它不是数据库表中的列而是列表页联查后展示用的。这种“实体类里带一个只读冗余字段”的做法在小型管理系统里很常见可以少写一大段对象组装代码。如果你追求更规范的分层可以单独建一个 EmployeeVO把 deptName 放进去但课程设计阶段没必要把简单问题复杂化。再展开一个更进阶的细节Java 实体对象的复制。如果你要给编辑页回显数据直接把对象塞给表单有时候你需要一份“旧值”做对比。这时候用对象引用的浅拷贝会出问题——修改副本时原对象也会变。我一般不用 clone因为深浅拷贝的规则本身就很绕更稳的做法是手动 new 一个 DTO把需要的字段一个个传过去。像员工信息这种字段不多的小对象手工拷贝反而最可靠这就是 java 对象深度拷贝在实际写码时最常见的替代方案。另外写这类系统时你会大量用到 Java 容器。列表查询结果用 ArrayList 没问题但构造动态查询条件时我更偏向用 LinkedHashMap 保存参数和值的键值对因为它能保证参数顺序。JDBC 的 PreparedStatement 占位符是按下标 1、2、3 来的如果你用 HashMap遍历顺序不受控一旦参数顺序和 SQL 里的问号顺序不一致数据就会写进错误的字段这是真实发生过的翻车现场。养成这个习惯后后面写动态 SQL 会顺很多。3. 环境与项目骨架从JDK配置到跑起第一个查询3.1 JDK、Tomcat、MySQL版本搭配别让环境变量成为第一道坎代码写得再好环境跑不起来也白搭。根据我的经验2025 年的当前环境下最稳的组合是 JDK 8 或 11 Tomcat 9 MySQL 8.0 mysql-connector-java 8.x。这个组合的 JSP/Servlet 包名还是 javax.servlet网上绝大多数资料都能直接用踩坑成本最低。但如果你手一抖装了 JDK 17又配了 Tomcat 10那就容易掉进大坑Tomcat 10 把 javax.servlet 改名成了 jakarta.servlet代码里所有 import javax.servlet.* 都会编译报错。这时要么把 Tomcat 降回 9要么把所有 javax 改成 jakarta工作量说大不大但卡住新手一小时很轻松。优先使用 JDK 8 Tomcat 9这是很多一线开发仍在用的保守配置。再来看 java 环境变量配置。很多人装了多个 JDK命令行里 java -version 永远显示旧版本原因多半是 PATH 里有一个 C:\Program Files\Common Files\Oracle\Java\javapath 条目它排在配置的 JAVA_HOME\bin 前面把真实版本盖住了。解决办法是把那个条目从 PATH 中删掉或者把 JAVA_HOME\bin 移到最前面。还有一点要提醒JDK 1.5 以后根本不需要配置 CLASSPATH网上有些老教程还在教配置 classpath那是写给九十年代的 JDK 看的千万别跟着配配了反而可能干扰。3.2 用Maven还是手动导包课程设计最稳的项目结构课程设计的传统做法是手动下载一堆 jar 包再在 IDEA 里逐个 Add as Library最后 Web/WEB-INF/lib 下面挤满了版本各异的依赖。我不推荐这么做因为依赖冲突会让你排查到怀疑人生。更稳的做法是用 Maven哪怕老师不要求Maven 也能帮你把依赖树理顺。只需要在 pom.xml 里声明依赖Maven 会自动下载并传递依赖dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies这里有两个关键点新手很容易踩。第一servlet-api 和 jsp-api 的 scope 必须是 provided因为 Tomcat 自带这两个 jar如果你把 scope 设成默认的 compile打成 WAR 包时会连 Tomcat 自带类一起打包进去启动直接报 NoSuchMethodError 这类莫名错误。第二mysql-connector-java 8.x 的驱动类是 com.mysql.cj.jdbc.Driver不再是老版本的 com.mysql.jdbc.Driver虽然旧类名也能用但会打一条红色日志看着难受。Maven 还会帮你规避一个常见问题不同版本的 jar 包冲突。比如你手动下载了一个 commons-dbutils 1.6又下载了一个 1.7类路径里存在两份运行时加载到哪个版本全看运气。这类“玄学报错”在 Maven 项目里几乎不会出现。3.3 数据库连接池为什么要用Druid而不是DriverManager.getConnection我见过很多课程设计的代码长这样每个 DAO 方法里都写一次 DriverManager.getConnection()用完再 close()。这种写法在小系统里勉强能跑但只要用户稍微多点页面刷新频繁些MySQL 就会报“Too many connections”。原因很简单每次创建连接都要经过 TCP 握手、MySQL 权限校验、会话初始化开销很大而且连接用完就销毁一进一出全是成本。解决方向是连接池。连接池本质上是一个 Java 容器里面提前放着若干个已经建立好的连接对象谁用谁借用完归还。这里我选 Druid阿里巴巴开源的项目配置简单还带监控面板比手写一个连接池靠谱得多。在 src/main/resources 下创建 druid.propertiesdriverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/employee_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai usernameroot password123456 initialSize5 maxActive20 minIdle5 maxWait10000 filtersstat注意 url 里必须有 serverTimezoneAsia/ShanghaiMySQL 8.0 的驱动默认取系统时区如果你在中国不指定这个参数连接会直接报 CST 时区错误。initialSize5 表示启动时创建 5 个连接maxActive20 是最大连接数maxWait10000 是拿连接的最长等待时间超过 10 秒直接抛异常避免线程无限等下去。Druid 的 filtersstat 能开启 SQL 监控。在 WEB-INF/web.xml 里配置一个 DruidServlet浏览器访问 /druid/index.html 就能看到每条 SQL 的执行次数、平均耗时、慢查询列表。这个功能在答辩时特别好用老师就算不问你也可以主动展示一条慢查询是怎么定位的。3.4 第一个JDBC查询从Class.forName到ResultSet的完整闭环连接池配好了接下来看怎么用。先写一个 JdbcUtils 工具类类加载时初始化 DruidDataSourcepublic class JdbcUtils { private static DruidDataSource dataSource; static { try { Properties props new Properties(); props.load(new FileInputStream(src/main/resources/druid.properties)); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }注意 JdbcUtils 里没有写关闭方法因为我会在所有 DAO 里用 try-with-resources 自动关闭。很多老教程里写的关闭工具类是手动 close 三层资源其实在 JDK 8之后已经没必要手写了try-with-resources 更安全。下面是一个完整的按 ID 查员工方法public Employee findById(Integer id) { String sql SELECT e.*, d.dept_name FROM emp e LEFT JOIN dept d ON e.dept_id d.id WHERE e.id ?; try (Connection conn JdbcUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Employee emp new Employee(); emp.setId(rs.getInt(id)); emp.setName(rs.getString(name)); emp.setDeptName(rs.getString(dept_name)); // 其他字段按同样方式赋值 return emp; } } } catch (SQLException e) { e.printStackTrace(); } return null; }第一PreparedStatement 的占位符下标从 1 开始不是 0写错会报 Parameter index out of range。第二try-with-resources 的变量声明括号里Connection 和 PreparedStatement 的关闭顺序是自动处理的完全不用管。第三rs.getXxx 的列名要和 SQL 里的别名一致比如 dept_name 来自别名不能写成 deptNameJDBC 不认驼峰。到这里环境、连接池、第一个查询都通了后面写 DAO、Servlet、JSP 就是循环往复的体力活。但正是这个“能跑通”的起点决定了你后面是顺利推进还是天天跟异常搏斗。4. 核心功能实现登录、员工CRUD、分页搜索的完整代码路径4.1 登录与Session密码加密和会话失效一个都不能少登录是所有管理系统的门面。先立一个底线密码不能明文存数据库。课程设计答辩时老师只要瞄一眼数据库看到明文密码印象分会大打折扣。最省事的做法是 MD5 加盐——虽然 MD5 在安全要求高的场景里已经不够看了但比明文强一个量级。注册时用 MD5Util.md5(password salt)登录时把用户输入的内容加同样的盐再比对。登录逻辑本身不复杂但有两个细节需要注意WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); UserDao dao new UserDao(); User user dao.findByUsername(username); if (user ! null MD5Util.verify(password, user.getPassword())) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() /employee/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }第一个细节是 session.setMaxInactiveInterval(30 * 60)这里的单位是秒等于 30 分钟无人操作后会话自动失效。虽然 Tomcat 默认也是 30 分钟但显式写出来能让老师知道你有会话管理意识。第二个细节是登录成功后用 sendRedirect 而不是 forward因为 forward 只是服务器内部跳转浏览器地址栏不变用户按 F5 刷新会重复提交表单可能造成重复登录的副作用。会话失效后怎么拦截未登录用户常见做法是写一个 Filter拦截除了 login 之外的所有 URL检查 session 里有没有 loginUser没有就跳回登录页。这个 Filter 代码很固定相信你能写出来这里就不贴了。需要提醒的是别把 Filter 底下的请求路径写死用 req.getRequestURI() 判断一下是否包含 .css/.js/.png否则页面样式会加载不出来。4.2 员工列表分页用PageBean把SQL和前端解耦员工数量一旦超过几十条不分页的页面就像一坨长长的流水账浏览器渲染也会卡。分页的 SQL 核心是 LIMIT offset, pageSize配合一个 PageBean 把分页信息封装起来。PageBean 的简化版public class PageBeanT { private int pageNum; private int pageSize; private long total; private ListT rows; }DAO 层先查 total再查 rowspublic PageBeanEmployee findPage(int pageNum, int pageSize, String name, Integer deptId) { PageBeanEmployee pb new PageBean(); pb.setPageNum(pageNum); pb.setPageSize(pageSize); String countSql SELECT COUNT(*) FROM emp e WHERE 11; // 动态追加 and 条件同查询SQL // ... String listSql SELECT e.*, d.dept_name FROM emp e LEFT JOIN dept d ON e.dept_id d.id WHERE 11; // 追加条件、ORDER BY e.id DESC LIMIT ?, ? // 设置参数先设置业务参数最后设置 offset 和 pageSize // ... return pb; }这条路径里最容易踩的坑是参数顺序。如果前面有动态条件那么 PreparedStatement 的 set 顺序必须和 SQL 里的占位符顺序严格一致。我一般用一个 List 收集所有参数值最后统一设置避免手滑。offset 的计算是 (pageNum - 1) * pageSize第一页 offset 0不要写成 pageNum * pageSize那是典型的差一错误。前端列表底部分页导航用 JSTL 标签循环生成页码即可。这里有个思维点PageBean 把分页状态和业务数据封装在一起Controller 只需要往 request 里塞一个 pbJSP 页面用 ${pb.rows} 遍历数据、用 ${pb.total} 显示总条数。这样前后端各司其职不会出现 JSP 里写大量 Java 脚本块的情况。4.3 新增和修改如何用同一个JSP页面兜住两个场景很多入门项目会把新增和编辑写成两个 JSP 页面导致重复的 HTML 代码量大维护起来也累。更常见的做法是共用 employee_form.jsp通过一个隐藏的 id 字段区分动作id 为空就是新增id 有值就是编辑。后端 Servlet 收参的写法如下Integer id null; if (req.getParameter(id) ! null !req.getParameter(id).isEmpty()) { id Integer.parseInt(req.getParameter(id)); } String name req.getParameter(name); String gender req.getParameter(gender); String deptIdStr req.getParameter(deptId); if (deptIdStr ! null !deptIdStr.isEmpty()) { emp.setDeptId(Integer.parseInt(deptIdStr)); } // ... if (id null) { dao.insert(emp); } else { emp.setId(id); dao.update(emp); }新手最容易在这里翻一个大跟头前端页面如果只回显了部分字段比如编辑员工时下拉框没有设置选中项那么提交上来的 deptId 是空字符串后端 parseInt 就会抛 NumberFormatException。所以在取值时要先判断空串。另外我强烈建议在 JSP 中为隐藏 id 的 value 做空判断input typehidden nameid value${employee.id ! null ? employee.id : }/如果直接用 ${employee.id}当 employee 对象为 null 时EL 表达式会输出字符串 “null”提交到后端后 Integer.parseInt(null) 直接报错。这种问题很难一眼看出但只要你记住“EL 输出前必须判空”这条铁律就能避免。4.4 模糊搜索与部门筛选SQL拼接的三种边界情况列表页通常有一个主搜索框和一个部门筛选下拉框。动态 SQL 是逃不掉的。这里的核心安全原则是所有用户输入都必须交给 PreparedStatement 占位符处理绝不能用字符串拼接。我惯用的写法如下StringBuilder sql new StringBuilder(SELECT e.* FROM emp e WHERE 11); ListObject params new ArrayList(); if (name ! null !name.trim().isEmpty()) { sql.append( AND e.name LIKE ?); params.add(% name.trim() %); } if (deptId ! null deptId 0) { sql.append( AND e.dept_id ?); params.add(deptId); } if (status ! null) { sql.append( AND e.status ?); params.add(status); } // 最后执行时遍历 params 依次 setObject(i1, params.get(i))这里三个边界情况必须说清。第一WHERE 11 虽然看起来有点“野”但它让后面每个条件都能无脑追加 AND不需要判断是否是第一个条件代码复杂度瞬间降下来。第二LIKE 的 % 位置决定了匹配语义“%” name “%” 是包含匹配“%” 加在右侧是前缀匹配。业务上搜“张”应该匹配“张三”和“老张”所以要左右都加。第三deptId 从页面过来时可能是个空字符串后端要先转成 Integer 判断是否 0否则把 0 拼进 SQL 会查出所有 dept_id 为 0 的数据而实际上没有 id0 的部门导致列表一片空白。动态 SQL 写好后列表、新增、修改、删除这几个核心流程就能串成一条完整的链路。到这里系统已经能跑了但距离“稳定”还差一步很多坑得等你自己踩过一次才能长记性。下面我直接把高频的五个坑说出来避免你重复踩。5. 常见问题与避坑数据不一致、中文乱码、Tomcat重启卡死5.1 插入中文变“??”不是MySQL字符集没设utf8那么简单现象数据库表、连接串、页面编码都写了 UTF-8可插入的中文在库里变成了问号。原因第一层是建库时字符集没指定 utf8mb4默认 latin1 存不了中文。第二层是 MySQL Connector/J 8.x 对连接参数更严格url 里 characterEncodingutf8 经常不够你还需要在 MySQL 服务端把 character_set_server 改成 utf8mb4。第三层是 JSP 页面本身没声明 contentType导致浏览器提交中文时按 ISO-8859-1 解码。解决按顺序做三件事。建库时显式写 DEFAULT CHARACTER SET utf8mb4JSP 顶部写 % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8%在 web.xml 里配置一个 CharacterEncodingFilter把所有请求的编码统一成 UTF-8。最后别忘检查 Tomcat 的 server.xml 中 Connector 是否设置了 URIEncodingUTF-8这个对 GET 请求的乱码至关重要。按我经验80% 的乱码问题都出在 Tomcat 的 URIEncoding 上而不是代码里。5.2 更新员工时部门被清空埋点/调试/回滚的排查顺序现象编辑员工时只改了手机号保存后部门反而变成空白。原因前端表单里根本没有 dept_id 的输入项或者 JSP 下拉框没设置选中态导致提交的 deptId 为空。后端拿到 null 后直接 set 到 Employee 对象里再 update 整条记录未提交的旧值就被 null 覆盖了。这是最典型的“持久层更新覆盖旧值”问题。解决排查时不要一上来就加日志。先看浏览器开发者工具里提交的 Form Data 有哪些字段确认 deptId 是否出现。如果没出现就在编辑页表单里补上部门下拉框。但更稳妥的办法是后端只允许更新表单里出现的非空字段而不是把整个 Employee 对象序列化成 update SQL。具体实现可以做成“字段非空才拼接 set 子句”虽然 SQL 写得麻烦一点但能避免很多覆盖问题。另外如果表单里确实需要回显旧值记得在 JSP 的 option 标签里加上 selected${emp.deptId dept.id} 这样的判断。5.3 Tomcat热部署卡死注意静态资源占用与线程泄漏现象IDEA 里改一下 JSP点 Redeploy 后 Tomcat 直接卡死控制台毫无输出只能杀进程。原因大部分发生在 Windows 系统下。Tomcat 在删除旧 webapp 目录时某些文件被进程占用比如浏览器还开着页面导致 JS/CSS 文件句柄未释放或者 Druid 连接池没有在 contextDestroyed 时关闭连接线程没退干净。解决针对连接池泄漏在 web.xml 里配置一个实现了 ServletContextListener 的监听器在 contextDestroyed() 里调用 dataSource.close()。针对文件占用只能先杀 Tomcat 进程再重启。最省心的做法是开发阶段不要依赖热部署改完代码直接重启 Tomcat几秒钟的事。课程设计阶段本来就没什么超长启动流程别为了省那几秒把时间耗在卡死上。5.4 连接池连接耗尽finally里释放资源为什么还会泄漏现象系统跑一段时间后页面加载报“无法获取连接等待超时”Druid 监控显示 active 连接数满。原因最直接的泄漏是你根本没在 finally 里 close Connection。但还有一种隐蔽情况finally 块里先关闭了 ResultSet如果 ResultSet 是 nullrs.close() 会抛 NullPointerException导致后续 Statement 和 Connection 的关闭代码根本执行不到。这个我想特别提醒你因为有很多老教程的关闭模板是三层嵌套 try-catch一不小心就把后面的关闭吞了。解决使用 try-with-resources代替手写 finally。这是 JDK 7 以后的语言特性也是目前最不容易出错的写法。前面 JDBC 查询代码已经展示过不再重复。你需要注意一个细节Connection 和 PreparedStatement 必须在同一个 try 的括号里声明这样 close 顺序才能保证 PreparedStatement 先关闭否则连接归还后 PreparedStatement 还握着底层 socket时间长了照样泄漏。5.5 分页页码溢出前端传参攻击和后端校验现象把 URL 参数 pageNum 改成 -1 或 999页面要么报 SQL 异常要么返回空白。原因后端没做边界校验。pageNum 小于 1offset 变成负数MySQL 直接报错pageNum 大于总页数虽然 LIMIT 不减但会扫描大量无效行查询变慢同时前端展示空页显然不合理。解决在 Servlet 入口处统一校验。拿到 pageNum 字符串后先 parseInt失败就默认第一页然后判断是否小于 1小于 1 就重置为 1最后和总页数比较超过总页数就强制落到最后一页。这段代码比较固定建议写在 Controller 工具类里每个分页接口复用。另外你还可以给分页接口加一个最大 pageSize 限制比如每页最多 100 条防止有人把 pageSize 改成 999999 直接把整个表拉出来。6. 进阶与验证从“能跑”到“敢写进简历”6.1 用事务保证数据一致性删除部门时员工表的外键策略员工和部门存在外键约束直接删除一个还有员工的部门会被 MySQL 拒绝。业务上合理的做法是删除前检查部门下员工数为 0 才允许删除。这个检查与删除之间可能会插入新数据所以必须放进同一个事务。Connection conn JdbcUtils.getConnection(); try { conn.setAutoCommit(false); int count empDao.countByDeptId(conn, deptId); if (count 0) { throw new BizException(部门下存在员工不能删除); } deptDao.deleteById(conn, deptId); conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }这段代码的核心是让同一个 Connection 贯穿检查和删除两个 DAO 方法。所以 DAO 方法要设计成可以接收 Connection 参数的重载版本而不是各自去 getConnection()。如果你在两处各拿一个连接事务等于白设——不同连接各自有独立的隔离级别根本做不到原子性。6.2 日志、统一异常与try-with-resources的落地写法课程设计阶段可以 e.printStackTrace()但如果想让它出现在简历的项目描述里请把打印异常换成日志。不需要引入庞大的 log4j2JDK 自带的 java.util.logging 足够小型项目使用private static final Logger LOGGER Logger.getLogger(EmployeeService.class.getName()); LOGGER.warning(删除部门失败: deptId deptId);同时建议在 web.xml 里配置错误页把 500、404 分别指向友好的 JSP而不是让容器默认输出一大片英文异常栈。统一异常处理其实不复杂但很多课程设计都没做你做了就是加分项。最后我会走一遍自测清单登录失败和成功分别验证Session 过期后访问列表页是否跳回登录分页首页、末页、越界三种情况搜索框输入空格和 SQL 片段是否安全编辑页面回显、取消后数据是否不变删除一个被引用的部门是否提示友好直接访问 /employee/edit?id999 是否返回友好页面而不是白屏。这一套走完这个基于 Java 的企业员工信息管理系统就可以放心交出去。我自己做第一个系统时就是靠这条清单避免了答辩现场翻车——把用户乱输入当成最大的敌人而不是功能有没有写完。这个习惯让我少改很多 bug希望帮到你。本文还有配套的精品资源点击获取
返回列表