ARTICLE DETAIL

资讯详情

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

JavaWeb旅游管理系统:JSP+Servlet+MySQL实战入门指南

JavaWeb旅游管理系统:JSP+Servlet+MySQL实战入门指南 简介这是一套基于JavaWeb技术栈开发的旅游管理系统完整源码面向Java初学者与Web开发入门者聚焦ServletJSPMySQL三层架构实践帮助学习者掌握用户注册审核、景点信息展示、酒店在线预订及后台多模块管理等典型业务场景。资源包共738个文件含111个JSP页面实现前后端交互、415个GIF动图用于界面效果展示、69个JPG图片景点与酒店素材、40个CSS样式文件统一前端视觉、23个JS脚本增强交互逻辑以及SQL数据库脚本、Java类文件和Eclipse项目配置文件等整体压缩后仅8.46MB轻量易部署。已有1111人下载学习资源结构清晰包含完整MVC分层目录、可运行的MySQL数据库含初始化数据、带审核机制的用户权限体系以及后台对塞北人文、景点、自驾游路线、留言板等特色模块的CRUD管理功能是理解JavaWeb企业级项目流程的理想练手案例。1. 为什么一个“JavaWeb旅游管理系统JSPServletMySQL”至今仍是校企衔接最稳的练手项目不是因为它多前沿——它用的是2003年就定型的JSP规范、2005年成熟的Servlet 2.5 API、以及MySQL 5.7这种连官方都已停止主流支持的版本而是因为它像一把解剖刀把Web开发最底层的三根主干请求生命周期如何被Servlet接管、HTML如何被JSP动态拼装、关系数据如何通过JDBC落地到MySQL表结构里全摊开在你眼皮底下。没有Spring Boot自动装配的黑匣子没有MyBatis的XML映射抽象层没有Vue的响应式劫持——你改一行request.getParameter(username)就能立刻看到登录页提交后用户名是否真的传进来了你删掉web.xml里一个servlet-mapping整个功能路由就404你把user表里phone字段设为NOT NULL但代码里没校验空值插入时就会在控制台炸出SQLException: Column phone cannot be null。它不教你怎么写高并发秒杀但它逼你亲手把HTTP请求从TCP三次握手开始一帧一帧拆解到ResultSet遍历结束。适合刚学完Java基础、还没碰过框架的本科生也适合想回炉重造、看清MVC骨架的老手——因为所有“玄学报错”最后都能归因到三个文件.jsp、.java、.sql。2. 搭建最小可运行环境用IDEA跑通第一个Servlet绕开90%新手卡点2.1 选版本组合为什么坚持用Tomcat 8.5 MySQL 5.7 JDK 8这不是守旧是踩坑后的血泪经验。Tomcat 9 默认启用HTTP/2而JSP 2.3规范对EL表达式解析有细微差异会导致c:if test${empty user}这类写法在某些页面突然失效MySQL 8.0默认开启caching_sha2_password认证插件而老版mysql-connector-java 5.1.47JSP项目标配驱动根本不认识它连接时直接抛Access denied for user——你查日志、改密码策略、重装驱动折腾半天才发现是版本错配JDK 11移除了javax.xml.bind等模块而部分JSP标签库如JSTL 1.2依赖它编译期就报NoClassDefFoundError。所以稳定压倒一切用apache-tomcat-8.5.99实际取8.5.x最新稳定版、mysql-5.7.44官网归档版、jdk-8u202非最新但兼容性最强。这三个版本在CSDN、掘金、B站实测视频中出现频率最高意味着你搜报错时90%的答案能直接复用。2.2 IDEA配置Tomcat三步完成部署拒绝“Artifact is not configured”提示不要用IDEA自带的“Add Framework Support”向导——它会自动生成冗余的web.xml和错误的输出路径导致/WEB-INF/classes下找不到编译后的Servlet类。手动配置才是正解下载并解压Tomcat将apache-tomcat-8.5.99.zip解压到无中文、无空格路径例如D:\tomcat85在IDEA中添加Tomcat ServerFile → Settings → Build, Execution, Deployment → Application Servers点击选Tomcat Server指向D:\tomcat85Run → Edit Configurations → → Tomcat Server → Local在Deployment选项卡中点击→Artifact→ 选中你的项目名如travel-system:war exploded关键一步修正Artifact输出路径File → Project Structure → Artifacts选中你的war exploded项在Output Layout右侧确认WEB-INF/classes目录下已包含所有*.class文件即src/main/java编译结果若为空点击 → Directory Content → src/main/java再点OK最后点Apply重启IDEA。此时运行http://localhost:8080/应看到Tomcat默认首页访问http://localhost:8080/hello假设你写了HelloServlet则返回Hello from Servlet!——这才是真正的最小闭环。2.3 写第一个Servlet不用注解手写web.xml理解容器初始化逻辑很多教程一上来就用WebServlet(/hello)但这样你会错过Servlet生命周期的关键钩子。先写XML方式再迁移到注解!-- web/WEB-INF/web.xml -- ?xml version1.0 encodingUTF-8? 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_4_0.xsd version4.0 servlet servlet-nameHelloServlet/servlet-name servlet-classcom.example.HelloServlet/servlet-class load-on-startup1/load-on-startup !-- 容器启动时加载 -- /servlet servlet-mapping servlet-nameHelloServlet/servlet-name url-pattern/hello/url-pattern /servlet-mapping /web-app对应Java类// src/main/java/com/example/HelloServlet.java package com.example; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; public class HelloServlet extends HttpServlet { Override public void init() throws ServletException { System.out.println(【Servlet初始化】HelloServlet已加载); } Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setContentType(text/html;charsetUTF-8); PrintWriter out resp.getWriter(); out.println(h1Hello from Servlet!/h1); out.println(p当前时间 new java.util.Date() /p); out.close(); } Override public void destroy() { System.out.println(【Servlet销毁】HelloServlet被卸载); } }参数说明load-on-startup1/load-on-startup数字越小优先级越高确保应用启动时就实例化避免首次访问延迟resp.setContentType(text/html;charsetUTF-8)必须显式设置否则中文会乱码JSP页面同理out.close()虽非强制但显式关闭流能避免缓冲区残留尤其在调试阶段更易定位输出问题。3. 数据库建模与JDBC直连用纯SQL定义旅游系统核心表手写DAO不靠框架3.1 旅游系统四张核心表设计为什么order_item必须拆分而user要加is_deleted软删除旅游管理系统不是博客或论坛业务实体间存在强关联与状态流转。我们不照搬电商的复杂模型只抓最痛的四个场景用户注册登录、景点信息管理、订单生成、订单明细。对应的表结构必须满足用户能修改手机号但历史订单归属不变 →user表需id主键phone唯一索引同一订单含多个景点门票 → 必须用order_item中间表而非在order表加item_ids逗号字符串违反第一范式无法索引查询景点价格可能调整但历史订单金额不能变 →order_item必须冗余存储当时的price而非只存spot_id管理员误删景点需可恢复 →spot表加is_deleted TINYINT(1) DEFAULT 0查询时加WHERE is_deleted0。建表SQLMySQL 5.7语法-- 用户表 CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 存BCrypt加密后字符串 phone VARCHAR(20) NOT NULL UNIQUE, email VARCHAR(100), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, is_deleted TINYINT(1) DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 景点表 CREATE TABLE spot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, is_deleted TINYINT(1) DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表 CREATE TABLE order_master ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT(1) NOT NULL DEFAULT 0 COMMENT 0待支付,1已支付,2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME NULL, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, spot_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, -- 下单时快照价格 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (order_id) REFERENCES order_master(id), FOREIGN KEY (spot_id) REFERENCES spot(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键设计理由VARCHAR(100)而非TEXT存景点名索引效率更高且名称长度有限DECIMAL(10,2)存价格避免浮点数精度丢失float在金额计算中会出0.01元误差TINYINT(1)代替BOOLEANMySQL 5.7不原生支持布尔类型TINYINT语义清晰且占空间最小所有DATETIME字段加DEFAULT CURRENT_TIMESTAMP省去Java端手动赋值数据库自动维护。3.2 手写JDBC工具类为什么DBUtil必须带连接池而close()要判空直接new Connection()每次创建新连接100并发就耗尽MySQL连接数默认151。必须用连接池但又不能引入HikariCP等第三方——这是练手项目要自己看懂池怎么工作。我们用最简commons-dbcpApache老牌轻量池配合手动回收// src/main/java/com/example/util/DBUtil.java package com.example.util; import org.apache.commons.dbcp.BasicDataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { private static BasicDataSource dataSource; static { dataSource new BasicDataSource(); dataSource.setDriverClassName(com.mysql.jdbc.Driver); // 注意MySQL 5.7用此驱动类 dataSource.setUrl(jdbc:mysql://localhost:3306/travel_db?useUnicodetruecharacterEncodingutf8); dataSource.setUsername(root); dataSource.setPassword(123456); dataSource.setInitialSize(5); // 初始连接数 dataSource.setMaxActive(20); // 最大活跃连接数 dataSource.setMaxIdle(10); // 最大空闲连接数 dataSource.setMinIdle(5); // 最小空闲连接数 dataSource.setRemoveAbandonedOnBorrow(true); dataSource.setRemoveAbandonedOnMaintenance(true); dataSource.setRemoveAbandonedTimeout(60); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn) { if (conn ! null !conn.isClosed()) { try { conn.close(); } catch (SQLException e) { e.printStackTrace(); // 生产环境应记日志此处仅调试 } } } }参数说明setRemoveAbandonedOnBorrow(true)当借连接超时removeAbandonedTimeout60秒自动回收被遗忘的连接防止泄漏setMinIdle(5)始终保持5个空闲连接避免突发请求时创建连接的延迟com.mysql.jdbc.DriverMySQL 5.7必须用此驱动类名MySQL 8.0才用com.mysql.cj.jdbc.Driver混用必报ClassNotFoundException。3.3 手写UserDAO演示PreparedStatement防SQL注入为何executeQuery()后必须rs.next()DAO层不追求复用只求每行代码可知可控。以用户登录为例// src/main/java/com/example/dao/UserDAO.java package com.example.dao; import com.example.entity.User; import com.example.util.DBUtil; import java.sql.*; public class UserDAO { public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, phone, email FROM user WHERE username ? AND password ? AND is_deleted 0; User user null; Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); ps.setString(1, username); // 防注入参数化查询不拼接字符串 ps.setString(2, password); rs ps.executeQuery(); if (rs.next()) { // 关键必须调用next()才能读取第一行否则getXXX()报SQLException user new User(); user.setId(rs.getLong(id)); user.setUsername(rs.getString(username)); user.setPhone(rs.getString(phone)); user.setEmail(rs.getString(email)); } } catch (SQLException e) { e.printStackTrace(); } finally { // 顺序不能错rs → ps → conn if (rs ! null) try { rs.close(); } catch (SQLException e) { } if (ps ! null) try { ps.close(); } catch (SQLException e) { } if (conn ! null) try { conn.close(); } catch (SQLException e) { } } return user; } }避坑点rs.next()必须放在if里判断因为executeQuery()返回的是游标未移动到首行前所有getXXX()都无效finally中关闭资源顺序ResultSet→PreparedStatement→Connection反序可能导致Connection closed异常ps.setString(1, username)中的1是参数位置不是列索引写错位置号如ps.setString(2, username)会导致Parameter index out of range。4. JSP页面开发用原生JSTLEL渲染景点列表解决图片路径、中文乱码、刷新陷阱4.1 JSP基础配置为什么pageEncoding和contentType必须同时设且值要一致很多新手只写% page contentTypetext/html;charsetUTF-8 %结果表单提交中文还是乱码。根本原因是contentType告诉浏览器用UTF-8解码HTMLpageEncoding告诉JSP引擎用UTF-8读取.jsp文件本身若.jsp文件保存为GBKWindows记事本默认而pageEncodingUTF-8引擎读取时就乱码后续所有输出都错。正确写法每页顶部第一行% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html head meta charsetUTF-8 title旅游景点列表/title /head body h2热门景点/h2 c:forEach items${spotList} varspot div h3${spot.name}/h3 p价格¥${spot.price} | 库存${spot.stock}/p a hrefspot_detail.jsp?id${spot.id}查看详情/a /div /c:forEach /body /html验证方法右键浏览器页面 → “查看页面源代码”确认meta charsetUTF-8存在且生效用curl -I http://localhost:8080/spot_list.jsp看响应头Content-Type: text/html;charsetUTF-8。4.2 JSTL遍历与EL表达式${spot.name}为何有时输出null而c:out value${spot.name}/更安全EL表达式${spot.name}在spot为null时直接输出空字符串但若spot对象本身为null${spot.name}会抛NullPointerException导致页面500。而c:out默认做null安全处理%-- 危险写法spot可能为null --% p景点名${spot.name}/p %-- 推荐写法自动转义HTML字符且null时输出空字符串 --% p景点名c:out value${spot.name} default未知景点//p参数说明default未知景点当value为null或空字符串时显示的备选文本escapeXmltrue默认自动将script转成lt;scriptgt;防XSS若需输出原始HTML设escapeXmlfalse但必须确保内容可信。4.3 图片路径与坐标定位为什么img srcimages/spot1.jpg总404而CSS定位要用绝对路径JSP中src路径是相对于当前JSP文件的URL路径不是相对于项目根目录。假设JSP文件在/web/spot_list.jsp图片在/web/images/spot1.jpg则img srcimages/spot1.jpg正确相对路径但若JSP在/web/admin/spot_manage.jsp同样写srcimages/spot1.jpg就变成请求/admin/images/spot1.jpg404。解决方案统一用应用上下文路径Context Path%-- 正确无论JSP在哪层目录都指向/web/images/ --% img src${pageContext.request.contextPath}/images/spot1.jpg alt景点图 %-- CSS中定位图片坐标必须用绝对路径以/开头 --% style .spot-img { background-image: url(%request.getContextPath()%/images/spot_bg.jpg); background-position: 10px 20px; /* X10px, Y20px */ } /stylerequest.getContextPath()返回应用部署路径如/travel-system拼接后生成/travel-system/images/spot1.jpg浏览器能准确定位。5. 避坑指南JSPServletMySQL组合里最常翻车的5个现场附诊断命令5.1 现象启动Tomcat后访问/login.jsp显示404但/helloServlet能正常访问原因JSP文件未放在web目录下或IDEA未将web设为Web资源根目录。诊断查Project Structure → Modules → [你的模块] → Web确认Web resource directory指向src/main/webapp或你实际的web目录查Artifacts → [你的war exploded] → Output Layout确认web文件夹下有login.jsp在Tomcat日志logs/catalina.out中搜索deploying web application看是否打印Deploying web application archive [...]及路径。解决右键web文件夹 →Mark Directory as → Resources Root若用Maven标准结构应为src/main/webapp。5.2 现象登录成功后跳转/index.jsp页面显示HTTP Status 404 – /index.jsp原因response.sendRedirect(/index.jsp)中的/是相对于服务器根路径即http://localhost:8080/而非应用上下文路径。诊断浏览器开发者工具Network标签看跳转请求URL是http://localhost:8080/index.jsp错还是http://localhost:8080/travel-system/index.jsp对。解决// 错误绝对路径跳转 response.sendRedirect(/index.jsp); // 正确相对当前应用路径跳转 response.sendRedirect(request.getContextPath() /index.jsp); // 或用RequestDispatcher服务端转发URL不变 request.getRequestDispatcher(/index.jsp).forward(request, response);5.3 现象MySQL连接报错Communications link failure但mysql -u root -p本地能登录原因MySQL未授权远程访问或防火墙拦截3306端口。诊断在服务器执行netstat -an | findstr :3306Windows或ss -tuln | grep 3306Linux确认MySQL监听0.0.0.0:3306而非127.0.0.1:3306登录MySQL执行SELECT host,user FROM mysql.user WHERE userroot;若host为localhost则只允许本地连接。解决-- 授权root从任意IP连接仅开发环境 GRANT ALL PRIVILEGES ON *.* TO root% IDENTIFIED BY 123456; FLUSH PRIVILEGES;注意生产环境严禁用root%应创建专用账号并限制IP段。5.4 现象JSP页面中文显示为??但数据库里存的是正确汉字原因JDBC URL缺少useUnicodetruecharacterEncodingutf8参数或MySQL服务端字符集非utf8mb4。诊断连接MySQL执行SHOW VARIABLES LIKE character_set%;确认character_set_server和collation_server为utf8mb4检查DBUtil.java中URL是否含?useUnicodetruecharacterEncodingutf8。解决修改MySQL配置文件my.iniWindows或my.cnfLinux[mysqld] character-set-serverutf8mb4 collation-serverutf8mb4_unicode_ci [client] default-character-setutf8mb4重启MySQL重建数据库CREATE DATABASE travel_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.5 现象c:forEach遍历spotList为空但Servlet中System.out.println(spotList.size())输出5原因Servlet未将List存入request.setAttribute(spotList, spotList)或JSP中EL表达式写错变量名。诊断在Servlet中request.setAttribute(spotList, spotList);后加System.out.println(Set attribute: spotList.size());在JSP中加% request.getAttribute(spotList) %看是否输出[com.example.entity.Spotxxx, ...]。解决确保request.setAttribute()在request.getRequestDispatcher().forward()之前调用EL变量名严格区分大小写spotList≠spotlist若用c:forEach items${spotList}确保spotList是request作用域而非session或application。6. 进阶技巧用Filter实现统一登录校验以及JSP页面加载后自动刷新一次的务实方案6.1 用Filter拦截未登录请求比每个Servlet里写if(usernull)更干净把登录校验从每个Servlet里抽出来是迈向工程化的第一步。Filter不侵入业务逻辑且能统一处理编码、日志、权限。// src/main/java/com/example/filter/LoginFilter.java package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginFilter implements Filter { // 不需要登录即可访问的路径 private static final String[] EXCLUDE_PATHS {/login.jsp, /login, /register.jsp, /register}; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getServletPath(); // 检查是否为免登录路径 boolean exclude false; for (String p : EXCLUDE_PATHS) { if (path.equals(p) || path.startsWith(p /)) { exclude true; break; } } if (exclude) { chain.doFilter(req, resp); return; } // 检查Session中是否有user HttpSession session request.getSession(false); Object user (session ! null) ? session.getAttribute(user) : null; if (user null) { // 未登录重定向到登录页并保留原请求URL用于登录后跳转 String originalUrl request.getRequestURL().toString(); if (request.getQueryString() ! null) { originalUrl ? request.getQueryString(); } session.setAttribute(originalUrl, originalUrl); response.sendRedirect(request.getContextPath() /login.jsp); } else { chain.doFilter(req, resp); } } Override public void init(FilterConfig filterConfig) throws ServletException {} Override public void destroy() {} }web.xml注册FilterServlet 3.0可用注解但XML更直观filter filter-nameLoginFilter/filter-name filter-classcom.example.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern !-- 拦截所有请求 -- /filter-mapping优势新增页面无需改Servlet代码只要不在EXCLUDE_PATHS里自动受保护originalUrl保存原请求地址登录成功后可跳回如从/order_list.jsp跳登录登录后回/order_list.jspgetSession(false)避免无谓创建Session提升性能。6.2 JSP页面加载完后刷新一次解决缓存导致的数据陈旧问题非F5硬刷有些场景需要页面首次加载后立即拉取最新数据比如订单列表页刚支付完跳转回来时需刷新状态。用JavaScript控制比服务端重定向更轻量% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8% % // 标记是否已刷新过存于request属性 Boolean refreshed (Boolean) request.getAttribute(refreshed); if (refreshed null || !refreshed) { // 第一次加载设标记并输出JS刷新 request.setAttribute(refreshed, true); % script // 页面加载完成后100ms刷新避免DOM未就绪 window.addEventListener(load, function() { setTimeout(function() { location.reload(); }, 100); }); /script % } % h2我的订单/h2 !-- 此处渲染订单列表 --为什么不用meta http-equivrefresh content0该标签在HTML解析前就触发可能导致页面白屏闪动location.reload()在load事件后执行确保DOM、CSS、JS全部加载完毕用户体验更平滑setTimeout加100ms缓冲避免极少数情况下load事件触发过早。6.3 一个真实血泪经验MySQL日期查询用DATE()函数别信BETWEEN旅游系统常要查“今天下单的订单”。新手常写-- ❌ 错误BETWEEN 2024-05-20 AND 2024-05-20 只匹配create_time2024-05-20 00:00:00 SELECT * FROM order_master WHERE create_time BETWEEN 2024-05-20 AND 2024-05-20; -- ✅ 正确用DATE()提取日期部分 SELECT * FROM order_master WHERE DATE(create_time) 2024-05-20;原理BETWEEN a AND b等价于 a AND b而2024-05-20被MySQL隐式转为2024-05-20 00:00:00所以2024-05-20 14:30:00不满足 2024-05-20 00:00:00。DATE(create_time)则剥离时间部分只比日期。我在某次课程设计答辩时导师故意用下午3点下单测试当场暴露这个Bug。从此养成了习惯所有日期范围查询先EXPLAIN看是否走索引——DATE(create_time)在MySQL 5.7中仍能利用create_time索引前提是索引存在而create_time 2024-05-20 AND create_time 2024-05-21是更优写法推荐。希望帮到你。本文还有配套的精品资源点击获取
返回列表