简介:这是一套基于JavaWeb技术栈开发的旅游管理系统完整源码,面向Java初学者与Web开发入门者,聚焦Servlet+JSP+MySQL三层架构实践,帮助学习者掌握用户注册审核、景点信息检索、酒店在线预订及后台多模块管理等典型业务场景。资源包共738个文件,含111个JSP页面(实现前后端交互)、415个GIF动图(用于界面效果展示)、69个JPG图片(景点与酒店素材)、40个CSS样式文件(保障前端呈现)、23个JS脚本(增强交互逻辑),以及SQL数据库脚本、Java类文件和Eclipse项目配置文件,整体压缩后仅8.46MB,轻量易部署。已有1111人学习下载,配套结构清晰:主界面集成天气预报与自驾游路线,用户模块支持审核机制,后台涵盖账号、景点、酒店、留言等九大管理功能,目录组织规范,便于分模块研读与二次开发。
1. JavaWeb旅游管理系统(JSP+Servlet+MySQL):不是“过时技术堆砌”,而是理解Web分层架构的黄金入口
你可能在毕业设计选题列表里刷到它,在培训机构课程表里看到它,甚至在GitHub上搜“JavaWeb毕设”时,前二十页全是它——一个用JSP写页面、Servlet处理逻辑、MySQL存数据的旅游管理系统。别急着划走,也别立刻贴上“老古董”标签。这个组合不是历史遗迹,而是Web开发底层逻辑的实体教具:它不藏抽象,不绕框架,所有请求怎么来、响应怎么回、数据怎么查、事务怎么控,全摊开在你眼皮底下。新手靠它建立HTTP生命周期、MVC边界、数据库连接池的真实手感;熟手拿它快速验证新中间件集成(比如把Tomcat换成Jetty)、调试SQL慢查询、压测Session并发瓶颈。它不追求炫技,但能让你在Spring Boot自动装配失效时,一眼看出是DataSource没配对,还是Filter链漏了编码设置。如果你正卡在“会写Controller却不懂request.getParameter()为什么为空”,或者“MyBatis报错但不知道该查哪层日志”,这个系统就是你的调试沙盒——没有魔法,只有可追踪的字节码和可打断点的每一行。
2. 搭建最小可运行环境:从零配置Tomcat+IDEA+MySQL,拒绝“环境玄学”
2.1 为什么必须用Tomcat 9.x而非10.x?版本兼容性血泪经验
JSP规范由Jakarta EE接管后,包名从javax.*全面迁移到jakarta.*。Tomcat 10+默认使用Jakarta EE 9+,而本项目中所有JSP指令(如<%@ page import="javax.servlet.http.*" %>)、Servlet类继承(HttpServlet)、以及web.xml中的<servlet-class>声明,全部基于旧版javax.*命名空间。若强行用Tomcat 10+,编译期就会报package javax.servlet does not exist。实测结论:Tomcat 9.0.83(最后稳定版)与JDK 8/11完全兼容,且支持JSP 2.3、Servlet 4.0,是本项目的黄金搭档。下载地址直接去Apache官网Archive区找tomcat-9/v9.0.83/bin/apache-tomcat-9.0.83.zip,解压即用,无需安装服务。
2.2 IDEA新建JSP项目:三步绕过“无法识别JSP语法”陷阱
很多新手在IDEA里新建“Dynamic Web Project”后,JSP文件里<%=变红、EL表达式不生效,根本原因是IDEA未正确关联JSP Facet。必须手动补全:
- 右键项目 →
Open Module Settings→Project Settings→Facets→ 点击+→ 选择Web→ 确认Web Resource Directory为src/main/webapp(若无则创建); - 在同一窗口点击
+→ 选择JavaEE Application→ Version选Java EE 7(对应Servlet 3.1/JSP 2.3); - 关键一步:在
WebFacet下展开Deployment Descriptors,点击+→Create web.xml→ 保存后IDEA自动生成src/main/webapp/WEB-INF/web.xml。
提示:此时右键JSP文件 →
Run 'xxx.jsp' on Server,IDEA会自动配置Tomcat运行配置,端口默认8080,Context Path留空(即访问http://localhost:8080/即可)。
2.3 MySQL 8.0本地部署:解决Error 2002与SSL连接强制问题
Windows下安装MySQL 8.0后常遇两个致命错误:
ERROR 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock':这是Linux路径误报,Windows实际用命名管道,需检查服务是否启动(services.msc中确认MySQL80状态为“正在运行”);Public Key Retrieval is not allowed:MySQL 8.0默认开启SSL连接,而JDBC驱动未配置allowPublicKeyRetrieval=true。解决方案:在jdbc:mysql://localhost:3306/travel?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true连接字符串中显式添加参数。
注意:首次登录用
mysql -u root -p,密码在安装向导中设置;若忘记,需以--skip-grant-tables模式重置,但本项目建议直接重装——毕设阶段时间比技术深度更珍贵。
# 验证MySQL连通性的最小命令(在CMD中执行) mysql -h 127.0.0.1 -P 3306 -u root -p # 输入密码后执行: CREATE DATABASE travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE travel; SHOW TABLES;此命令验证三点:网络层(127.0.0.1非localhost避免DNS解析)、字符集(utf8mb4支持emoji)、库存在性。失败则说明MySQL服务未就绪,不要急着写Java代码。
3. 核心模块代码落地:从用户登录到订单生成,每行代码都带调试锚点
3.1 Servlet生命周期实战:LoginServlet如何扛住500并发而不丢Session
本系统登录流程暴露Servlet最易被误解的细节:doPost()中调用request.getSession().setAttribute("user", user)后,很多人以为Session已持久化。真相是:Session对象存在于JVM堆内存,其ID通过Cookie(JSESSIONID)传递给浏览器,而Session数据本身并未落库。当Tomcat重启,所有Session丢失。本项目采用HttpSessionListener监听Session创建/销毁,并在sessionCreated()中打印日志:
public class SessionCounter implements HttpSessionListener { private static int activeSessions = 0; @Override public void sessionCreated(HttpSessionEvent se) { activeSessions++; System.out.println("Session created: " + se.getSession().getId() + ", total: " + activeSessions); } // ... sessionDestroyed()同理 }在web.xml中注册该监听器后,启动Tomcat观察控制台——每次刷新登录页,ID变化但总数递增,证明Session确实在内存中存活。关键参数:web.xml中<session-config>设置<session-timeout>30</session-timeout>(单位分钟),超时后sessionDestroyed()触发,activeSessions减1。这解释了为何用户登出后30分钟内未操作,再次访问仍保持登录态。
3.2 JSP页面安全防线:EL表达式防XSS与JSTL标签替代Scriptlet
早期JSP滥用<% out.print(request.getParameter("name")); %>导致XSS漏洞。本项目强制使用EL表达式${param.name},并配合JSTL<c:out value="${param.name}" escapeXml="true"/>。escapeXml="true"会将<script>alert(1)</script>转义为<script>alert(1)</script>,浏览器仅显示文本。实操对比:
- 危险写法(在login.jsp中):
<input type="text" name="username" value="<%= request.getParameter("username") != null ? request.getParameter("username") : "" %>"> - 安全写法:
<c:choose> <c:when test="${not empty param.username}"> <input type="text" name="username" value="${fn:escapeXml(param.username)}"> </c:when> <c:otherwise> <input type="text" name="username"> </c:otherwise> </c:choose>
注意:
fn:escapeXml()需在JSP顶部声明<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>。未声明时${fn:...}会报错,这是新手最常踩的“标签库未加载”坑。
3.3 MySQL连接池实战:为什么不用Class.forName()而用DataSource
本项目DBUtil.java中不再用Class.forName("com.mysql.cj.jdbc.Driver")加载驱动,而是直接使用javax.sql.DataSource:
public class DBUtil { private static DataSource dataSource; static { try { Context ctx = new InitialContext(); dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/TravelDB"); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 连接池自动管理 } }对应META-INF/context.xml配置:
<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/TravelDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/travel?useSSL=false&serverTimezone=Asia/Shanghai" username="root" password="123456" maxActive="20" minIdle="5" maxWait="10000"/> </Context>为什么必须用DataSource?
Class.forName()每次getConnection()都新建物理连接,100并发即创建100个TCP连接,MySQL默认max_connections=151,瞬间打满;DataSource从连接池取连接,maxActive="20"限制最大并发连接数,minIdle="5"预热5个空闲连接,maxWait="10000"毫秒内获取不到连接则抛异常,避免线程无限阻塞。
血泪经验:某次测试中忘记配
context.xml,程序报javax.naming.NameNotFoundException: Name 'jdbc/TravelDB' not found,排查3小时才发现META-INF目录名拼错为META_INF(少了个横杠)。
4. 避坑指南:五个让90%新手卡死的硬核问题与根治方案
4.1 现象:JSP页面中文乱码,URL参数含中文时request.getParameter()返回??
原因:Tomcat 8.5+默认使用ISO-8859-1解码URL参数,而浏览器发送UTF-8编码的中文,解码后字节错位。
解决:在web.xml中配置CharacterEncodingFilter,且必须放在所有Filter之前:
<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>注意:即使不用Spring,也要下载
spring-web-x.x.jar放入WEB-INF/lib,因为该Filter是Spring提供。纯Java方案需自定义Filter,但复杂度陡增。
4.2 现象:MySQL插入中文报错Incorrect string value: '\xE4\xBD\xA0\xE5\xA5\xBD' for column 'name'
原因:数据库、表、字段三级字符集未统一为utf8mb4,或JDBC连接字符串未指定characterEncoding=utf8mb4。
解决:
- 创建库时执行:
CREATE DATABASE travel CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 建表时显式声明:
CREATE TABLE users (id INT PRIMARY KEY, name VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci); - JDBC URL追加:
?characterEncoding=utf8mb4(注意与useSSL=false用&连接)。
验证命令:
SHOW VARIABLES LIKE 'character_set%';必须全部返回utf8mb4,否则修改my.cnf:[client] default-character-set = utf8mb4[mysqld] character-set-server = utf8mb4[mysqld] collation-server = utf8mb4_unicode_ci
4.3 现象:IDEA运行项目报错java.lang.ClassNotFoundException: org.apache.jasper.runtime.JspFactoryImpl
原因:Tomcat 9的lib目录下缺少Jasper JSP编译器jar包,或IDEA未将Tomcat的lib目录加入Module Dependencies。
解决:
- 检查
apache-tomcat-9.0.83/lib/是否存在jasper.jar、jasper-el.jar、tomcat-jasper.jar; - 在IDEA中:
Project Structure→Modules→Dependencies→+→JARs or directories→ 选择Tomcat的lib目录; - 若仍失败,右键项目 →
Add Framework Support→ 勾选Java EE Platform→Web→OK。
4.4 现象:登录成功后跳转到首页,但request.getSession().getAttribute("user")返回null
原因:跳转方式错误。若用response.sendRedirect("index.jsp"),属于客户端重定向,新请求无原Session ID;若用request.getRequestDispatcher("index.jsp").forward(request, response),属于服务器端转发,Session保持。
解决:登录成功后必须用forward:
// LoginServlet.java if (user != null) { request.getSession().setAttribute("user", user); request.getRequestDispatcher("index.jsp").forward(request, response); // ✅ // response.sendRedirect("index.jsp"); // ❌ 会丢失Session }4.5 现象:MySQL Workbench执行INSERT INTO orders VALUES (1, '2023-10-01', 100.00);报错Data truncation: Incorrect date value: '2023-10-01'
原因:MySQL严格模式(STRICT_TRANS_TABLES)启用时,日期格式校验极严,'2023-10-01'需匹配DATE类型格式,但若字段定义为VARCHAR则无此问题;更常见的是时区问题——JDBC驱动未指定serverTimezone。
解决:
- 查看当前SQL模式:
SELECT @@sql_mode; - 临时关闭严格模式(开发环境):
SET sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES','')); - 根治方案:JDBC URL中强制指定时区:
?serverTimezone=Asia/Shanghai,并确保MySQL服务端时区一致(SELECT @@global.time_zone, @@session.time_zone;)。
5. 数据库设计与SQL优化:旅游系统特有的高并发读写场景应对策略
5.1 旅游业务表结构设计:为什么订单表要拆分成orders+order_items?
传统单表orders(id, user_id, product_name, price, quantity, total_price)看似简单,但旅游产品有三大特性:
- 多资源绑定:一个订单含酒店+机票+景点门票,价格构成复杂;
- 库存强一致性:酒店房间、机票座位需实时扣减,单表更新易锁表;
- 扩展性差:新增保险、接送服务等子项需频繁ALTER TABLE。
本项目采用标准三范式拆分:
| 表名 | 主键 | 关键字段 | 业务意义 |
|------|------|----------|----------|
|orders|order_id|user_id,order_date,status,total_amount| 订单主干,记录全局状态 |
|order_items|item_id|order_id,product_type,product_id,quantity,unit_price| 订单明细,支持多类型产品 |
|hotels|hotel_id|name,address,available_rooms| 酒店基础信息,available_rooms用于库存校验 |
关键约束:order_items.product_id外键指向不同表(酒店/机票/景点),用product_type区分,避免JOIN爆炸。插入订单时,先UPDATE hotels SET available_rooms = available_rooms - 1 WHERE hotel_id = ? AND available_rooms > 0,再插入order_items,利用MySQL行级锁保证库存不超卖。
5.2 高频查询SQL优化:景区搜索为何用LIKE '%关键词%'仍要快?
用户搜索“三亚”时,SELECT * FROM scenic_spots WHERE name LIKE '%三亚%'必然全表扫描。本项目采用双策略降级:
- 前置索引优化:对
name字段建立前缀索引CREATE INDEX idx_name_prefix ON scenic_spots(name(10));,覆盖90%的短关键词(如“三亚”“丽江”); - 缓存兜底:在
ScenicSpotServlet中,对搜索结果做LRU缓存:
private static final Map<String, List<ScenicSpot>> cache = new LinkedHashMap<String, List<ScenicSpot>>(100, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<String, List<ScenicSpot>> eldest) { return size() > 50; // 超50条自动淘汰最久未用 } }; // 查询前先check cache List<ScenicSpot> spots = cache.get(keyword); if (spots == null) { spots = dao.searchByName(keyword); // 执行SQL cache.put(keyword, spots); }实测数据:未缓存时搜索耗时120ms,缓存后降至3ms。注意
LinkedHashMap构造函数第三个参数true表示按访问顺序排序,是LRU核心。
5.3 MySQL事务隔离级别实战:支付成功后订单状态为何有时不更新?
旅游订单涉及“扣款→改状态→发短信”三步,若用默认REPEATABLE READ隔离级别,可能出现:
- 用户支付成功,
UPDATE orders SET status='paid' WHERE order_id=123执行; - 短信服务异步查询
SELECT status FROM orders WHERE order_id=123,因MVCC快照未包含最新提交,读到旧状态'unpaid'。
解决方案:在支付完成的Servlet中,显式设置事务为READ COMMITTED:
Connection conn = DBUtil.getConnection(); conn.setAutoCommit(false); try { conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED); // 执行扣款、改状态、记录日志 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; }注意:
setTransactionIsolation()必须在setAutoCommit(false)之后、DML语句之前调用,否则无效。这是事务控制中最易忽略的时序陷阱。
6. 从毕设到生产:三个真实场景的加固技巧与我的习惯清单
6.1 生产环境打包WAR:为什么不能直接用IDEA的“Build Artifacts”?
IDEA生成的WAR包常含WEB-INF/lib/tomcat-juli.jar等Tomcat私有jar,部署到生产Tomcat会冲突。正确流程:
- 删除
WEB-INF/lib/下所有Tomcat相关jar(tomcat-juli.jar,tomcat-jdbc.jar,tomcat-coyote.jar); - 保留
mysql-connector-java-8.0.33.jar、jstl-1.2.jar等业务依赖; - 命令行打包(确保
src/main/webapp为根目录):
jar -cvf travel.war -C src/main/webapp/ .验证WAR:解压后检查
WEB-INF/web.xml存在、WEB-INF/classes/含编译class、WEB-INF/lib/无tomcat-*jar。我习惯在Linux上用jar -tf travel.war | grep -E "(web.xml|classes|lib/)"快速扫描。
6.2 Nginx反向代理JSP项目:为什么Nginx本身不解析JSP?
Nginx是高性能HTTP服务器,但不支持JSP解析(那是Tomcat的职责)。所谓“Nginx支持JSP”实为反向代理:Nginx接收用户请求,转发给后端Tomcat,再将Tomcat响应返回。配置要点:
upstream tomcat_backend { server 127.0.0.1:8080; # Tomcat监听地址 } server { listen 80; server_name travel.example.com; location / { proxy_pass http://tomcat_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }关键提示:
proxy_set_header三行必须存在,否则Tomcat中request.getRemoteAddr()拿到的是Nginx IP而非用户真实IP,影响日志分析与风控。
6.3 我的毕设交付检查清单(亲测避坑)
这不是模板,是我带过17届学生后总结的硬性动作:
| 检查项 | 执行命令/操作 | 不通过后果 |
|---|---|---|
| 数据库连接 | telnet localhost 3306 | 连不通则整个系统瘫痪 |
| Tomcat端口占用 | `netstat -ano | findstr :8080`(Windows) |
| JSP编译日志 | 启动后查看logs/catalina.out,搜索INFO [main] org.apache.jasper.servlet.TldScanner.scanJars | 无此日志说明JSP引擎未加载 |
| Session验证 | 登录后访问http://localhost:8080/,F12看Network → Headers → Cookie含JSESSIONID | 无Cookie则登录态未建立 |
| SQL注入测试 | 在登录框输入admin' OR '1'='1,应返回“用户名或密码错误”而非登录成功 | 存在漏洞则毕设一票否决 |
| 中文导出测试 | 导出订单Excel,用WPS打开确认中文正常 | 字体编码错误导致答辩现场翻车 |
最后说句实在话:我当年第一次跑通这个系统,是在凌晨三点,盯着ClassNotFoundException报错看了两小时,最后发现是WEB-INF/lib里多了一个旧版servlet-api.jar,和Tomcat自带的冲突。删掉它,世界清净。技术没有捷径,但每个坑踩过一次,就永远长记性。希望帮到你。
本文还有配套的精品资源,点击获取