☰
JSP会议室预约系统开发实战:Servlet三层架构与并发冲突解决
2026/10/7 18:10:19 网站建设 项目流程

简介:这是一套面向企业或组织内部会议室资源管理的JSP加Servlet经典Web项目,主要解决会议预订冲突、部门与员工管理、公告发布等日常办公问题。项目包含管理员与员工两种角色,覆盖登录认证、会议室增删改查、预约与取消预约、权限控制等完整业务流程,适合Java Web初学者或毕业设计学习者参考。压缩包共有744个文件,体积约5.01MB,以gif、js、html、css等前端资源为主,同时包含20个jsp页面、4个java源文件、15个jar依赖库以及sql数据库脚本,基本可还原系统运行环境。已有1657人学习下载。通过阅读源码和数据库脚本,可以掌握JSP与Servlet协作方式、JavaBean数据封装、数据库增删改查操作、会话管理等技术要点;目录结构还包含fckeditor富文本编辑器等组件,便于理解常见Java Web项目组织方式,是一份适合边看边练的实用参考。

1. JSP会议室预约系统是什么:解决的不只是“订到会议室”

周五下午的会议室登记本上,两行“14:00-16:00”并排写在一起,没人说得清谁先来谁后到,管理员只好挨个打电话确认。JSP会议室预约系统就是为了把这个“靠喊、靠贴条、靠翻本子”的过程搬上浏览器:用 JSP + Servlet 做一套网页,让员工自己看空闲时段、在线提交预约,管理员在后台确认或驳回。它解决三件事——谁在什么时间用哪个会议室、时间冲突时由谁裁决、以及打开浏览器就能查看全局,不用装任何客户端。这套技术路线年岁不短但依旧被广泛用在课程设计、毕业论文和公司内部轻量管理工具上,核心是因为它结构直白、上手门槛低,一个 Java 基础扎实的人两周就能交付可用版本。

2. 先定技术选型与数据模型:JSP+Servlet 三层架构为什么还值得做

2.1 架构选型理由:JSP+Servlet+JavaBean 三层,适合轻量交付

我见过不少把“会议室预约”直接做成一个超大 JSP 页面的项目,页面里连数据库连接都写在<%!声明块里,几百行 Java 代码和 HTML 混在一起,改一个颜色都可能牵动 SQL。这种写法不能说不能用,但后续维护基本是灾难。更常见、也更值得照做的方案是 JSP + Servlet + JavaBean 三层结构:JSP 只负责展示,Servlet 接收请求、调用业务、控制页面跳转,JavaBean(通常拆成 DAO 和 Service)负责数据库操作和业务规则。

选这套而不是直接上 Spring Boot,理由很实际:如果是课程设计或毕业论文,评审重点往往就是考察对 JSP/Servlet 本身的理解,Spring Boot 会掩盖掉太多底层细节;如果是公司内部小工具,项目只有几个人用,不需要依赖注入、AOP 这些重机制,原生 Servlet 反而启动快、出问题好排查。Tomcat 自带 JSP 引擎,把 war 包丢进 webapps 就能跑,连配置文件的量都比 Spring 少一个数量级。

各层的协作关系一句话就能讲清:浏览器请求 → Servlet 控制器解析参数 → Service 处理业务(比如判断时间段冲突)→ DAO 操作 MySQL → 结果存到 request 或 session 作用域 → JSP 用 JSTL 标签渲染后响应。层与层之间通过 JavaBean 对象传递数据,不直接互相 new 依赖,后续把 DAO 换成 MyBatis 也不会伤筋动骨。

2.2 数据库设计:三张核心表与状态机,从源头拦住时间冲突

会议室预约系统的数据模型比想象中简单,核心就三张表:会议室表、预约记录表、用户表。会议室表存名称、容量、设备信息和状态;用户表存登录账号、密码和角色(普通用户/管理员);预约记录表则是整个系统的业务核心,它不能只记“谁借了哪间”,还得记时间段、用途和状态。

