ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于JSP+Servlet+MySQL的会议室管理系统设计与实现

基于JSP+Servlet+MySQL的会议室管理系统设计与实现 接到一个用传统技术栈——javaweb体系下的jspservlet后端搭配mysql数据库——做会议室管理系统的需求时我第一反应是有点恍惚。毕竟现在Spring Boot、Vue一类的框架满天飞JSPServlet似乎已经被很多教程归为“毕业设计专用技术栈”。但真正动手做完一遍我反而觉得这套组合被低估了它逼着你把HTTP请求、Session生命周期、JDBC、数据库设计这些最底层的东西彻底搞明白而这些恰恰是很多直接上手Spring Boot的人最容易忽略的短板。这篇文章就围绕这个基于javaweb和mysql的jspservlet会议室管理系统来写。我会从数据库设计讲到Servlet请求流转再讲到JSP页面渲染最后把部署和踩坑经验一并整理了。不管你是正在做课程设计、毕业设计还是单纯想补一补传统JavaWeb的基础这篇都能直接照着做。1. 为什么都202X年了我还要手写一套JSPServlet的会议室系统1.1 这套技术栈没有过时而是回归了本质Spring Boot之所以受欢迎是因为它把很多烦人的配置和底层逻辑都封装好了。但封装也意味着学习者在遇到问题时往往只能在框架层打转真正问他“一个HTTP请求从浏览器到服务器再回到浏览器中间到底发生了什么”他说不清楚。JSPServlet这套旧组合恰好能把这个过程掰开揉碎浏览器发起请求Tomcat收到后交给对应的Servlet。Servlet调用DAO层访问MySQL处理业务逻辑。Servlet把结果转发或重定向到JSP页面。JSP在服务器端被翻译成Servlet动态拼出HTML返回给浏览器。整个链路没有任何隐藏魔法。会议室管理系统的业务量不大用这套技术栈完全够跑而且能让你亲手体会到每个环节的职责。1.2 项目到底要做什么功能边界和角色划分做系统之前先把边界划清楚否则越做越散。一个标准的会议室管理系统核心就是两块会议室资源和预约流程。我落地时定了两个角色普通用户登录、注册、查看会议室列表、发起预约申请、查看和取消自己的预约、查看审批结果。管理员在普通用户功能之外能维护会议室信息增删改查、审核预约通过或驳回、查看所有预约记录。不需要做花哨的报表和统计把预约状态流转做严谨就已经超过很多课程设计作品了。功能多了反而容易暴露代码逻辑漏洞比如权限绕过、时间冲突没校验。1.3 整体架构MVC在传统项目里怎么落地的这个项目的架构可以在脑子里建立这样一个模型层次技术载体职责视图层JSP、HTML、CSS、JavaScript展示数据、收集用户输入控制层Servlet接收请求、调用业务对象、决定跳转模型层JavaBean、DAO封装数据、访问MySQL数据库数据库MySQL持久化存储用户、会议室、预约信息我习惯把DAO单独抽一层不直接在Servlet里写JDBC代码。这样Servlet只负责“对接前端请求和后台业务”数据库操作集中在几个类里后续好维护答辩问到分层设计时也讲得清楚。2. 数据库设计四张核心表撑起整个会议预约流程2.1 表结构设计用户表、会议室表、预约表数据库设计是整个系统的地基。我实际使用的四张表如下第四张是会话记录表也可以不用用Session即可但为了学生答辩能多讲点东西我把操作日志表也建了下面先给三张核心表。CREATE TABLE user ( id INT(11) NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 密码建议加密存储, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, role TINYINT(4) NOT NULL DEFAULT 1 COMMENT 1-普通用户 2-管理员, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEINNODB DEFAULT CHARSETutf8mb4;CREATE TABLE meeting_room ( id INT(11) NOT NULL AUTO_INCREMENT, room_name VARCHAR(100) NOT NULL, location VARCHAR(200) DEFAULT NULL, capacity INT(11) DEFAULT 0 COMMENT 容纳人数, equipment VARCHAR(500) DEFAULT NULL COMMENT 设备说明投影仪/白板等, status TINYINT(4) NOT NULL DEFAULT 1 COMMENT 1-可用 0-停用, PRIMARY KEY (id) ) ENGINEINNODB DEFAULT CHARSETutf8mb4;CREATE TABLE reservation ( id INT(11) NOT NULL AUTO_INCREMENT, room_id INT(11) NOT NULL, user_id INT(11) NOT NULL, title VARCHAR(100) NOT NULL DEFAULT COMMENT 会议主题, reserve_date DATE NOT NULL COMMENT 预约日期, start_time TIME NOT NULL, end_time TIME NOT NULL, status TINYINT(4) NOT NULL DEFAULT 0 COMMENT 状态0-待审核 1-已通过 2-已驳回 3-已取消, reason VARCHAR(255) DEFAULT NULL COMMENT 驳回或取消原因, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME DEFAULT NULL, audit_user_id INT(11) DEFAULT NULL, PRIMARY KEY (id), KEY idx_room_date (room_id, reserve_date) ) ENGINEINNODB DEFAULT CHARSETutf8mb4;设计细节上我特别强调了几点表名尽量避免使用保留字比如user在某些环境要加反引号如果你嫌麻烦可以改成sys_user。reservation表必须建联合索引(room_id, reserve_date)因为查询“某会议室某天有哪些预约”是最频繁的操作没索引的表数据一多就开始卡。2.2 时间冲突校验为什么不能只靠程序判断会议室预约最核心的校验是时间冲突。同一间会议室、同一天两条预约的时间段不能重叠。我见过很多人只在Java代码里写判断把预约列表中已通过的记录查出来再跟新提交的时间段比较大小。这在单机下勉强能用但存在一个很脏的隐患并发请求同时提交时两次查询都没有查到冲突结果都插入成功数据就错了。更可靠的做法是双保险第一层数据库层面用SQL查重叠区间。第二层程序里面再提示用户“该时段已被预约”。重叠判断SQL可以这么写SELECT COUNT(*) FROM reservation WHERE room_id ? AND status 1 AND reserve_date ? AND ((start_time ? AND end_time ?) OR (start_time ? AND end_time ?));注意这里的时间比较逻辑新预约是[a, b)已存在预约是[c, d)只要c b and a d就说明有重叠。上面的SQL把两种传入方向都覆盖了。2.3 初始化数据与默认值陷阱建表之后要插入初始化数据。管理员账号、默认会议室都是刚需INSERT INTO sys_user (username, password, real_name, role) VALUES (admin, 202cb962ac59075b964b07152d234b70, 系统管理员, 2);密码字段我给出的是纯MD5的示例202cb962ac59075b964b07152d234b70对应明文123。实际项目中建议加盐或者用BCrypt课程设计阶段至少别把明文存进去。这里提一个MySQL 8.0的坑大多数MySQL版本里TIME和DATETIME类型字段设置默认值很麻烦比如start_time TIME DEFAULT 09:00在低版本直接报错。所以我的表设计里时间字段都没有用默认值而是靠Java代码在插入时填好。2.4 MySQL连接层面的常见坑驱动类名、时区、socket错误搜索mysql安装教程的人特别多但我发现真正让新人卡住的往往不是安装而是连接。MySQL 5.x和8.x的连接参数差异很大下面这个表你保存好配置项MySQL 5.xMySQL 8.xJDBC驱动类com.mysql.jdbc.Drivercom.mysql.cj.jdbc.Driver连接URLjdbc:mysql://localhost:3306/meetingjdbc:mysql://localhost:3306/meeting?serverTimezoneAsia/ShanghaiuseSSLfalsecharacterEncodingutf8驱动JAR包mysql-connector-java-5.1.x.jarmysql-connector-java-8.0.x.jarMySQL8如果不写serverTimezoneAsia/Shanghai运行时会抛The server time zone value йʱ is unrecognized的异常。连接不上还有一种典型报错ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这个报错我在Linux服务器上遇到好几次。它根本原因不是你的Java代码有问题而是MySQL服务没有启动或者客户端指定的socket文件路径和MySQL实际生成的socket路径不一致。排查路径很简单服务端执行service mysql status确认mysql进程是否存活。socket路径执行mysql -uroot -p测试能否本地登录如果本地都登录不了说明服务确实没起来。如果你确定服务是启动的那就在连接时改host为127.0.0.1而不是localhost让JDBC走TCP而不是走socket文件。3. Servlet为核心的请求流转登录、预约、审批怎么组织代码3.1 登录与Session管理不要只做页面存个值登录是所有业务系统的入口。在用JSPServlet做登录时最容易被忽略的是登录态存哪里、怎么保证后续每个请求都需要验证。我的做法如下LoginServlet接收username和password。调用LoginDAO查询数据库比对密码用MD5后与库中比较。用户存在且密码正确就把user对象放入SessionHttpSession session request.getSession(); session.setAttribute(loginUser, user);返回重定向到首页而不是转发。对应地写一个LoginFilter实现JavaWeb里的Filter接口在doFilter方法中检查Sessionpublic void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); HttpServletResponse response (HttpServletResponse) resp; if (session null || session.getAttribute(loginUser) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(request, response); }有了这个Filter就不会再出现“直接在浏览器地址栏输入list.jsp就能绕过登录”的低级漏洞了。忘了聊Filter在web.xml里配置时要把login相关的Servlet路径排除掉否则登录页面本身也被拦截导致死循环。3.2 Servlet路径设计和请求处理组织传统的Servlet项目路径规范的两种方式我都在项目里用了。一种是改web.xml一种是直接用注解WebServlet(/reserve/add)我建议新项目直接用注解代码即文档少一个配置文件少一个坑。这个项目的Servlet可以这样划分Servlet路径职责LoginServlet/login登录LogoutServlet/logout退出登录RegisterServlet/register用户注册RoomListServlet/room/list会议室列表RoomEditServlet/room/edit管理员新增/编辑会议室ReserveAddServlet/reserve/add提交预约申请MyReserveServlet/reserve/my我的预约列表ApproveServlet/reserve/approve管理员处理预约申请CancelServlet/reserve/cancel用户取消预约核心思想是一个Servlet只处理一类资源操作不要写成一个万能Servlet传个参数区分事务。那样子代码膨胀到一个类五六百行排查问题时谁看谁崩溃。每个Servlet内部的标准处理模板protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); boolean success service.doReserve(request); if (success) { response.sendRedirect(request.getContextPath() /reserve/my); } else { request.setAttribute(errorMsg, 预约失败时间冲突或会议室已停用); request.getRequestDispatcher(/reserve/add.jsp).forward(request, response); } }通过这种模式你能清楚看到JSPServlet的MVC流转表单POST到ServletServlet调用Service/DAO层执行数据库操作失败则带着错误消息转发回JSP成功则重定向避免重复提交。3.3 审批状态机的设计状态字段驱动业务预约审批的难点不是表结构而是状态流转。我在代码里定义了一个ReservationStatus常量类public class ReservationStatus { public static final int PENDING 0; public static final int APPROVED 1; public static final int REJECTED 2; public static final int CANCELED 3; }流转规则如下用户提交预约状态为PENDING。管理员看到待审核列表可把状态置为APPROVED或REJECTED。用户只能对PENDING状态的预约执行取消置为CANCELED。状态一旦变成APPROVED用户不能直接取消必须联系管理员处理。这个设定可以防止“我已经批准了会议室也锁定了用户又取消导致会议室闲置”的尴尬场景。在SQL更新时一定要带上前置状态条件。比如管理员审核通过UPDATE reservation SET status 1, audit_time NOW(), audit_user_id ? WHERE id ? AND status 0;这里执行后如果返回影响行数为0说明这条预约已经被别人处理过了比如用户已取消不应该再操作成功。3.4 表单重复提交、数据回显和重定向传统Servlet项目有个经典问题用户点了一次提交结果页面刷新时又弹出“重新提交表单”然后重复插入了一条预约数据。解决方案就是上面提到的“POST/REDIRECT/GET”模式表单POST提交到Servlet处理。处理成功后Servlet不转发回JSP而是生成一个302重定向响应让浏览器重新发起GET请求到列表页。这时刷新页面浏览器只会刷新GET响应不会再次提交POST。如果处理失败需要带错误提示回显时才用forward转发到表单页这样用户填写的部分内容还在表单里。实际开发中我发现还有一种要用到的小技巧给表单加一个隐藏的随机token提交时校验也能防重复提交但对于课程项目POST/REDIRECT/GET这个习惯已经能防住90%的问题了。4. JSP页面的动态渲染与HTML页面的分工4.1 JSP本质就是一个Servlet但千万别把业务逻辑写进去很多新人对JSP有个误解既然JSP里能写Java代码是不是业务逻辑放JSP里也无所谓答案是否定的。JSP在运行时会被Tomcat翻译成一个Servlet再编译成class执行。所以你在JSP里写% if(user.getRole()2) { %这类逻辑最终执行的还是一个Servlet Java类。但把大量业务写进JSP会导致页面源代码混杂Java控制流前端同学完全没法维护。出错时没有编译期检查一堆运行时异常。违背MVC职责划分答辩老师一看就给差评。我的底线是JSP里只允许出现EL表达式和JSTL自定义标签不写原生Java代码。页面顶部可以做权限判断、遍历列表但所有的数据都应该由Servlet提前放到request域中。比如会议室未读列表循环% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % ... ul c:forEach items${roomList} varroom li${room.roomName} - 容纳${room.capacity}人 - ${room.location}/li /c:forEach /ul${room.roomName}这种写法就是EL表达式它会自动调用Room.roomName的getter方法比用%room.getRoomName()%清爽得多。4.2 预约表单页的开发细节预约表单是这个系统最重要的用户输入入口。JSP页面里必须把“可选会议室”和“日期”“开始时间”“结束时间”做一个组合联动。可选会议室从RoomListServlet查询所有状态为1的会议室通过JSTL循环生成下拉框。日期输入我直接用了input typedate这个HTML原生控件比自己拼字符串日历强多了。时间范围用两个input typetime再写一个JS校验startTime endTime不合格直接拦截提交减少后端无谓的请求。当然前端校验只是提升体验真正的合法性判断和冲突校验仍要在Servlet后端做。页面回显时有一个细节如果表单校验失败被转发回来request.getParameter(startTime)是可以直接取到刚提交的值的。在JSP里回填input typetime namestartTime value${param.startTime} required /${param.startTime}是EL表达式读取请求参数的快捷方式不用手动在Servlet里再次setAttribute。4.3 纯HTML页面在传统项目里的重要地位这个项目的技术栈里也有HTML的标签。很多人以为JSP项目的界面全用jsp文件其实静态HTML页面也扮演关键角色尤其是在混合开发模式下登录页可以用静态login.html做高颜值页面用户输入后通过fetch或jQuery的ajax把JSON数据POST到/loginServlet返回JSON字符串页面拿到结果再跳转。会议室管理后台的表格也可以用静态HTML页面拉取RoomListServlet返回的最新JSON数据来渲染减少整页刷新。在这个项目里我建议你至少把登录页写成静态HTML AJAX这种“前端动态渲染、后端返回JSON”的合作方式实际上是很多公司里旧项目升级换代的第一步。让项目展示的时候可以很自然地说一句“这里我做了一个前后端分离的过渡体验。”Servlet返回JSON也很简单不需要额外的json库手动拼也够用response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\success\: true, \msg\: \登录成功\});当然如果是更复杂的列表数据建议引入fastjson或者Jackson别手拼JSON字符串容易被引号转义搞到怀疑人生。5. 开发与调试从IDEA配置到WAR包部署的完整链路5.1 环境配置JDK、Tomcat、IDEA三者缺一不可开发JSPServlet项目环境配置环节就能劝退一批人。我的推荐组合组件版本建议备注JDK8 或 11不要轻易上JDK17老Tomcat版本不兼容Tomcat8.5 或 9.0对应Java EE规范支持Servlet 4.0IDEAUltimate版社区版对Web项目支持稍弱构建工具Maven或直接用IDEA的Artifacts打WAR如果你第一次在IDEA里跑JavaWeb项目记住一个核心操作需要在Run Configuration里添加一个Tomcat Server - Local然后在Deployment标签页添加war exploded工件Application context建议填/meeting_system。很多人卡在“代码能编译但启动后访问404”八成是IDEA里的Tomcat部署配置没有做对没把Web模块加到部署列表里。5.2 MySQL连接池从DriverManager到Druid的升级传统的DriverManager.getConnection()在会议室管理系统这种教学项目里常见但如果你熟悉真实工程实践应该认识一下连接池。实现方案我推荐阿里巴巴的Druid配置简单还有监控页面可以用。先引入依赖dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency然后在工具类里初始化连接池public class DBUtil { private static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setDriverClassName(com.mysql.cj.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/meeting_system?serverTimezoneAsia/ShanghaicharacterEncodingutf8useSSLfalse); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(5); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }用连接池的好处是每个请求来的时候不需要重新创建物理连接用完的连接直接还回池里性能提升非常明显。测试阶段你可以肉眼看到系统在并发请求下不卡顿了。5.3 传统JSP项目打包WAR并部署搜索引擎里“传统jsp项目打包war”这个热搜说明这个问题困扰了很多人。打包WAR部署是课程设计展示和答辩前的最后一道关卡。有两种打包方式方式一IDEA自带Artifacts → 选Web Application: Archive → For 项目名:war exploded → Build Artifacts生成war包。方式二Maven install前提是项目结构按Maven规范建好。打包之后把war文件放到$CATALINA_HOME/webapps目录下Tomcat安装目录启动Tomcat它会自动解压war包访问路径就是war包的名字。例如包名叫meeting.war访问地址是http://localhost:8080/meeting/。部署时遇到过最恶心的问题就是乱码。排查顺序很固定页面响应乱码检查JSP顶部% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8%。数据库存取乱码检查数据库连接URL里是否带characterEncodingutf8。控制台乱码检查IDEA的Help - Edit Custom VM Options加一行-Dfile.encodingUTF-8。请求参数乱码写一个CharacterEncodingFilter把所有请求和响应编码统一为UTF-8。WebFilter(/*) public class EncodingFilter implements Filter { private FilterConfig config; public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }这个Filter应该是所有JavaWeb项目的标配但很多教程居然都没讲。5.4 Servlet生命周期和JSP执行过程答辩常考考点面试和答辩必问的“Servlet生命周期”其实在这个项目里有机会真实体会到。Servlet生命周期分三步init()Servlet首次被请求时创建只执行一次。可以在初始化阶段加载全局配置。service()每次请求都会执行Tomcat根据请求方法分发到doGet或doPost。destroy()Web应用卸载或Tomcat关闭时执行释放资源。如果项目用了数据库连接池可以把连接池初始化动作放到一个专门的InitListener里用ServletContextListener在应用启动时初始化关闭时释放池。这也是评分卡上的加分项。JSP同样有生命周期。当浏览器第一次请求某个.jsp页面时Tomcat会把它翻译成一个Java文件再编译成class之后每次请求都直接执行这个Servlet实例。这也是为什么JSP第一次访问慢、后面快的原因。6. 安全与质量这些细节决定了项目能得多少分6.1 预编译SQL与SQL注入防护会议室管理系统的搜索和预约条件很多最容易写出拼接SQL的代码。比如新手为了省事可能这么写String sql SELECT * FROM meeting_room WHERE room_name LIKE % keyword %;这条SQL在单机测试时没问题但用户在搜索框输入一个 OR 11整张表数据就被带出来了这就是最典型的SQL注入。正确做法是使用PreparedStatementString sql SELECT * FROM meeting_room WHERE room_name LIKE ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, % keyword %);预编译的好处很直白SQL结构在编译时已固定用户输入永远只是参数不会参与SQL语法解析。这个习惯从JSPServlet阶段就要养好等你以后接触MyBatis时对#{}和${}的区别理解会通透得多。6.2 密码安全与敏感操作权限校验密码安全前面提过要加密存储这里再补一个实操方案用户注册时后端拿到明文密码做一次加盐MD5。存储格式简化为盐值 MD5(盐值 明文密码)登录时从库里取出盐值重新计算比对。另一个容易漏的是权限校验。很多课程项目只在页面上判断管理员身份比如显示“审核”按钮或者不显示但是坏人可以直接构造一个POST请求发给/reserve/approve接口。所以必须在Servlet内部再校验一次登录用户角色User loginUser (User) request.getSession().getAttribute(loginUser); if (loginUser null || loginUser.getRole() ! 2) { response.sendError(403); return; }记住这条黄金法则前端控制一切只是用户体验后端校验才是安全边界。6.3 连接池泄露与资源释放的细节使用连接池后有一个非常隐蔽的坑连接泄漏。getConnection()之后如果代码某处抛了异常且没有在finally里close()这个连接就永远不会回到池子里当池子里的连接耗尽时整个系统会卡死或报Connection is not available, request timed out。我的经验是所有获取连接的代码都用try-with-resources结构try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { ... } } catch (SQLException e) { e.printStackTrace(); }这样不管正常还是异常路径连接都能归还到连接池。就这一条能帮你避免掉生产环境里80%的数据库连接问题。6.4 关于接口返回结果再包装一下状态码与消息统一如果你打算在项目里做基于HTMLAJAX的交互那么接口返回统一格式这个习惯越早建立越好。我给自己约定的统一字段{ code: 200, msg: 操作成功, data: {...} }code表示业务状态码200成功500失败权限不足用403。这样前端用Ajax拿到响应后统一判断code 200决定是否跳转或刷新逻辑会简洁很多另外也为答辩时讲“代码可扩展性”准备了谈资。我在这个项目里写了一个简单的Result工具类用静态方法生成标准响应。代码本身不长但对整个工程代码风格的统一非常有帮助。这就是我从数据库、Servlet、JSP到部署调试全流程做这个会议室管理系统的核心经验。最后再分享一个我自己的实操心得如果你打算拿这个项目做毕业设计或课程作业千万不要临到答辩才把项目跑起来。把运行环境的配置步骤、数据库初始化脚本、访问地址写成一个README文档答辩现场照着步骤几分钟内就能启动演示印象分至少能提一档。没人喜欢看一个满头大汗在命令行里敲错命令的演示者稳才是硬道理。
返回列表