接到一个用传统技术栈——javaweb体系下的jsp+servlet,后端搭配mysql数据库——做会议室管理系统的需求时,我第一反应是有点恍惚。毕竟现在Spring Boot、Vue一类的框架满天飞,JSP+Servlet似乎已经被很多教程归为“毕业设计专用技术栈”。但真正动手做完一遍,我反而觉得这套组合被低估了:它逼着你把HTTP请求、Session生命周期、JDBC、数据库设计这些最底层的东西彻底搞明白,而这些恰恰是很多直接上手Spring Boot的人最容易忽略的短板。
这篇文章就围绕这个基于javaweb和mysql的jsp+servlet会议室管理系统来写。我会从数据库设计讲到Servlet请求流转,再讲到JSP页面渲染,最后把部署和踩坑经验一并整理了。不管你是正在做课程设计、毕业设计,还是单纯想补一补传统JavaWeb的基础,这篇都能直接照着做。
1. 为什么都202X年了,我还要手写一套JSP+Servlet的会议室系统
1.1 这套技术栈没有过时,而是回归了本质
Spring Boot之所以受欢迎,是因为它把很多烦人的配置和底层逻辑都封装好了。但封装也意味着学习者在遇到问题时,往往只能在框架层打转,真正问他“一个HTTP请求从浏览器到服务器,再回到浏览器,中间到底发生了什么”,他说不清楚。
JSP+Servlet这套旧组合恰好能把这个过程掰开揉碎:
- 浏览器发起请求,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`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;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`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;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`) ) ENGINE=INNODB DEFAULT CHARSET=utf8mb4;设计细节上,我特别强调了几点:
- 表名尽量避免使用保留字,比如
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.x | MySQL 8.x |
|---|---|---|
| JDBC驱动类 | com.mysql.jdbc.Driver | com.mysql.cj.jdbc.Driver |
| 连接URL | jdbc:mysql://localhost:3306/meeting | jdbc:mysql://localhost:3306/meeting?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8 |
| 驱动JAR包 | mysql-connector-java-5.1.x.jar | mysql-connector-java-8.0.x.jar |
MySQL8如果不写serverTimezone=Asia/Shanghai,运行时会抛The server time zone value '�й���ʱ��' is unrecognized的异常。连接不上还有一种典型报错:
ERROR 2002 (HY000): Can't 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管理:不要只做页面存个值
登录是所有业务系统的入口。在用JSP+Servlet做登录时,最容易被忽略的是:登录态存哪里、怎么保证后续每个请求都需要验证。
我的做法如下:
- LoginServlet接收
username和password。 - 调用LoginDAO查询数据库,比对密码(用MD5后与库中比较)。
- 用户存在且密码正确,就把
user对象放入Session:
HttpSession session = request.getSession(); session.setAttribute("loginUser", user);- 返回重定向到首页,而不是转发。
对应地,写一个LoginFilter实现JavaWeb里的Filter接口,在doFilter方法中检查Session:
public 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); } }通过这种模式,你能清楚看到JSP+Servlet的MVC流转:表单POST到Servlet,Servlet调用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 prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> ... <ul> <c:forEach items="${roomList}" var="room"> <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 type="date",这个HTML原生控件比自己拼字符串日历强多了。 - 时间范围:用两个
input type="time",再写一个JS校验startTime < endTime,不合格直接拦截提交,减少后端无谓的请求。
当然,前端校验只是提升体验,真正的合法性判断和冲突校验仍要在Servlet后端做。
页面回显时有一个细节:如果表单校验失败被转发回来,request.getParameter("startTime")是可以直接取到刚提交的值的。在JSP里回填:
<input type="time" name="startTime" value="${param.startTime}" required />${param.startTime}是EL表达式读取请求参数的快捷方式,不用手动在Servlet里再次setAttribute。
4.3 纯HTML页面在传统项目里的重要地位
这个项目的技术栈里也有HTML的标签。很多人以为JSP项目的界面全用jsp文件,其实静态HTML页面也扮演关键角色,尤其是在混合开发模式下:
- 登录页可以用静态
login.html做高颜值页面,用户输入后通过fetch或jQuery的ajax把JSON数据POST到/login,Servlet返回JSON字符串,页面拿到结果再跳转。 - 会议室管理后台的表格,也可以用静态HTML页面拉取
RoomListServlet返回的最新JSON数据来渲染,减少整页刷新。
在这个项目里,我建议你至少把登录页写成静态HTML + AJAX,这种“前端动态渲染、后端返回JSON”的合作方式,实际上是很多公司里旧项目升级换代的第一步。让项目展示的时候,可以很自然地说一句:“这里我做了一个前后端分离的过渡体验。”
Servlet返回JSON也很简单,不需要额外的json库,手动拼也够用:
response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"success\": true, \"msg\": \"登录成功\"}");当然,如果是更复杂的列表数据,建议引入fastjson或者Jackson,别手拼JSON字符串,容易被引号转义搞到怀疑人生。
5. 开发与调试:从IDEA配置到WAR包部署的完整链路
5.1 环境配置:JDK、Tomcat、IDEA三者缺一不可
开发JSP+Servlet项目,环境配置环节就能劝退一批人。我的推荐组合:
| 组件 | 版本建议 | 备注 |
|---|---|---|
| JDK | 8 或 11 | 不要轻易上JDK17,老Tomcat版本不兼容 |
| Tomcat | 8.5 或 9.0 | 对应Java EE规范,支持Servlet 4.0 |
| IDEA | Ultimate版 | 社区版对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> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.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?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useSSL=false"); 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 contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>。 - 数据库存取乱码:检查数据库连接URL里是否带
characterEncoding=utf8。 - 控制台乱码:检查IDEA的
Help -> Edit Custom VM Options,加一行-Dfile.encoding=UTF-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;charset=UTF-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 '1'='1,整张表数据就被带出来了,这就是最典型的SQL注入。
正确做法是使用PreparedStatement:
String sql = "SELECT * FROM meeting_room WHERE room_name LIKE ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, "%" + keyword + "%");预编译的好处很直白:SQL结构在编译时已固定,用户输入永远只是参数,不会参与SQL语法解析。这个习惯从JSP+Servlet阶段就要养好,等你以后接触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 关于接口返回结果再包装一下:状态码与消息统一
如果你打算在项目里做基于HTML+AJAX的交互,那么接口返回统一格式这个习惯越早建立越好。我给自己约定的统一字段:
{ "code": 200, "msg": "操作成功", "data": {...} }code表示业务状态码,200成功,500失败,权限不足用403。这样前端用Ajax拿到响应后统一判断code === 200决定是否跳转或刷新,逻辑会简洁很多,另外也为答辩时讲“代码可扩展性”准备了谈资。
我在这个项目里写了一个简单的Result工具类,用静态方法生成标准响应。代码本身不长,但对整个工程代码风格的统一非常有帮助。
这就是我从数据库、Servlet、JSP到部署调试全流程做这个会议室管理系统的核心经验。最后再分享一个我自己的实操心得:如果你打算拿这个项目做毕业设计或课程作业,千万不要临到答辩才把项目跑起来。把运行环境的配置步骤、数据库初始化脚本、访问地址写成一个README文档,答辩现场照着步骤几分钟内就能启动演示,印象分至少能提一档。没人喜欢看一个满头大汗在命令行里敲错命令的演示者,稳才是硬道理。