预约状态建议做成一个可流转的状态机,而不是只用一个status数字字段瞎填。我常用的状态集合是:待确认、已确认、已取消、已驳回。用户提交后默认“待确认”,管理员审核通过后变“已确认”,用户自己改主意可以“已取消”,管理员拒绝则“已驳回”。这套状态设计的好处是:列表页可以按状态过滤,统计报表也能直接分组计数,比用删除操作表示“不约了”要干净得多——删除会连同审批记录一起消失,出问题连审计线索都没有。

时间字段有一个容易忽略的选型点:开始时间和结束时间用TIME类型还是DATETIME?DATETIME只有在跨天预约(比如晚上 22:00 约到次日 02:00)时才真正必要,多数会议室系统按半天或小时为单位预约,用TIME更直观,也更方便前台做时间段选择器。日期单独用DATE字段存,这样查询某一天的空闲会议室时,WHERE meet_date = ?的索引效率最高。如果预约单位是固定时段(比如 9:00-11:00、14:00-16:00),建议把时间段也做成枚举或模板表,能省掉一大部分前端校验的麻烦。

2.3 建表 SQL:带注释的初始化脚本可直接粘贴执行

把三张表的初始化脚本直接贴出来,MySQL 5.7 和 MySQL 8.0 都能跑,字符集统一用utf8mb4,避免中文乱码。

-- 用户表:普通员工和管理员共用 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '存MD5摘要,不要存明文', real_name VARCHAR(50) COMMENT '真实姓名,列表页展示用', role TINYINT NOT NULL DEFAULT 1 COMMENT '1=普通用户,2=管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 会议室表:equipment 存设备清单的JSON字符串 CREATE TABLE meeting_room ( id INT PRIMARY KEY AUTO_INCREMENT, room_name VARCHAR(100) NOT NULL COMMENT '如:三楼小会议室', capacity INT NOT NULL DEFAULT 0 COMMENT '容纳人数', equipment VARCHAR(500) DEFAULT NULL COMMENT '投影仪/白板/视频终端等', status TINYINT NOT NULL DEFAULT 1 COMMENT '1=启用,0=停用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约记录表:核心业务表,状态字段务必加索引 CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, room_id INT NOT NULL COMMENT '关联 meeting_room.id', user_id INT NOT NULL COMMENT '关联 sys_user.id', meet_date DATE NOT NULL COMMENT '预约日期', start_time TIME NOT NULL COMMENT '开始时间,如 14:00', end_time TIME NOT NULL COMMENT '结束时间,如 16:00', title VARCHAR(200) NOT NULL COMMENT '会议主题', status TINYINT NOT NULL DEFAULT 0 COMMENT '0=待确认 1=已确认 2=已取消 3=已驳回', remark VARCHAR(500) DEFAULT NULL COMMENT '备注或驳回原因', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_room_date (room_id, meet_date), INDEX idx_user (user_id), INDEX idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 SQL 里有几个细节值得说明。sys_user.password字段长度定成 64 是因为 MD5 摘要固定 32 位,加盐后可能到 64 位,预留够用;role用 TINYINT 而不是字符串枚举,是为了后续加角色(比如“前台管理员”)时不改表结构。reservation表里idx_room_date这个联合索引直接服务于“某天某会议室有哪些预约”的查询,也是冲突校验时最频繁命中的路径。status单独建索引是因为管理员后台最常用的过滤条件就是按状态筛选待办。

注意:start_time和end_time不要考虑跨天场景,如果真有跨天需求,把meet_date拆成start_date和end_date两个字段更稳妥,别用 DATETIME 硬扛。

3. 把预约主流程跑通:从登录到提交预约的完整代码路径

3.1 登录与 Session:JSP 页面 + Servlet 控制器的标准写法

预约系统第一个要写通的就是登录,否则后面所有页面都没有用户身份可用。登录页本身是个纯静态的 HTML 表单,提交到LoginServlet,Servlet 里查数据库比对密码,成功后把用户对象放进 session,失败则带错误提示跳回登录页。

