
简介这是基于SSM与JSP的喀什网上游客服务中心系统面向Java后端学习者及毕业设计人群。项目整合Spring、SpringMVC、MyBatis与JSP前端覆盖用户注册、导游信息、酒店预订等旅游业务模块同时包含支付宝接口相关源码适合作为毕设或课程设计进行二次开发。压缩包共1300个文件体积33.65MB主体为144个JSP页面、88个Java源文件及对应class文件搭配JS/CSS等前端资源、80个依赖jar包、SQL初始化脚本与XML配置结构完整按模块导入开发工具运行即可各业务模块按Controller、Service、Mapper分层组织便于理解SSM调用链和JSP交互。运行环境已标注JDK1.8、Tomcat7、MySQL5.7、Maven3.3.9。目前已有101人学习浏览源码经过调试部署疑问可直接与博主沟通可获取可运行工程及数据库脚本便于对照学习核心业务逻辑并修改扩展。1. 喀什网上游客服务中心系统一个 JSP 项目为什么还值得你拆开看很多人看到 JSP 会下意识觉得这是上古技术但如果你在旅游信息管理、政务预约这类业务里待过会发现 JSP Servlet MySQL 的存量项目比想象中多得多。这个喀什网上游客服务中心系统从命名看就是一个把景区服务项目、游客注册、在线预约、后台公告糅在一起的经典 Java Web 工程。它的价值在于你不需要追新框架就能在几天内把一套可用系统从建表跑到部署特别适合课程设计、毕业设计、小型外包或者刚接手老项目的人当蓝本。接下来我会按自己实际做这类项目的顺序把数据模型、项目骨架、核心页面、常见坑和部署验证一次性讲透。2. 数据模型先行游客服务中心系统要建哪几张表2.1 技术选型理由为什么这个组合仍然能打先别急着写代码做这类系统第一步是确定技术边界。喀什网上游客服务中心系统的核心诉求是游客能浏览服务项目、注册账号、提交预约管理员能维护项目内容、查看预约、发布公告。这类业务的特点是并发量不高、表单字段固定、页面以信息展示为主而且往往只有一两个人维护。JSP Servlet MySQL 的三层结构在这个量级下完全够用部署也简单——一个 Tomcat 加一个 MySQL 就能跑。有人会说前后端分离才是趋势但你要分辨场景。如果后期需要做小程序或 App 接口确实可以用 Spring Boot 写接口、前端用 Vue但如果是内部管理系统或课程设计JSP 页面直接嵌到 Web 容器里反而少了一层跨域和联调的麻烦。我一般建议这样选型需求以页面为主、团队没有专业前端就坚持 JSP Servlet预计有移动端扩展再把 Service 层抽出来之后加接口也方便。这种取舍不是技术倒退是对交付成本的务实判断。2.2 五张核心表的结构设计与字段说明游客服务中心系统的数据模型不复杂但表与表之间的关系要提前理清。我通常拆成五张表管理员表、游客表、服务项目表、预约表、公告表。下面是一份可以直接拿去建库的 SQL 脚本数据库用 MySQL 5.7 及以上。CREATE DATABASE IF NOT EXISTS kashi_travel DEFAULT CHARSET utf8mb4; USE kashi_travel; CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT MD5加盐后的密文, real_name VARCHAR(30) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE tourist ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(30) NOT NULL, phone VARCHAR(20) NOT NULL, id_card VARCHAR(18) DEFAULT COMMENT 预留证件字段, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE service_item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 服务项目名称, category VARCHAR(30) NOT NULL COMMENT 分类景点/线路/酒店/餐饮, description TEXT, image_url VARCHAR(255) DEFAULT COMMENT 展示图片相对路径, price DECIMAL(10,2) DEFAULT 0.00, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE reservation ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务单号, tourist_id INT NOT NULL, service_id INT NOT NULL, service_name VARCHAR(100) NOT NULL COMMENT 冗余冗余, reserve_date DATE NOT NULL COMMENT 预约游玩日期, people_count INT DEFAULT 1, contact_phone VARCHAR(20) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_tourist (tourist_id), KEY idx_service (service_id), CONSTRAINT fk_res_tourist FOREIGN KEY (tourist_id) REFERENCES tourist(id), CONSTRAINT fk_res_service FOREIGN KEY (service_id) REFERENCES service_item(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE notice ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(100) NOT NULL, content TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计有几点值得说。状态字段都用 TINYINT 而不是 VARCHAR因为状态是固定枚举用数字存储更省空间查询条件也写得更干净如果后面要加状态只需要在代码里维护一个常量类不必改表结构。reservation 表里冗余了 service_name虽然这违反了严格的范式但预约列表页要频繁展示服务名称冗余字段可以少一次关联查询这是典型的读多写少场景下的妥协做法。外键约束我保留了但在生产环境如果数据量上去可以考虑去掉外键、改成应用层保证避免插入更新时的锁开销。2.3 初始化数据与连接层让项目先跑起来表建好后要插入默认管理员和几条服务项目数据不然系统启动后是空页面调试不方便。常见做法是写一个 init.sql 并在建库后手动执行或者直接在项目启动时用 ServletContextListener 检测表无数据再插入。这里给出最直接的初始化语句INSERT INTO admin (username, password) VALUES (admin, e10adc3949ba59abbe56e057f20f883e); INSERT INTO service_item (name, category, description, image_url, price) VALUES (喀什古城一日游, 景点, 漫步千年古城体验维吾尔族风情, /images/city.jpg, 0.00), (帕米尔高原两日游, 线路, 慕士塔格峰、卡拉库里湖经典线路, /images/pamir.jpg, 680.00), (香妃园参观, 景点, 了解历史典故与民族建筑风格, /images/xiangfei.jpg, 30.00);注意管理员密码我这里只写了一个 MD5 明文结果实际项目中密码必须加盐后面第 4 章会讲到具体做法。现在把这些数据准备好接下来要处理的是 Java 代码怎么连上 MySQL。不引框架的话最省事的方案是用 Druid 连接池加一个工具类。public class DbUtil { private static DruidDataSource dataSource; static { try { Properties props new Properties(); props.load(DbUtil.class.getClassLoader().getResourceAsStream(db.properties)); dataSource (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (ps ! null) ps.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }这里的关键是 db.properties 里的配置项。initialSize 建议设 5maxActive 设 20maxWait 设 60000因为游客中心的预约量通常不会超过这个连接数。连接池的好处是避免每次请求都新建数据库连接我之前见过一个没上连接池的项目压测到 50 个并发就直接报 Too many connections换了 Druid 之后同样的并发量连接数稳定在 10 个以内。工具类里的 close 方法必须在 finally 块中调用漏掉一次就是一次连接泄漏这个问题在第 5 章会再展开。3. 搭建项目骨架目录结构、web.xml 与三层分包3.1 一个 JSP 工程的标准目录长什么样不管这个压缩包是用 MyEclipse 还是 IDEA 导出的JSP Web 项目的目录结构基本逃不出两种Maven 结构和传统 WebRoot 结构。Maven 结构下Java 源码在 src/main/javaJSP 页面在 src/main/webapp依赖由 pom.xml 管理。传统结构则是 src 下直接放 Java 包WebRoot 下放 JSP 和 WEB-INF。我个人推荐 Maven 结构哪怕是一个小项目也值得用 Maven 管 jar 包不然你手动画 JSTL、MySQL 驱动的依赖会把自己搞疯。下面是典型的 Maven Web 工程片段project groupIdcom.kashi/groupId artifactIdtravel-center/artifactId packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version3.1.0/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.6/version /dependency /dependencies /project注意 servlet-api 的 scope 必须是 provided因为 Tomcat 自己带了 Servlet 容器实现如果你打成 war 还把这个依赖打包进去部署时会报类冲突。JSTL 1.2 是打包了实现类的可以正常打进去。如果你在 Java 代码里用了 javax.servlet 但又不需要单独运行provided 是唯一正确的选择。分包上我习惯按 com.kashi.travel 作为根包下面建 controller、service、dao、entity、util 五个子包。Entity 类对应每张表DAO 只负责 SQLService 管事务和业务判断Controller 接收请求并跳转页面。这样做的直接好处是如果 JSP 页面要改成接口返回 JSON你只需要改 Controller 层Service 和 DAO 可以原封不动地复用。3.2 web.xml 配置编码、Session 超时与 Servlet 映射web.xml 是整个 Web 应用的入口配置。在 Servlet 3.0 之后可以用注解替代部分映射但编码过滤器、Session 超时、欢迎页这些还是会在 web.xml 里配置。建议保留一个简洁的 web.xml方便一眼看到全局配置。web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping session-config session-timeout30/session-timeout /session-config welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list /web-app如果项目没有引入 Spring 框架CharacterEncodingFilter 会不存在所以我在自己的项目里通常写一个自定义的 EncodingFilter逻辑就是强制 request 和 response 的字符编码为 UTF-8。注意一个细节Filter 的 url-pattern 用 /*这能覆盖所有 JSP 和 Servlet但如果你有静态资源路径最好再配一个静态资源映射避免图片被过滤器拦截处理。Session 超时 30 分钟是合理值游客在页面上停留时间不会太长管理员后台操作也不至于频繁超时。3.3 实体类与 DAO 基础CRUD 代码怎么写得少又稳实体类不复杂但要注意字段类型要和数据库对应。比如 service_item 的 price 用 BigDecimalcreate_time 用 java.util.Date。我给每个实体类都实现 Serializable 接口因为后面 Session 中可能要存对象。再写一个 BaseDAO 把重复的 JDBC 操作收敛起来。public class BaseDAOT { protected ListT queryList(String sql, RowMapperT mapper, Object... params) { ListT result new ArrayList(); Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DbUtil.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } rs ps.executeQuery(); while (rs.next()) { result.add(mapper.mapRow(rs)); } } catch (SQLException e) { throw new RuntimeException(查询失败, e); } finally { DbUtil.close(rs, ps, conn); } return result; } protected int update(String sql, Object... params) { Connection conn null; PreparedStatement ps null; try { conn DbUtil.getConnection(); ps conn.prepareStatement(sql); for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(更新失败, e); } finally { DbUtil.close(null, ps, conn); } } }这里用可变参数 Object... 把 SQL 参数统一传进去RowMapper 接口负责把 ResultSet 映射成实体对象。这种写法比每个 DAO 方法都写一遍连接获取、异常处理要干净得多也避免了最容易犯的忘了关闭连接的错误。注意 PreparedStatement 用 setObject 传参时MySQL 驱动会自动匹配类型但日期字段如果你传的是 java.util.Date驱动可能不认识建议在 SQL 里显式用 ? 配合 convert 或者在实体转换时用 java.sql.Timestamp这个问题排查起来挺玄学我一般在实体类日期字段用 java.sql.Timestamp 或调用 setTimestamp 处理。4. 核心功能实现从登录到个人信息展示页面的落地4.1 登录认证与 Session 管理别把用户状态搞丢游客和共用一个登录入口还是分开登录我建议分开前台游客登录走 TouristLoginServlet后台管理员走 AdminLoginServlet各自维护 Session 中的 key比如 session.setAttribute(loginTourist, tourist) 和 session.setAttribute(loginAdmin, admin)。这样的好处是权限控制时直接判断 Session 里存在哪个对象。下面是一个游客登录的 Servlet 核心逻辑WebServlet(/tourist/login) public class TouristLoginServlet extends HttpServlet { private TouristService touristService new TouristService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); String code req.getParameter(code); String sessionCode (String) req.getSession().getAttribute(kaptchaCode); if (sessionCode null || !sessionCode.equalsIgnoreCase(code)) { req.setAttribute(error, 验证码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } Tourist tourist touristService.login(username, md5WithSalt(password)); if (tourist null) { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(loginTourist, tourist); session.setMaxInactiveInterval(30 * 60); resp.sendRedirect(req.getContextPath() /index.jsp); } }这里我故意没有用重定向到 Servlet 再转发而是用了 sendRedirect 到首页核心原因是防止表单重复提交——用户刷新浏览器时不会再次触发 POST。密码那里我调用了 md5WithSalt 方法不要直接存 MD5 明文建议用 MD5(username password fixedSalt) 的组合虽然 MD5 本身不够强但在课程设计和内部系统里加盐之后的复杂度已经足够挡住绝大多数随手尝试。验证码我用了 Kaptcha 生成的图片然后把值存在 Session 的 kaptchaCode 属性里校验后立即删除防止重复使用同一个验证码。关于 Session 有几个必须注意的地方Session 默认是基于 Cookie 的如果用户浏览器禁用了 Cookiesession 就会丢失这时需要 URL 重写用 response.encodeURL() 给每个链接加上 JSESSIONID 参数。另外 sendRedirect 和 forward 对 Session 的影响完全不同sendRedirect 是新请求Session 仍然能用但 request 域里的属性会全部丢失所以登录成功后要传给页面的数据要么放 Session要么重定向后再查一次数据库。4.2 jsp个人信息展示页面从 Java 小脚印到 EL JSTL热词里的“JSP个人信息展示页面”正是这个系统核心中的核心。很多老项目在 JSP 里写一堆 % % 小脚印页面乱得没法维护。我建议从第一天就用 EL 表达式和 JSTL 标签库来写页面。下面是一个典型的个人中心页面展示游客基本信息和预约记录列表% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html head title个人中心/title /head body h2欢迎${sessionScope.loginTourist.realName}/h2 h3我的预约/h3 table border1 tr th订单号/th th服务项目/th th游玩日期/th th人数/th th状态/th /tr c:forEach items${reservationList} varres tr td${res.orderNo}/td td${res.serviceName}/td tdfmt:formatDate value${res.reserveDate} patternyyyy-MM-dd//td td${res.peopleCount}/td td c:choose c:when test${res.status 0}待确认/c:when c:when test${res.status 1}已确认/c:when c:when test${res.status 2}已完成/c:when c:otherwise已取消/c:otherwise /c:choose /td /tr /c:forEach /table /body /html后端 Servlet 要做的事情很简单从 Session 取出游客 ID调用 service 查询这个人的预约列表setAttribute 到 request 域然后 forward 到上面的 JSP 页面。这里就体现出分层的好处了JSP 里没有一行 Java 代码全部是 EL 取数据后期要换成 Bootstrap 界面或者加一个导出 Excel 功能完全不碰 Java 逻辑。注意 EL 表达式取数据的语义${sessionScope.loginTourist.realName} 会调用 Tourist 对象的 getRealName() 方法${res.status 0} 这里的比较EL 会自动把 Integer 和 int 比较不会出类型问题。想省事的话也可以用 ${res.status eq 0}。对于日期格式化JSTL 的 fmt:formatDate 要求 value 是 java.util.Date如果你的实体类字段是 Timestamp 也能正常响应。4.3 JSP 图片如何对坐标定位路径问题与图片地图搜索热词里那句“jsp图片如何对坐标定位”恰好戳中这类旅游网站的高频需求——景区导览图上标注景点位置。我把它拆成两个部分来处理一是图片本身能不能正确显示二是图片上怎么定位坐标。图片显示不出来九成是路径写错。JSP 页面里千万不要写相对路径images/city.jpg因为页面请求 URL 可能是/service/detail这种 Servlet 路径浏览器会把相对路径解析到/service/images/city.jpg自然 404。正确做法是用${pageContext.request.contextPath}拼绝对根路径img src${pageContext.request.contextPath}/images/city.jpg alt喀什古城如果你项目里用了base标签那相对路径可以以 base 为准但我的习惯是直接写 contextPath简单明了不会因为页面层级深了就翻车。图片上的坐标定位最简单的原生方式是用map标签的热区映射。比如喀什古城导览图上要把“艾提尕尔清真寺”“百年老茶馆”两个点做成可点击区域img src${pageContext.request.contextPath}/images/kashi-map.jpg usemap#kashiMap alt喀什古城导览图 map namekashiMap area shaperect coords120,80,220,150 href${pageContext.request.contextPath}/service/detail?id1 alt艾提尕尔清真寺 area shapecircle coords300,200,30 href${pageContext.request.contextPath}/service/detail?id2 alt百年老茶馆 /mapcoords 的四个数字分别代表左上角 x、左上角 y、右下角 x、右下角 ycircle 则是圆心 x、圆心 y、半径。这里的坐标是相对于图片原始尺寸的如果 CSS 把图片设成响应式缩放坐标就会移位。我的建议是给图片设一个固定宽度或者用 JavaScript 在页面加载后根据图片实际显示尺寸等比换算坐标。如果你需要更复杂的动态标注那就别用 map 标签了直接把坐标数据从数据库查出用 JS 在图片上动态生成覆盖层JSP 只负责把坐标数据用 EL 输出到 JavaScript 变量里这个方案后面做地图交互时更好扩展。4.4 管理后台的增删改查一个通用流程模板后台管理是整个系统中工作量最大的部分但它本质上是同一个模式的重复列表 → 新增/编辑表单 → 保存 → 列表。我建议用 Service 层封装好事务边界Controller 里只做参数接收和页面跳转。下面是新增服务项目的 Servlet 片段WebServlet(/admin/service/save) public class ServiceSaveServlet extends HttpServlet { private ServiceItemService serviceItemService new ServiceItemService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String idStr req.getParameter(id); String name req.getParameter(name); String category req.getParameter(category); String priceStr req.getParameter(price); String imageUrl req.getParameter(imageUrl); String statusStr req.getParameter(status); ServiceItem item new ServiceItem(); if (idStr ! null !idStr.isEmpty()) { item.setId(Integer.parseInt(idStr)); } item.setName(name); item.setCategory(category); item.setPrice(new BigDecimal(priceStr)); item.setImageUrl(imageUrl); item.setStatus(Integer.parseInt(statusStr)); boolean success serviceItemService.save(item); resp.sendRedirect(req.getContextPath() /admin/service/list?success success); } }这段代码有一个隐藏的问题如果 price 参数不是合法数字new BigDecimal 会抛异常。我在自己的项目里通常会用一个简单的参数校验工具或者直接在 doPost 里 try-catch把异常信息设置到 Session 里再重定向回编辑页面。红色按钮的教训是千万不要把异常堆栈直接输出到页面上既难看又泄露敏感信息。这里没有用事务注解是因为简单项目里 Service 方法通常只调一个 DAO 方法不存在跨表问题如果新增服务项目时要同时写操作日志表那就要在 Service 层获取同一个 Connection 并开启事务。这一点设计时要想清楚别到上线后数据不一致了才后悔。5. 避坑指南JSP 项目从开发到部署的四个高频翻车点5.1 中文乱码过滤器配了还是乱问题出在哪现象JSP 页面中文正常但表单提交到 Servlet 后request.getParameter 取出来的值变成了问号或者乱码或者数据库里读出来的中文是乱码。原因Tomcat 8 及以上版本中POST 请求的编码默认是 UTF-8但如果你在 Servlet 里写了 request.setCharacterEncoding(UTF-8)并且这一行在第一次读取参数之前执行那一般不会乱。真正的坑在两点第一web.xml 里的编码过滤器没有配置导致每个 Servlet 都要自己写那行代码第二MySQL 连接串上没有指定 characterEncodingutf8导致写入数据库时按默认的 latin1 转换还有一处在 JSP 页面顶部没有写 contentType 的 charsetUTF-8。解决三步走。一是在 web.xml 或自定义过滤器里统一设置 request 和 response 编码二是在 JDBC 连接串上加上?useUnicodetruecharacterEncodingutf8三是 JSP 页面头部写全% page contentTypetext/html;charsetUTF-8 languagejava %。少数情况下数据库表本身是 latin1那就需要 ALTER TABLE 转换字符集这个只能靠运维脚本处理。5.2 图片 404 与路径错乱Servlet 转发和重定向的后果完全不同现象页面在根目录下打开时图片正常点进某个详情页后图片全部裂开或者部署到服务器后所有图片都变成 404。原因JSP 中使用相对路径访问图片时浏览器会以当前 URL 的目录为基准去拼图片地址。比如详情页的 URL 是/service/detail?id1浏览器看到的目录是/service/那么images/city.jpg就会变成/service/images/city.jpg而这个路径在项目里根本不存在。解决所有静态资源引用一律使用${pageContext.request.contextPath}开头。如果你在写 CSS 文件时也用了 url()那 CSS 内部相对路径是相对于 CSS 文件所在目录的情况又不一样。我踩过最深的坑是表情包似的一个是 JSP 页面里的img一个是link引入的 CSS 里 background-image 的路径两个需要分别处理。前者用 contextPath后者把图片放到 CSS 同级的 images 目录下才安全。5.3 连接池耗尽一个页面慢查询拖垮整个系统现象系统运行一两天后所有页面突然变得极慢后台日志大量报Cannot get a connection, pool exhausted。重启 Tomcat 后恢复正常但过一两天又复发。原因代码里某处获取了 Connection 之后没有关闭。最常见的是在查询方法里先拿到了 conn又创建了 ps 和 rsfinally 里只关了 ps 或只关了 rsconn 一直没释放。Druid 连接池默认 maxActive 是 20当 20 个连接全部泄漏后新的请求只能等 maxWait 超时然后抛异常。解决给 BaseDAO 的 finally 块里统一调用 DbUtil.close(rs, ps, conn)并且在开发阶段打开 Druid 的 leakDetection 监控设置druid.testWhileIdletrue、druid.removeAbandonedtrue。你可以在连接池配置里加上 removeAbandonedTimeoutMillis30000这样超过 30 秒没归还的连接会被强制回收但根本上还是要靠代码审查把所有 conn 关闭逻辑收口到一个工具类方法中。5.4 JSP 里 Java 代码过重功能能跑维护是噩梦现象某天要改一个显示逻辑打开 JSP 发现里面有几百行%开头的 Java 代码数据查询、权限判断、HTML 拼接全混在一起改一个字段要花半天时间找位置。原因这是老项目的通病也是这个标题下很多网上下载的源码存在的普遍问题。因为 JSP 里写 Java 代码最直接不需要额外建类所以大家默认往里堆。解决狠下心做一轮重构。把页面里的数据获取逻辑全部挪到 Servlet把 if-else 判断尽量留在 Controller页面里只用 EL 和 JSTL。这个重构不需要动数据库也不用重写 Service 层就是机械地把%块改成 Servlet 代码然后在 JSP 中用c:forEach和c:if替换循环和分支。做完之后你会发现页面模板的保真度大幅提升也更容易套用现成的 Bootstrap 主题。我给这种重构留的经验是每次只重构一个页面因为一次全量改完万一某个变量名写错整个系统都要回滚。6. 部署验证与交付技巧让这套系统在 Tomcat 上稳定跑起来系统开发完最后一步是把 war 包部署到 Tomcat 并做基础验证。我习惯先在本地用 IDEA 集成 Tomcat 跑通再执行mvn clean package打包把生成的 war 文件拷到服务器的 webapps 目录下。第一次部署建议直接 wget 方式上传别用 FTP 传一半导致 war 包损坏。部署后第一个要验证的是日志打开 Tomcat 的 catalina.out 和 localhost.log看有没有中文乱码、ClassNotFound、端口占用。之后按业务链路走一遍游客注册 → 登录 → 查看服务项目 → 提交预约 → 管理员登录 → 确认预约。这条链路能走通基本功能就没有大问题了。优化上有三个点值得投入。第一是 SQL 层面service_item 列表页如果数据量上百条必须加分页我用的是 LIMIT ?, ?参数从页面传入时记得做类型检查和上限控制防止被人传个一万页把数据库打爆。第二是安全层面所有输入框都要在 Servlet 里做长度和格式校验JSP 页面输出时用c:out或 JSTL fn:escapeXml 转义用户内容防止存储型 XSS。第三是备份策略用一个 crontab 脚本每天凌晨执行 mysqldump 备份数据库文件到另外一台机器磁盘坏了也能恢复。还有一个我特别想提醒的习惯交付给别人时不要只给一个 war 包要把建表 SQL、示例数据、部署说明写在一个 README 里。很多人收到的压缩包里只有源码没有数据库脚本结果项目怎么都跑不起来到处问人。你如果能多写几行说明对方省下的时间是你无法想象的。我也是从课设一路做到生产系统的JSP 项目看起来简单但真正踩过路径、编码、连接池、发布这些坑之后你再看任何 Java Web 项目都能一眼看出问题在哪。希望今天这篇能把你的路上几块石头提前搬开帮你少熬几个夜。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。