尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从零手搓JavaWeb旅游管理系统:jsp+servlet+mysql实战指南

发布时间:2026/9/24 18:14:45

资讯中心
01
ARTICLE

从零手搓JavaWeb旅游管理系统:jsp+servlet+mysql实战指南

从零手搓JavaWeb旅游管理系统:jsp+servlet+mysql实战指南
简介这是一套基于JSP、Servlet与MySQL实现的JavaWeb旅游管理系统完整源码面向正在学习JavaWeb开发、需要课程设计或毕业设计参考的学生与初级开发者。系统采用面向对象编程思想覆盖主界面、用户注册、旅游景点、酒店预订与后台管理五大模块可实现景点信息展示与查询、酒店预订、自驾游路线管理、塞北人文与故事维护、留言板及账号权限管理等功能帮助读者理解前后端交互与数据库设计的完整流程。压缩包共738个文件约8.46MB其中111个jsp页面承载各功能视图415个gif与69个jpg构成景点与酒店图片素材40个css与23个js负责界面样式与交互另含sql脚本、jar依赖及项目配置文件结构清晰便于二次开发。目前已有1111人学习下载适合作为JavaWeb入门到进阶的实战参考可快速掌握Servlet请求处理、JSP页面渲染与MySQL数据持久化的综合应用。1. 从零手搓 JavaWeb 旅游管理系统为什么 jspservletmysql 依然是理解 Web 本质的最短路径很多人第一次接触 JavaWeb 旅游管理系统是被课程设计或毕设逼的。打开 IDE 新建项目发现网上教程要么是 SpringBoot 一把梭要么是远古 Eclipse 截图中间缺了最关键的一环请求从浏览器发出到数据库返回到底经过了哪些类、哪些方法、哪些配置。jspservletmysql 这套组合之所以被反复拿来当教学案例不是因为它先进而是因为它把 Web 的骨架暴露得最彻底。没有自动配置帮你藏起 DispatcherServlet没有 Starter 帮你封装连接池每一个跳转、每一次查询都得自己写。对于想搞懂“一个网页到底怎么跑起来”的人来说这是成本最低的路径。这篇笔记面向能看懂 Java 基础语法、装过 MySQL、但没完整跑通过一个动态网站的人。我会按真实开发顺序从建库建表到页面跳转把每个环节的参数和坑讲清楚最后给出一套能直接复现的代码结构。2. 环境与数据库先把 mysql 8.0 和 JDBC 驱动这关过了2.1 选 MySQL 8.0 还是 5.7驱动类和时区参数的区别旅游管理系统的数据实体很明确用户、景点、线路、订单、评论。这些表用 MySQL 存最顺手。但 MySQL 8.0 和 5.7 在 JavaWeb 里的连接方式有硬性差异选错了启动就报错。对比项MySQL 5.7MySQL 8.0驱动类名com.mysql.jdbc.Drivercom.mysql.cj.jdbc.DriverJDBC URL 必需参数无特殊要求serverTimezoneAsia/Shanghai默认字符集latin1需手动改utf8mb4驱动 jar 包mysql-connector-java-5.1.xmysql-connector-java-8.0.x我一般直接上 8.0因为新装环境默认就是它而且 utf8mb4 能存 emoji评论区的表情不会变成问号。但要注意8.0 的驱动类名多了cjURL 里不加时区参数会抛The server time zone value ?D1ú±ê×?ê±?? is unrecognized。这个报错看着吓人其实就是时区没配对。建库语句如下字符集必须显式指定CREATE DATABASE tourism_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE tourism_db; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE scenic ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, city VARCHAR(50), price DECIMAL(10,2) DEFAULT 0.00, description TEXT, image_url VARCHAR(255) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT, scenic_id INT, quantity INT DEFAULT 1, total_price DECIMAL(10,2), order_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (scenic_id) REFERENCES scenic(id) );price用DECIMAL(10,2)而不是FLOAT因为金额计算不能有精度丢失。create_time和order_time设默认值CURRENT_TIMESTAMP插入时不用手动传时间。外键约束在开发阶段建议加上能帮你提前发现脏数据但上线前如果并发高可以考虑去掉外键改由应用层保证这是后话。2.2 JDBC 连接封装为什么不能每次 new Connection新手最容易犯的错是在每个 Servlet 里写DriverManager.getConnection()。这样每次请求都新建物理连接MySQL 默认最大连接数 151压测几轮就Too many connections。正确做法是抽一个工具类用静态代码块加载驱动并提供获取和关闭连接的方法。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/tourism_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }URL 里的useSSLfalse在开发环境加上避免 SSL 握手警告characterEncodingutf8保证中文不乱码。close方法按 ResultSet → PreparedStatement → Connection 的顺序关闭反过来关会报错。这个工具类不是连接池只是避免了驱动重复加载真正的连接池后面章节会讲。提示如果启动报Public Key Retrieval is not allowed在 URL 后追加allowPublicKeyRetrievaltrue。这是 MySQL 8.0 的 caching_sha2_password 认证插件导致的开发环境加上即可。3. Servlet 与 JSP 分工按 Model2 思想把旅游管理系统的请求链路跑通3.1 为什么 JSP 里不写 JDBC 代码Model2 的职责边界早期教程喜欢在 JSP 里直接写% Class.forName... %这叫 Model1。页面里混着 HTML、Java 逻辑和 SQL改一个字段要翻三个文件。Model2 的核心就一句话JSP 只负责展示Servlet 负责接收请求和调度JavaBean 负责数据承载DAO 负责数据库操作。以旅游管理系统的景点列表为例完整链路是浏览器访问/scenicListScenicListServlet的doGet被调用Servlet 调用ScenicDao.findAll()拿到ListScenic把 list 存入request.setAttribute(scenicList, list)request.getRequestDispatcher(/scenic_list.jsp).forward(request, response)JSP 用 JSTL 的c:forEach遍历展示。这样 Servlet 里没有一行 HTMLJSP 里没有一行 SQL。改页面样式不动 Java改查询逻辑不动页面。3.2 一个完整的 Servlet从参数接收到转发下面以“添加景点”为例展示 Servlet 的标准写法。注意编码设置必须在获取参数之前。WebServlet(/addScenic) public class AddScenicServlet extends HttpServlet { private ScenicDao scenicDao new ScenicDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); String name req.getParameter(name); String city req.getParameter(city); String priceStr req.getParameter(price); String description req.getParameter(description); if (name null || name.trim().isEmpty()) { req.setAttribute(msg, 景点名称不能为空); req.getRequestDispatcher(/add_scenic.jsp).forward(req, resp); return; } BigDecimal price new BigDecimal(0); try { price new BigDecimal(priceStr); } catch (NumberFormatException e) { req.setAttribute(msg, 价格格式不正确); req.getRequestDispatcher(/add_scenic.jsp).forward(req, resp); return; } Scenic scenic new Scenic(); scenic.setName(name); scenic.setCity(city); scenic.setPrice(price); scenic.setDescription(description); boolean success scenicDao.insert(scenic); if (success) { resp.sendRedirect(req.getContextPath() /scenicList); } else { req.setAttribute(msg, 添加失败请重试); req.getRequestDispatcher(/add_scenic.jsp).forward(req, resp); } } }req.setCharacterEncoding(UTF-8)只对 POST 请求体生效GET 请求的中文乱码要在 Tomcat 的server.xml里配URIEncodingUTF-8。sendRedirect和forward的区别前者是浏览器发起新请求地址栏变request 域数据丢失后者是服务器内部跳转地址栏不变request 域数据保留。添加成功后用重定向防止刷新页面重复提交。3.3 DAO 层用 PreparedStatement防注入和参数占位DAO 层只做数据库操作返回 JavaBean 或集合。下面以查询和插入为例。public class ScenicDao { public ListScenic findAll() { ListScenic list new ArrayList(); String sql SELECT id, name, city, price, description, image_url FROM scenic ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { Scenic s new Scenic(); s.setId(rs.getInt(id)); s.setName(rs.getString(name)); s.setCity(rs.getString(city)); s.setPrice(rs.getBigDecimal(price)); s.setDescription(rs.getString(description)); s.setImageUrl(rs.getString(image_url)); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; } public boolean insert(Scenic scenic) { String sql INSERT INTO scenic(name, city, price, description, image_url) VALUES(?, ?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, scenic.getName()); ps.setString(2, scenic.getCity()); ps.setBigDecimal(3, scenic.getPrice()); ps.setString(4, scenic.getDescription()); ps.setString(5, scenic.getImageUrl()); return ps.executeUpdate() 0; } catch (SQLException e) { e.printStackTrace(); return false; } } }用PreparedStatement而不是Statement拼接字符串因为 OR 11这种输入会被当成参数值而不是 SQL 语法。try-with-resources自动关闭资源比手动 finally 简洁。ORDER BY id DESC让最新添加的景点排前面符合管理后台的使用习惯。3.4 JSP 页面用 JSTL 遍历把 Java 代码从视图里清出去JSP 里引入 JSTL 标签库用c:forEach替代% for(...) %。需要在pom.xml或WEB-INF/lib下加入jstl-1.2.jar和standard-1.1.2.jar。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle景点列表/title/head body h2旅游景点管理/h2 a hrefadd_scenic.jsp添加景点/a table border1 cellpadding8 tr th编号/thth名称/thth城市/thth价格/thth操作/th /tr c:forEach items${scenicList} vars tr td${s.id}/td td${s.name}/td td${s.city}/td td${s.price}/td td a hrefeditScenic?id${s.id}编辑/a a hrefdeleteScenic?id${s.id} onclickreturn confirm(确认删除)删除/a /td /tr /c:forEach /table /body /html${scenicList}对应 Servlet 里request.setAttribute(scenicList, list)的 key。vars是循环变量${s.name}调用的是getName()方法。删除链接加onclick确认框避免误操作。JSTL 的c:forEach比脚本片段更安全因为它在编译期就检查标签闭合。4. 避坑排查旅游管理系统从启动到查询的 5 个血泪现场4.1 现象404 错误Servlet 路径明明写了却访问不到原因通常有三种WebServlet(/scenicList)的路径和浏览器地址不一致web.xml的metadata-completetrue导致注解被忽略项目没有部署到 Tomcat 的 webapps 下而是用了 IDE 的虚拟路径但配置错误。解决先看 Tomcat 启动日志里有没有Deployment of web application archive这行确认项目已部署。再检查WebServlet的 value 是否以/开头。如果用了web.xml确认servlet-mapping的url-pattern和访问路径完全匹配。IDEA 里重点看 Run/Debug Configurations 的 Deployment 标签Application context 是否设成了/。4.2 现象中文乱码数据库里存的是问号原因POST 请求没设setCharacterEncoding或者数据库连接 URL 没加characterEncodingutf8或者建表时字符集是 latin1。解决三处同时检查。Servlet 第一行req.setCharacterEncoding(UTF-8)JDBC URL 加characterEncodingutf8建库建表用utf8mb4。如果是 GET 请求在 Tomcat 的conf/server.xml里找到Connector标签加URIEncodingUTF-8。4.3 现象ClassNotFoundException: com.mysql.cj.jdbc.Driver原因驱动 jar 包没放进WEB-INF/lib目录或者 IDEA 里虽然加了库但没勾选打包到 Artifact。解决确认WEB-INF/lib下有mysql-connector-java-8.0.x.jar。IDEA 里 File → Project Structure → Artifacts → 选中项目 → 右侧 Output Layout → 确认 Available Elements 里的 MySQL 驱动已双击添加到WEB-INF/lib下。改完重新 Build Artifact。4.4 现象java.sql.SQLException: Access denied for user rootlocalhost原因密码错了或者 MySQL 8.0 的 root 用户默认只允许localhost连接而项目里写的是127.0.0.1。解决先用命令行mysql -u root -p确认密码能登录。如果项目 URL 写的是127.0.0.1改成localhost或者执行CREATE USER root127.0.0.1 IDENTIFIED BY 密码; GRANT ALL ON tourism_db.* TO root127.0.0.1;。开发环境图省事直接用localhost最稳。4.5 现象查询结果为空但数据库里明明有数据原因ResultSet的列名和rs.getString(name)里的字符串不匹配或者while(rs.next())之前已经调用过一次next()。解决列名必须和 SQL 查询里的字段名或别名完全一致大小写不敏感但拼写不能错。如果 SQL 写了SELECT name AS scenic_name那rs.getString里就得用scenic_name。另外检查if(rs.next())和while(rs.next())是否混用if只取第一行while才遍历全部。5. 从能跑到好用连接池、分页与订单事务的进阶收尾5.1 用 Druid 连接池替换 DriverManager前面的DBUtil每次请求都新建连接并发一高就崩。生产环境必须用连接池。Druid 是常见选择配置如下# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/tourism_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000public class DruidUtil { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(DruidUtil.class.getClassLoader().getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException(Druid连接池初始化失败, e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }initialSize5表示启动时建 5 个连接maxActive20是最大并发连接数maxWait3000是获取连接的超时毫秒数。DAO 层把DBUtil.getConnection()换成DruidUtil.getConnection()即可其他代码不动。5.2 订单表的分页查询用 LIMIT 而不是全量查旅游管理系统的订单列表数据量大必须分页。MySQL 用LIMIT offset, size。public ListOrder findByPage(int page, int size) { ListOrder list new ArrayList(); String sql SELECT o.id, o.quantity, o.total_price, o.order_time, u.username, s.name AS scenic_name FROM orders o JOIN user u ON o.user_id u.id JOIN scenic s ON o.scenic_id s.id ORDER BY o.order_time DESC LIMIT ?, ?; try (Connection conn DruidUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); ps.setInt(2, size); ResultSet rs ps.executeQuery(); while (rs.next()) { Order o new Order(); o.setId(rs.getInt(id)); o.setQuantity(rs.getInt(quantity)); o.setTotalPrice(rs.getBigDecimal(total_price)); o.setOrderTime(rs.getTimestamp(order_time)); o.setUsername(rs.getString(username)); o.setScenicName(rs.getString(scenic_name)); list.add(o); } } catch (SQLException e) { e.printStackTrace(); } return list; }(page - 1) * size是偏移量page 从 1 开始。JOIN 查询把用户名和景点名一起取出来避免在 JSP 里再查一次。rs.getTimestamp对应DATETIME类型转成java.sql.Timestamp后可以直接格式化。5.3 下单事务库存扣减和订单插入必须同生共死用户下单时要做两件事插入订单记录、扣减景点库存如果有限量。这两步必须在一个事务里。public boolean createOrder(int userId, int scenicId, int quantity) { Connection conn null; try { conn DruidUtil.getConnection(); conn.setAutoCommit(false); String insertOrder INSERT INTO orders(user_id, scenic_id, quantity, total_price) SELECT ?, ?, ?, price * ? FROM scenic WHERE id ?; try (PreparedStatement ps conn.prepareStatement(insertOrder)) { ps.setInt(1, userId); ps.setInt(2, scenicId); ps.setInt(3, quantity); ps.setInt(4, quantity); ps.setInt(5, scenicId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }setAutoCommit(false)开启事务commit()提交rollback()回滚。INSERT ... SELECT一条语句完成价格计算和插入避免先查价格再插入的竞态。finally 里恢复自动提交再关闭连接否则连接池回收后状态会乱。5.4 一个验证技巧用日志确认 SQL 真的执行了Druid 内置了 StatFilter可以在druid.properties里加filtersstat然后访问/druid/index.html查看 SQL 执行统计。更简单的方式是在 DAO 里加一行System.out.println(sql)确认拼接后的 SQL 和参数符合预期。我习惯在PreparedStatement执行前打印ps.toString()它会输出带参数的完整 SQL比只看问号直观得多。这套 jspservletmysql 的旅游管理系统代码量不大但把请求流转、数据库连接、事务边界、页面渲染串了一遍。跑通之后再看 SpringBoot会发现它只是把DBUtil换成了自动配置把WebServlet换成了RestController底层逻辑没变。我自己的习惯是每学一个新框架先想清楚它替我省了哪一步手工代码这样不容易被注解淹没。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。