<%-- login.jsp,注意 pageEncoding 必须和表单提交编码一致 --%> <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <!DOCTYPE html> <html> <head> <title>会议室预约系统登录</title> </head> <body> <form action="login" method="post"> <label>账号:<input type="text" name="username" required></label> <label>密码:<input type="password" name="password" required></label> <button type="submit">登录</button> <p style="color:red;">${errorMsg}</p> </form> </body> </html>

对应的LoginServlet是系统里所有 Servlet 的标准模板,包括设置编码、读取参数、调 DAO、跳转这一步。

// LoginServlet.java @WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = MD5Util.md5(request.getParameter("password")); UserDao dao = new UserDao(); User user = dao.findByUsernameAndPassword(username, password); if (user == null) { request.setAttribute("errorMsg", "账号或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } // 登录成功,把用户对象放进 session request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/roomList"); } }

参数说明:request.setCharacterEncoding("UTF-8")放在读取任何参数之前,只对 POST 请求生效,这是 JSP 中文处理最容易漏的第一环;password入库前统一走MD5Util做摘要,不要明文存库,虽然这是演示系统但血泪教训告诉我,明文密码一旦被翻出是可以直接丢工作的;response.sendRedirect走的是第二次请求,避免刷新页面时重复提交表单;request.getContextPath()拼接上下文路径,部署到 Tomcat 后 webapp 名称可能变化,写死路径会 404。

3.2 会议室列表与按日期筛选:JSP 里用 JSTL 渲染,不写 Scriptlet

登录后第一个页面就是会议室列表。按日期筛选是必备功能——用户看到的是“今天能约哪几间”,而不是全部堆在一起。

列表页推荐用 JSTL 的c:forEach渲染,不要在<%标签里写循环。JSTL 标签能保持页面结构清晰,也更贴近现代前后端分离的思路。DAO 层返回List<Room>,Servlet 把它塞进request.setAttribute("roomList", list),JSP 只用一行标签就能遍历。

<%-- roomList.jsp --%> <%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table border="1" cellpadding="8"> <tr> <th>会议室</th> <th>容纳人数</th> <th>设备</th> <th>状态</th> <th>操作</th> </tr> <c:forEach items="${roomList}" var="room"> <tr> <td>${room.roomName}</td> <td>${room.capacity}人</td> <td>${room.equipment}</td> <td> <c:choose> <c:when test="${room.status == 1}">可用</c:when> <c:otherwise>停用</c:otherwise> </c:choose> </td> <td><a href="${ctx}/roomDetail?roomId=${room.id}">查看时段</a></td> </tr> </c:forEach> </table>

说明几个使用细节:页面顶部需要加<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,否则c:forEach不会被解析,浏览器直接输出标签原文;${ctx}是在请求最开始用<c:set var="ctx" value="${pageContext.request.contextPath}"/>定义的,整个页面所有链接都用它做前缀,避免硬编码路径;room.status是 JavaBean 的属性,JSP 的 EL 表达式会自动调用getStatus()方法,不需要手动写括号。列表接口的排序按会议室容量倒序排,同一会议室状态停用的排在最后,这个排序逻辑在 SQL 的ORDER BY里完成,不要在 Java 里做二次排序。

3.3 预约提交与冲突校验:用“先查重再插入 + 事务”保证不超售

会议室预约最核心的逻辑是:用户选了一个时间段,系统必须确认这段时间没人用。最简单也最容易翻车的写法是“先查有没有冲突,没有就插入”——两个用户同时点提交,两个请求都查到了空档,然后都插入成功,会议室就超售了。

正确做法是把查询和插入放进同一个事务,并且对预约涉及的行加锁。项目里用的方案是:在事务里执行一个带FOR UPDATE的查询,锁定该会议室当天已有预约的记录,再判断新时间段是否重叠,没有重叠才插入。

