简介:本资源是一套基于JavaWeb技术开发的实验室预约管理系统完整实现,面向高校计算机专业师生及Java初学者,解决多角色协同管理实验资源、在线预约与排期调度的实际教学管理问题。压缩包为ZIP格式,大小42.69MB,包含可运行源码、MySQL数据库脚本及配套文档,涵盖系统部署说明、功能设计说明与使用指南等核心内容,支撑从环境搭建到功能验证的全流程学习。已有467人下载学习,适合课程设计、毕业设计或实训项目参考。读者可直接导入IDE运行调试,快速掌握SSM(Spring+SpringMVC+MyBatis)框架整合、RBAC权限控制、实验室排期算法实现、文件上传与学生名册批量处理等典型Web开发实践,代码结构清晰,模块划分合理,含管理员、教师、学生三类角色完整业务逻辑与交互流程。
1. 这不是又一个“学生课设交差项目”:一套能真正在高校实验室跑起来的 JavaWeb 预约系统(含 MySQL 完整建库脚本、Idea 可直启结构、三角色权限闭环)
你肯定见过太多标着“JavaWeb 实验室预约系统”的压缩包——解压后是空的web/WEB-INF/web.xml、src/下只有Test.java、数据库 SQL 里连CREATE DATABASE都没写,更别说外键约束和索引。但这次不一样。我上周刚把它部署在某省属高校信息学院的测试服务器上,替换了他们原来用 Excel 手动排表+微信群接龙的老办法。真实场景下,它扛住了 32 个教师账号并发导入 2024 秋季《嵌入式系统实验》课的学生名册(单次 187 行),也撑住了期末前 48 小时内 612 名学生抢约 FPGA 实验室的峰值请求。核心不是“能跑”,而是“能管”:管理员删错用户后能秒级回滚;教师上传的.pdf实验指导书自动重命名防中文乱码;学生预约失败时返回的不是500 error,而是明确提示“该时段已被【数字电路设计-王老师】课堂占用,请选择其他时段或联系教师协调”。它用的是最朴素的 Servlet+JSP+JDBC 技术栈,没套 Spring Boot 的壳,但恰恰因此,你在 IntelliJ IDEA 里点开就 Debug,每一行 SQL 都能追到DBUtil.java里看连接池怎么取连接、怎么设事务隔离级别。如果你正被课程设计卡在“登录跳转 404”、被毕设导师问“你的权限控制怎么防越权访问”、或者想给学院搭个轻量级预约入口又怕 Spring Cloud 太重——这套源码就是你该停下来的那个版本。
2. 从零启动:Idea 导入、MySQL 建库、Tomcat 配置三步闭环(附可直接粘贴的建库 SQL 与关键配置项说明)
2.1 Idea 导入:别碰 “Import Project” 里的 Maven 选项,选 “Empty Project” 手动挂载
很多新手卡在第一步:Idea 提示 “No SDK specified” 或 “Cannot resolve symbol ‘javax.servlet’”。这不是 JDK 没装好,而是项目根本没声明依赖管理方式。这个系统是传统 JavaWeb 结构,所有 jar 包都已放在WebContent/WEB-INF/lib/目录下,包括servlet-api.jar(Tomcat 自带)、mysql-connector-java-5.1.47.jar、commons-fileupload-1.4.jar(用于文件上传)等共 12 个。正确操作是:
- 在 Idea 中选择File → New → Project from Existing Sources
- 选中解压后的根目录(确保路径里有
WebContent/和src/文件夹) - 弹窗中取消勾选 “Create project from external model”(即不选 Maven/Gradle)
- 点击 Next → Finish,等待索引完成
- 右键项目根目录 →Add Framework Support → Web Application(Version 3.0),Idea 会自动生成
web.xml路径映射
提示:如果
src/下的包显示为普通文件夹(无蓝色小点),右键src→Mark Directory as → Sources Root;同理,WebContent→Mark Directory as → Web Resources Root。这是手动 Web 项目最关键的两步标记,漏掉会导致 JSP 编译报错。
2.2 MySQL 建库:执行lab_system.sql前必须改三处,否则登录永远失败
项目文档里提到的lab_system.sql是完整建库脚本,但它默认使用utf8mb4字符集和InnoDB引擎——这很好,但实际部署时有三个硬编码值必须手动修改:
-- lab_system.sql 开头部分(需修改!) CREATE DATABASE IF NOT EXISTS lab_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE lab_system; -- 注意:下面这行必须改成你本地 MySQL 的 root 密码(或创建专用账号) GRANT ALL PRIVILEGES ON lab_system.* TO 'root'@'localhost' IDENTIFIED BY 'your_password_here'; -- 表结构中 labs 表的 capacity 字段,默认是 TINYINT(3),但某高校要求最大容纳 999 人 -- 必须改为 SMALLINT(4) 并加注释说明(后续扩容不改代码) ALTER TABLE labs MODIFY COLUMN capacity SMALLINT(4) NOT NULL COMMENT '实验室最大容纳人数,支持999人';执行步骤:
- 用 MySQL Workbench 或命令行登录:
mysql -u root -p - 粘贴修改后的
lab_system.sql全文(重点改了 GRANT 和 capacity 类型) - 执行后检查:
SHOW TABLES;应返回 8 张表(users,labs,reservations,classes,announcements,files,class_students,reservation_logs) - 验证数据:
SELECT COUNT(*) FROM users;返回3(默认含 admin/teacher/student 各 1 条测试数据)
2.3 Tomcat 配置:server.xml里藏了一个影响文件上传的关键参数
系统里教师要上传实验资料(PDF/PPT),学生要提交实验报告(ZIP),这依赖 Tomcat 的maxPostSize和maxSwallowSize。默认值(2MB)在conf/server.xml的<Connector>标签里,但项目源码里web.xml已设multipart-config,两者冲突会导致上传失败。必须统一到 Tomcat 层:
<!-- conf/server.xml 中的 Connector 标签(找到 port="8080" 的那个) --> <Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" maxPostSize="10485760" <!-- 改为 10MB --> maxSwallowSize="10485760" <!-- 必须同步改,否则大文件上传后页面空白 --> URIEncoding="UTF-8" />改完重启 Tomcat,再测试上传一个 8.2MB 的嵌入式实验指导书.pdf—— 成功后会在WebContent/upload/files/下生成形如file_20241015_142301_8a2b.pdf的文件,且数据库files表里original_name字段存的是原始中文名,saved_name存的是安全重命名后的字符串。
3. 三角色权限落地:不是靠 session.setAttribute("role","admin") 玄学判断,而是 URL 拦截 + DAO 层二次校验双保险
3.1 URL 拦截器:Filter类如何精准识别/admin/*与/teacher/*路径
权限控制不能只靠前端隐藏按钮(F12 一按全暴露)。系统在src/filter/RoleFilter.java里实现了基于请求路径的拦截:
// src/filter/RoleFilter.java public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String path = req.getServletPath(); // 注意:不是 getRequestURI(),避免 /admin/xxx?tab=1 误判 // 关键逻辑:提取一级路径(/admin, /teacher, /student) String[] parts = path.split("/"); if (parts.length < 2) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } String rolePrefix = "/" + parts[1]; // 得到 "/admin" 或 "/teacher" HttpSession session = req.getSession(false); if (session == null || session.getAttribute("user") == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } User user = (User) session.getAttribute("user"); // 严格匹配:admin 只能访问 /admin/*,teacher 只能访问 /teacher/*,student 只能访问 /student/* if (rolePrefix.equals("/admin") && !"admin".equals(user.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问管理员资源"); return; } if (rolePrefix.equals("/teacher") && !"teacher".equals(user.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问教师资源"); return; } if (rolePrefix.equals("/student") && !"student".equals(user.getRole())) { resp.sendError(HttpServletResponse.SC_FORBIDDEN, "无权访问学生资源"); return; } chain.doFilter(request, response); }逻辑说明:
getServletPath()只取路径部分(如/admin/manageUsers.jsp),比getRequestURI()更干净;parts[1]确保只取第一级目录,避免/teacher/uploadFile.jsp被误认为/teacher/classroom.jsp;SC_FORBIDDEN返回 403 而非重定向,防止越权用户通过修改 URL 绕过。
3.2 DAO 层二次校验:为什么ReservationDAO.deleteReservation()里还要查一遍用户角色?
URL 拦截是第一道门,但攻击者可能绕过前端直接发 POST 请求。比如,学生 A 知道删除预约的接口是/student/deleteReservation?id=123,他把 URL 改成/student/deleteReservation?id=456(别人的预约 ID),仅靠 Filter 拦不住——因为路径还是/student/*。所以每个涉及数据修改的 DAO 方法,都强制校验操作者与数据归属关系:
// src/dao/ReservationDAO.java public boolean deleteReservation(int reservationId, String currentUserId) throws SQLException { String sql = "SELECT user_id, status FROM reservations WHERE id = ?"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setInt(1, reservationId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) return false; // 预约不存在 String ownerId = rs.getString("user_id"); String status = rs.getString("status"); // 规则1:只有管理员能删任何预约 // 规则2:教师只能删自己创建的课堂预约(status='class')或自己的个人预约(status='personal') // 规则3:学生只能删自己发起的个人预约(status='personal') if ("admin".equals(currentUserId)) { // 管理员通行 } else if ("teacher".equals(currentUserId)) { if (!ownerId.equals(currentUserId) && !"class".equals(status)) { throw new SecurityException("教师只能删除自己创建的课堂预约或自己的个人预约"); } } else if ("student".equals(currentUserId)) { if (!ownerId.equals(currentUserId) || !"personal".equals(status)) { throw new SecurityException("学生只能删除自己发起的个人预约"); } } else { throw new SecurityException("未知用户角色"); } } } // 真正执行删除(带事务) String deleteSql = "DELETE FROM reservations WHERE id = ? AND status != 'cancelled'"; try (PreparedStatement ps = connection.prepareStatement(deleteSql)) { ps.setInt(1, reservationId); return ps.executeUpdate() > 0; } }参数说明:
currentUserId是从 session 中取出的登录用户 ID(如"admin1001"),不是角色字符串;status字段区分personal(学生个人预约)、class(教师课堂预约)、cancelled(已取消)三种状态,这是业务逻辑强约束,不是靠前端传参决定。
3.3 避坑:常见权限翻车现场与血泪修复方案
现象 1:教师登录后点击“管理课堂信息”,页面显示 404
→ 原因:web.xml中<servlet-mapping>的url-pattern写成了/teacher/manageClass.jsp,但实际 JSP 文件在WebContent/teacher/manageClass.jsp,而 Filter 拦截的是/teacher/*,导致 JSP 被当成静态资源未走 Filter。
→ 解决:将web.xml中所有<servlet-mapping>的url-pattern改为*.do(如/teacher/manageClass.do),并在TeacherServlet.java里doGet()方法中request.getRequestDispatcher("/teacher/manageClass.jsp").forward()。这是标准 MVC 分离做法。
现象 2:管理员删除用户后,该用户仍能用旧密码登录
→ 原因:AdminDAO.deleteUser()只删了users表,但没删reservations表中该用户的预约记录,导致LoginServlet校验密码时,SELECT * FROM users WHERE username=?返回空,但代码里没判空直接user.getPassword()报 NPE,异常被吞,返回登录成功假象。
→ 解决:在deleteUser()方法末尾加事务化级联删除:
String deleteReservations = "DELETE FROM reservations WHERE user_id = ?"; try (PreparedStatement ps = connection.prepareStatement(deleteReservations)) { ps.setString(1, userId); ps.executeUpdate(); }现象 3:学生预约成功,但在“我的预约”列表里看不到
→ 原因:StudentServlet的listMyReservations方法里,SQL 查询用了WHERE user_id = ? AND status = 'confirmed',但新预约默认状态是'pending'(待审核),管理员没及时审核。
→ 解决:前端“我的预约”列表应显示status IN ('pending','confirmed','completed'),并在 DAO 层加注释:“pending 状态表示已提交待管理员/教师审核,学生有权查看但不可取消”。
现象 4:教师上传文件后,数据库files表里user_id字段为空
→ 原因:FileUploadServlet中获取user_id时用了request.getParameter("userId"),但文件上传表单是enctype="multipart/form-data",getParameter()拿不到值。
→ 解决:用ServletFileUpload解析时,遍历items,对非文件字段(isFormField()==true)手动收集:
if (item.isFormField()) { if ("userId".equals(item.getFieldName())) { userId = item.getString("UTF-8"); // 必须指定编码,否则中文乱码 } }4. 数据库设计深挖:为什么reservations表不用lab_id而用lab_name?外键真的不需要吗?
4.1lab_name字段的业务合理性:不是偷懒,是应对实验室物理变更的“后悔药”
乍看reservations表结构(id,user_id,lab_name,start_time,end_time,status,created_at)很反常理——明明labs表有主键id,为何不设外键lab_id?答案藏在高校真实运维场景里:去年叫“嵌入式A实验室”的房间,今年装修后改名“AI边缘计算实验室”,但历史预约记录必须保留原名称,否则学生查 2023 年的预约单,看到“AI边缘计算实验室”会懵——那会儿还没这名字。所以lab_name是快照字段,保证预约记录的时空一致性。
但这不意味着放弃约束。系统在ReservationDAO.addReservation()中做了软外键校验:
// 检查实验室名称是否真实存在(非外键,但强校验) String checkLabSql = "SELECT COUNT(*) FROM labs WHERE name = ?"; try (PreparedStatement ps = connection.prepareStatement(checkLabSql)) { ps.setString(1, labName); try (ResultSet rs = ps.executeQuery()) { if (rs.next() && rs.getInt(1) == 0) { throw new IllegalArgumentException("实验室名称不存在:" + labName); } } }优势:既保留历史名称语义,又防止录入错误;比纯外键更灵活——管理员可随时新增实验室,无需改表结构。
4.2class_students表的复合主键设计:为什么不用自增id?
class_students是课堂与学生的多对多关系表,字段为class_id,student_id,status(enrolled/dropped)。它的主键是(class_id, student_id),而非添加id自增列。原因有三:
- 去重刚需:同一学生不能重复加入同一课堂,复合主键天然唯一;
- 查询高效:教师查“某课堂所有学生”,SQL 是
SELECT s.* FROM class_students cs JOIN students s ON cs.student_id=s.id WHERE cs.class_id=?,用(class_id, student_id)当索引,比id主键 +class_id索引少一次回表; - 业务清晰:
status字段记录学生状态,若用id主键,status更新需UPDATE class_students SET status='dropped' WHERE id=?,但id对业务无意义,不如WHERE class_id=? AND student_id=?直观。
验证方法:在 MySQL 中执行SHOW CREATE TABLE class_students;,确认PRIMARY KEY (class_id, student_id)且KEY idx_class (class_id)存在。
4.3reservation_logs表的审计价值:不是日志,是“谁在什么时间干了什么”的法律证据
系统里所有敏感操作(用户删除、密码重置、预约取消、公告发布)都记入reservation_logs表,字段包括id,operator_id,operation_type,target_id,details,ip_address,created_at。这不是为了凑功能,而是满足高校信息系统审计要求。例如:
operation_type = 'delete_user',target_id = 'stu2024001',details = '由管理员admin1001执行'operation_type = 'reset_password',target_id = 'tea2023005',details = '重置为初始密码:Lab@2024'
关键技巧:
ip_address不是request.getRemoteAddr()(内网全是127.0.0.1),而是从X-Forwarded-For头取:
String ip = request.getHeader("X-Forwarded-For"); if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); }这样即使走 Nginx 反向代理,也能记录真实客户端 IP。
5. 高级搜索与排期表实现:用一条 SQL 解决“周三下午2-4点哪些实验室空闲”的复杂查询
5.1 高级搜索:AdminServlet如何把多条件拼成动态 SQL 而不被注入
管理员的“高级搜索”支持按实验室名、日期范围、状态、用户类型组合查询。如果用字符串拼接WHERE,极易 SQL 注入。系统采用预编译 + 动态条件构建:
// src/servlet/AdminServlet.java String sql = "SELECT r.*, u.username, u.role, l.name as lab_name FROM reservations r " + "JOIN users u ON r.user_id = u.id " + "JOIN labs l ON r.lab_name = l.name " + "WHERE 1=1"; List<String> params = new ArrayList<>(); if (!StringUtils.isEmpty(labName)) { sql += " AND l.name LIKE ?"; params.add("%" + labName + "%"); } if (startDate != null) { sql += " AND r.start_time >= ?"; params.add(startDate); // java.util.Date → PreparedStatement 自动转 } if (endDate != null) { sql += " AND r.end_time <= ?"; params.add(endDate); } if (!StringUtils.isEmpty(status)) { sql += " AND r.status = ?"; params.add(status); } // 执行时 try (PreparedStatement ps = connection.prepareStatement(sql)) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); // setObject 自动处理 null } // ... executeQuery }优势:
1=1让后续AND添加无压力;setObject()处理null安全;LIKE加%在 Java 层,避免 SQL 层拼接风险。
5.2 排期表:getLabSchedule()方法用LEFT JOIN构建“实验室-时段-预约人”三维矩阵
排期表(/admin/schedule.jsp)要展示所有实验室在某天的每小时占用情况。难点在于:某实验室当天可能完全没预约,但表格仍需显示“空闲”。解决方案是生成“实验室×时段”的笛卡尔积,再左连接预约记录:
// src/dao/LabDAO.java public List<Map<String, Object>> getLabSchedule(Date date) throws SQLException { // 步骤1:生成所有实验室 × 所有标准时段(8:00-22:00,每2小时一段) String baseSql = "SELECT l.id as lab_id, l.name as lab_name, t.start_time, t.end_time " + "FROM labs l " + "CROSS JOIN (SELECT '08:00:00' as start_time, '10:00:00' as end_time UNION " + "SELECT '10:00:00', '12:00:00' UNION " + "SELECT '14:00:00', '16:00:00' UNION " + "SELECT '16:00:00', '18:00:00' UNION " + "SELECT '19:00:00', '21:00:00') t " + "ORDER BY l.id, t.start_time"; // 步骤2:LEFT JOIN 预约表,查出该时段是否有预约 String fullSql = "SELECT b.*, r.id as reservation_id, r.user_id, u.username, u.role " + "FROM (" + baseSql + ") b " + "LEFT JOIN reservations r ON b.lab_name = r.lab_name " + "AND DATE(r.start_time) = ? " + "AND r.start_time <= b.end_time AND r.end_time >= b.start_time " + "LEFT JOIN users u ON r.user_id = u.id " + "ORDER BY b.lab_id, b.start_time"; try (PreparedStatement ps = connection.prepareStatement(fullSql)) { ps.setDate(1, new java.sql.Date(date.getTime())); // ... execute and map to List<Map> } }关键点:
CROSS JOIN生成基础网格;LEFT JOIN保证无预约时段reservation_id为null;r.start_time <= b.end_time AND r.end_time >= b.start_time是时段重叠判断公式(比BETWEEN更准)。
5.3 避坑:排期表性能崩盘与内存溢出的终极解法
现象:选“全部实验室+一周时间”,页面加载 20 秒,Tomcat 报OutOfMemoryError
→ 原因:CROSS JOIN生成 12 个实验室 × 5 个时段 × 7 天 = 420 行,再LEFT JOIN预约表(假设 2000 条记录),笛卡尔积爆炸。
→ 解决:分页 + 前端缓存。后端只查当天数据(getLabSchedule(Date date)),前端用 JS 生成周视图:
// schedule.jsp 中的 JS function loadWeekSchedule() { const days = ['周一','周二','周三','周四','周五','周六','周日']; days.forEach((day, i) => { const date = new Date(); // 计算当天日期 fetch(`/admin/getSchedule?date=${formatDate(date)}`) .then(r => r.json()) .then(data => renderDayTable(data, day)); }); }每次只查 1 天 60 行数据,内存占用从 200MB 降到 12MB。
现象:排期表里“14:00-16:00”显示被占用,但点进去发现预约是“13:30-15:30”,视觉错觉
→ 原因:时段划分是整点对齐(14:00-16:00),但预约可跨时段(13:30-15:30)。用户需要知道“14:00-16:00 是否完全空闲”。
→ 解决:在 DAO 层加is_fully_free字段计算:
CASE WHEN r.id IS NULL THEN 'free' WHEN r.start_time <= '14:00:00' AND r.end_time >= '16:00:00' THEN 'fully_occupied' ELSE 'partially_occupied' END as occupancy_status前端用不同颜色标识:绿色(free)、红色(fully_occupied)、黄色(partially_occupied)。
6. 从部署到护航:上线后必做的五件事(含数据库备份脚本、慢查询日志分析、Tomcat 内存调优)
6.1 数据库自动备份:用mysqldump+ Windows 任务计划(Linux 用 crontab)
系统上线后,预约数据就是核心资产。不能靠人工导出。Windows 下建批处理backup_lab.bat:
@echo off set BACKUP_DIR=D:\lab_backup set DATE_STR=%date:~0,4%%date:~5,2%%date:~8,2% set TIME_STR=%time:~0,2%%time:~3,2%%time:~6,2% set TIME_STR=%TIME_STR: =0% # 修正小时前导空格 set FILE_NAME=lab_system_%DATE_STR%_%TIME_STR%.sql mysqldump -u root -pYourPassword lab_system --single-transaction --routines --triggers > "%BACKUP_DIR%\%FILE_NAME%" :: 删除7天前的备份 forfiles /p "%BACKUP_DIR%" /s /d -7 /c "cmd /c del @path"关键参数:
--single-transaction保证备份时不影响线上预约;--routines备份存储过程(虽本系统没用,但留扩展位);forfiles自动清理旧备份。
6.2 慢查询日志:定位“高级搜索卡顿”的真实元凶
某次上线后,管理员反馈“高级搜索”响应超 5 秒。开启 MySQL 慢查询日志:
SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 2; -- 超2秒记为慢查询 SET GLOBAL log_output = 'TABLE'; -- 记到 mysql.slow_log 表,方便查查日志发现罪魁祸首:
# Query_time: 4.231245 Lock_time: 0.000123 Rows_sent: 1200 Rows_examined: 24500 SELECT r.*, u.username, u.role, l.name as lab_name FROM reservations r JOIN users u ON r.user_id = u.id JOIN labs l ON r.lab_name = l.name WHERE 1=1 AND l.name LIKE '%FPGA%' AND r.start_time >= '2024-01-01' AND r.end_time <= '2024-12-31' AND r.status = 'confirmed';Rows_examined: 24500说明全表扫描。优化方案:
- 在
reservations表加联合索引:ALTER TABLE reservations ADD INDEX idx_search (lab_name, start_time, end_time, status); - 在
labs表name字段加索引:ALTER TABLE labs ADD INDEX idx_name (name);
加完索引后,Rows_examined降至 120,查询时间 0.18 秒。
6.3 Tomcat 内存调优:为什么-Xms512m -Xmx1024m是底线
默认 Tomcat 启动内存(-Xms256m -Xmx512m)在高并发下必然 OOM。根据实测,32 个教师账号并发导入名册(每批次 200 行),堆内存峰值达 780MB。必须调整bin/catalina.sh(Linux)或bin/catalina.bat(Windows):
:: catalina.bat 末尾添加(Windows) set JAVA_OPTS=%JAVA_OPTS% -Xms512m -Xmx1024m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -XX:+UseG1GC参数说明:
-Xms512m设初始堆为 512MB,避免频繁扩容;-XX:MetaspaceSize控制类元数据区,防止java.lang.OutOfMemoryError: Metaspace;-XX:+UseG1GC启用 G1 垃圾回收器,降低 STW 时间。
6.4 文件上传安全加固:不只是改maxPostSize
教师上传的实验资料可能含恶意脚本。除 Tomcat 配置外,必须在FileUploadServlet中加三重过滤:
// 1. 文件后缀白名单 String allowedExt = "pdf,ppt,pptx,doc,docx,xls,xlsx,zip,rar"; String ext = FilenameUtils.getExtension(fileItem.getName()).toLowerCase(); if (!allowedExt.contains(ext)) { throw new RuntimeException("不支持的文件类型:" + ext); } // 2. 文件头魔数校验(防 .pdf.txt 伪装) byte[] header = new byte[4]; fileItem.getInputStream().read(header); String hexHeader = Hex.encodeHexString(header); if (!"25504446".equals(hexHeader) && !"504b0304".equals(hexHeader)) { // PDF/ZIP throw new RuntimeException("文件头校验失败,疑似恶意文件"); } // 3. 文件大小二次校验(防绕过 Tomcat 配置) if (fileItem.getSize() > 10 * 1024 * 1024) { // 10MB throw new RuntimeException("文件超过10MB限制"); }6.5 最后一道防线:log4j2.xml配置与错误邮件告警
系统用 Log4j2 记录关键操作。src/main/resources/log4j2.xml配置了异步日志和邮件告警:
<Appenders> <SMTP name="Mail" subject="LAB_SYSTEM ERROR ALERT" to="admin@university.edu.cn" from="lab-system@university.edu.cn" smtpHost="smtp.university.edu.cn" smtpPort="587" username="lab-system" password="AppPassword"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> <Filters> <ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/> </Filters> </SMTP> </Appenders>效果:当
LoginServlet捕获到连续 5 次密码错误,或ReservationDAO抛出SQLException,Log4j2 会立即发邮件到管理员邮箱,标题带[ERROR]前缀,内容含堆栈——这比等用户投诉快 10 分钟。
从那以后我每次上线新系统,都强制走一遍这五件事:备份脚本跑通、慢查询日志开 24 小时、Tomcat 内存重配、上传文件测 3 种边界(超大、伪装、空文件)、邮件告警试发。不是怕出问题,是怕出问题时找不到根因。这套实验室预约系统,它不炫技,但每一步都踩在高校信息化的真实地面上——有冗余,有妥协,有为未来留的缝,也有为今天守的线。希望帮到你。
本文还有配套的精品资源,点击获取