// ReservationService.java 核心方法 public boolean createReservation(Reservation res) throws SQLException { Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 1. 锁定该会议室当天的所有预约记录 String lockSql = "SELECT id FROM reservation WHERE room_id=? AND meet_date=? FOR UPDATE"; PreparedStatement ps = conn.prepareStatement(lockSql); ps.setInt(1, res.getRoomId()); ps.setDate(2, res.getMeetDate()); ResultSet rs = ps.executeQuery(); // 有行锁,其他事务必须等待 // 2. 在锁定结果集的基础上做重叠判断(串行化后不会查错) String checkSql = "SELECT COUNT(*) FROM reservation WHERE room_id=? AND meet_date=? " + "AND status IN (0,1) " + "AND start_time < ? AND end_time > ?"; PreparedStatement ps2 = conn.prepareStatement(checkSql); ps2.setInt(1, res.getRoomId()); ps2.setDate(2, res.getMeetDate()); ps2.setTime(3, res.getEndTime()); ps2.setTime(4, res.getStartTime()); ResultSet rs2 = ps2.executeQuery(); rs2.next(); if (rs2.getInt(1) > 0) { conn.rollback(); return false; // 时间段冲突 } // 3. 无冲突则插入,状态为待确认 String insertSql = "INSERT INTO reservation (room_id,user_id,meet_date,start_time,end_time,title,status) " + "VALUES (?,?,?,?,?,?,0)"; PreparedStatement ps3 = conn.prepareStatement(insertSql); // ... 设置参数 ... ps3.executeUpdate(); conn.commit(); return true; } finally { conn.setAutoCommit(true); if (conn != null) conn.close(); } }

这段代码最关键的是第 1 步的FOR UPDATE。它把“该会议室当天的所有预约记录行”锁住,第二个事务的执行会阻塞在同一个查询上,直到第一个事务提交或回滚,从而保证第 2 步的冲突判断是一个接一个串行执行的,不会出现两个事务同时看到“空闲”。注意第 2 步查重叠的 SQL 条件:start_time < ? AND end_time > ?,参数传的是新预约的结束时间和开始时间。这个写法能覆盖所有重叠情况——新时间段被已有时间段完全包含、部分重叠、完全包含已有时间段,都逃不过这个条件。只判断start_time >= ? OR end_time <= ?是错的,那只能挡住恰好完全重合的情况。

4. 管理功能怎么落:审批流转与会议室维护

4.1 审批与驳回:把状态机做成代码可见的分支

有了管理员角色,预约系统才能形成闭环。普通用户提交后状态是“待确认”,管理员在待办列表里看到提交记录,选择通过或驳回。逻辑本身不复杂,但状态的可达性一定要写清楚:不是每个状态都能跳转成任意状态。比如“已驳回”的记录不能直接被管理员改成“已确认”;用户取消只发生在“待确认”或“已确认”状态;“已取消”不能恢复成“待确认”。

为了不让状态越迁失控,我在代码里写成显式分支,而不是简单的UPDATE ... SET status=?:

// AdminApproveServlet.java 核心逻辑 String action = request.getParameter("action"); // "approve" 或 "reject" int resId = Integer.parseInt(request.getParameter("id")); // 只允许操作“待确认”状态的记录,防止重复审批 String sql = "UPDATE reservation SET status=?, remark=? WHERE id=? AND status=0"; PreparedStatement ps = conn.prepareStatement(sql); ps.setInt(1, "approve".equals(action) ? 1 : 3); ps.setString(2, request.getParameter("remark")); ps.setInt(3, resId); int rows = ps.executeUpdate(); if (rows == 0) { // 说明该记录已被别的管理员处理过,提示“记录已变更” }

参数分析:WHERE id=? AND status=0这个条件是这串代码的灵魂,它用数据库来保证只有“待确认”的记录能被审批,重复点击提交按钮也不会产生两条审批结果。rows == 0时要给用户明确提示“这条记录已经被处理过了”,而不是默默失败。remark字段在驳回时填原因,在通过时可以为空,列表页把备注展示在会议详情里,让其他同事知道为什么被拒。这套显式状态机的写法,比用大量if判断散落在各种 Servlet 里要可靠得多——状态流转只发生在数据库里受约束的地方。

4.2 会议室管理与设备字段:后台 CRUD 的通用套路

后台维护会议室本质是单表 CRUD,但有一个容易被忽略的点:设备字段怎么设计。很多系统把equipment设计成逗号分隔的字符串,“投影仪,白板,电视会议终端”,然后前端用split(",")渲染成标签。这种做法够用但不耐查——“有哪些会议室支持电视机”这类检索做不了;更规范的做法是设备字段存 JSON 数组,Java 端用 Jackson 序列化和反序列化。

// Room.java 中 equipment 字段 private String equipment; // 数据库存 JSON,如 ["投影仪","白板"] private List<String> equipmentList; // 页面展示用 public List<String> getEquipmentList() { if (this.equipment == null || this.equipment.isEmpty()) { return new ArrayList<>(); } try { return new ObjectMapper().readValue(this.equipment, new TypeReference<List<String>>() {}); } catch (JsonProcessingException e) { return new ArrayList<>(); } }

getEquipmentList()方法放在 JavaBean 里而不是 JSP 页面里处理,是为了让视图层保持干净,EL 表达式直接访问${room.equipmentList}就能拿到 List。这里顺带讲一个常见的会议室图片坐标定位需求——有些预约系统会在会议室平面图上标出每个座位的位置。做法一般是在会议室维护页面里,上传平面图后让管理员用鼠标点击设定坐标点,把坐标以相对比例存到 JSON 字段:{"name":"E区","x":0.42,"y":0.63},展示时按图片实际尺寸乘比例定位。用相对坐标而不是绝对像素,是避免不同分辨率的屏幕上位置漂移的常用手段。

4.3 用户取消预约:给普通用户一个后悔药

取消预约功能看似简单,很多系统却做成了“直接删记录”,这其实是错的——管理员已经在审批记录里看过这条预约,删除后审批历史就断了。更合适的做法是把状态改成“已取消”,保留现场。

// CancelReservationServlet.java String sql = "UPDATE reservation SET status=2 WHERE id=? AND user_id=? AND status IN (0,1)";

这里AND user_id=?是权限校验,用户不能取消别人的预约;status IN (0,1)限定了只有“待确认”和“已确认”才能取消,已经驳回的不能再操作。如果用户在会议室已确认后临时改主意,取消的同时可以给管理员发一条站内通知(系统里做一个小型消息表即可),避免管理员到了现场才发现会议取消。项目里是给sys_user表加一个need_notice字段,取消时往notice表插一条记录,管理员的待办列表上红点提示。

5. 避坑与排查:JSP 会议室预约系统最常见的五个翻车点

5.1 中文乱码:页面显示“???”或写入数据库全是问号

这是 JSP 项目里出现频率最高的玄学问题,一共三处编码都得管住,少一处就乱。第一处是 JSP 文件本身,pageEncoding="UTF-8"必须要写,它告诉 JSP 引擎用什么编码读取源文件;第二处是 Servlet 接收请求参数,request.setCharacterEncoding("UTF-8"),只在 POST 请求且必须放在读取参数之前才有效;第三处是 JDBC URL,连接串里必须有characterEncoding=utf-8,否则 MySQL 连接层用默认 latin1 传中文,入库后必乱。

除此之外还有一个 GET 请求的坑:Tomcat 8 及更高版本对 GET 请求参数的默认编码是 UTF-8,但如果你用的是 Tomcat 7 或者中间件改过配置,链接上的中文参数就会乱。处理方式是在提交路径里用encodeURI()转码,或者在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。排查顺序建议是:页面输出了吗→数据库存了吗→URL 里传了吗,逐层定位断点。

5.2 JDBC 驱动加载失败:ClassNotFoundException 与版本失配

新装 MySQL 8 的机器上跑老代码,最容易出现java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。MySQL 官方驱动从 8.0 开始,驱动类更名为com.mysql.cj.jdbc.Driver,同时 JDBC URL 需要额外加serverTimezone=Asia/Shanghai,否则连接时报The server time zone value 'CST'错误。我自己踩过最冤的一次是把mysql-connector-java-8.0.33.jar放到了 Tomcat 的lib目录而不是项目的WEB-INF/lib,导致本地开发没事、部署到服务器后启动报 NoClassDefFoundError。规范做法是驱动 jar 跟着项目走,随 war 一起部署,不要依赖 Tomcat 公共目录。

5.3 onclick 传参会丢参数:字符串拼接引号打架

JSP 页面里写<a href="roomDetail?roomId=${room.id}" onclick="return confirm('确定查看?')">一般没事,但一旦参数是字符串且内容里含单引号或日期时间格式,拼接就会翻车。比如预约详情页的回调函数:

<a href="javascript:void(0)" onclick="cancelReserve(${res.id}, '${res.startTime}')">取消</a>

${res.startTime}渲染出来是14:00:00,单引号没问题,但如果某个字段的值是"O'Brien"这类含单引号的字符串,JavaScript 直接语法报错。更稳妥的做法是不要拼接值进函数参数,改用><a href="javascript:void(0)">response.setContentType("application/json;charset=UTF-8"); PrintWriter out = response.getWriter(); out.write(jsonString);

这个charset=UTF-8不能省,否则浏览器按默认编码解析 JSON,中文又成乱码。.getWriter()和.getOutputStream()不能同时调用,一次请求只能选一个,这是 Servlet 规范的限制。前端页面里注意encodeURIComponent处理日期参数:

fetch(ctx + '/roomAvailability?date=' + encodeURIComponent(dateValue)) .then(res => res.json()) .then(data => renderRoomTable(data));

6.2 分页查询与 Filter 登录拦截

当预约记录积累到上千条时,后台列表必须做分页。常规实现是 LIMIT 加偏移量,pageSize固定为 10,currentPage从请求参数取:

int pageSize = 10; int currentPage = Integer.parseInt(request.getParameter("page") == null ? "1" : request.getParameter("page")); String sql = "SELECT * FROM reservation ORDER BY create_time DESC LIMIT ?,?"; ps.setInt(1, (currentPage - 1) * pageSize); ps.setInt(2, pageSize);

分页的关键不是那两句 SQL,而是总页数计算。SELECT COUNT(*)单独发一次查询,总页数是(totalCount + pageSize - 1) / pageSize,前端渲染页码链接时循环输出数字。如果数据量大,COUNT(*)也别在大表上跑全量,预约表里按meet_date >= CURDATE()过滤掉历史记录通常就够用。

登录拦截用 Filter 实现。Filter 的注册有两种方式:@WebFilter("/*")注解或web.xml配置。核心逻辑是检查 session 里有没有loginUser,没有就重定向到登录页。

@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; HttpSession session = request.getSession(false); // 不创建新 session String uri = request.getRequestURI(); boolean isStaticResource = uri.endsWith(".css") || uri.endsWith(".js") || uri.endsWith(".png") || uri.endsWith(".jpg"); boolean isLoginPage = uri.endsWith("/login.jsp") || uri.endsWith("/login"); if (isStaticResource || isLoginPage || (session != null && session.getAttribute("loginUser") != null)) { chain.doFilter(req, resp); } else { response.sendRedirect(request.getContextPath() + "/login.jsp"); } } }

这段 Filter 有两个容易忽略的细节:getSession(false)表示如果当前没有 session 就返回 null,而不是强制创建一个空 session;静态资源必须放行,很多系统上线后图片加载不出来,排查一圈发现是 Filter 把图片请求也拦了。管理员权限的控制可以在 Filter 基础上加角色判断,但建议只对/admin/*路径做角色过滤,别对全站生效,否则改一次用户角色就要动 Filter 代码。

最后分享一个部署组合的教训:JDK 8 + Tomcat 9 + MySQL 5.7 是我用的最顺手的组合,驱动类用com.mysql.jdbc.Driver就能跑;如果数据库升级到 MySQL 8,记得换驱动类并加serverTimezone参数。JSP 项目没有 Spring Boot 那种“自动配置”的魔法,出问题大多能在 Tomcat 日志里直接看到根源,这是它作为教学项目最大的价值——每个字节的编码、每个类的加载、每个 sql 的执行都在掌控之中。希望这篇笔记帮你在做会议室预约系统的路上少走几个弯,把时间省下来用在真正的业务逻辑上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询