
简介这是一套基于Java Web技术实现的JSP机票预订系统完整源代码面向Java Web初学者、课程设计学生及需要实战案例的开发者用于模拟在线机票预订全流程帮助理解用户认证、会话管理与数据库操作等核心环节。压缩包共1644个文件约38.59MB以gif、png、jpg等图片资源和js、css前端文件为主辅以jar依赖库、xml配置、jsp页面、java源码与class编译文件并包含sql脚本与db数据库文件覆盖界面展示、业务逻辑与数据访问各层。已有157人学习下载。项目遵循MVC架构Servlet负责请求处理JavaBean封装数据DAO完成增删改查JSP页面结合EL表达式与动作元素实现动态内容目录中WEB-INF、jsp、css、js、images、classes、lib等模块划分清晰。读者可借此掌握JSP与Java构建完整Web应用的方法理解项目结构组织及与前端技术协作方式是难得的实践参考案例。1. 拿到一份 jsp 机票预订系统源代码先别急着改数据库密码很多人的第一反应是解压、找application.properties或者db.properties把数据库连接改成自己的然后mvn tomcat7:run跑起来。能跑通当然好但如果你打算拿它做课程设计、二次开发或者交付给客户光跑起来远远不够。jsp 机票预订系统源代码这类项目本质是一个典型的 Java Web 单体应用JSP 负责视图渲染Servlet 或 Spring MVC 处理请求JDBC 或 MyBatis 操作数据库核心业务围绕航班查询、座位库存、订单生成和支付状态流转展开。它适合三类人一是需要快速交付一个可演示系统的学生或初级开发者二是想拿一个完整业务闭环练手 Java Web 全链路的人三是需要在此基础上做定制化改造的外包开发者。这一章不写代码先把这份源代码里真正值钱的部分和最容易翻车的地方说清楚后面几章再逐层拆解怎么跑通、怎么改、怎么避坑。2. 先看清 jsp 机票预订系统的技术栈与目录结构拿到压缩包后不要直接导入 IDE 就开干。先花十分钟把目录结构和依赖关系摸清楚这决定了你后面改代码的效率。常见做法是先看根目录有没有pom.xml或build.gradle再看WEB-INF下有没有web.xml最后看src目录的包命名。这一步做扎实后面能省掉大量“为什么 404”的排查时间。2.1 从 pom.xml 和 web.xml 反推技术选型打开pom.xml重点看三块Servlet API 版本、数据库驱动、以及有没有引入 Spring 或 Struts。如果pom.xml里只有javax.servlet-api和mysql-connector-java说明这是一个原生 Servlet JSP 的项目业务逻辑大概率写在 Servlet 里SQL 直接拼在 Java 代码中。如果看到spring-webmvc和mybatis-spring那就是 SSM 架构配置分散在applicationContext.xml和spring-mvc.xml里。!-- 典型原生 Servlet 项目的 pom.xml 依赖片段 -- dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies这段依赖说明项目用 JSTL 做页面标签渲染MySQL 做持久化Servlet 容器负责请求分发。scope为provided表示 Servlet API 由 Tomcat 提供打包时不会重复引入。如果你把provided改成默认的compile部署到 Tomcat 时可能因为版本冲突导致ClassNotFoundException。再看web.xml重点关注welcome-file-list和servlet-mapping。欢迎页决定你访问根路径时跳转到哪个 JSP映射决定哪些 URL 由哪个 Servlet 处理。如果web.xml里配置了*.do或*.action的拦截说明请求走了前端控制器模式你需要找到对应的DispatcherServlet配置。2.2 数据库脚本与实体类的对应关系源代码里通常会带一个.sql文件位置可能在src/main/resources或项目根目录的db文件夹下。先执行这个脚本再对照实体类看字段映射。机票预订系统的核心表一般包括user用户、flight航班、seat_inventory座位库存、order订单、payment支付记录。表名关键字段业务含义flightflight_no, dep_city, arr_city, dep_time, price航班基础信息seat_inventoryflight_id, seat_class, total, remaining座位库存区分舱位orderorder_no, user_id, flight_id, status, create_time订单主表status 控制流转paymentorder_id, pay_amount, pay_time, pay_status支付流水与订单一对一实体类里如果用了Integer而不是int表示价格说明作者考虑了 null 值如果create_time在实体类里是String而不是Date那 SQL 里大概率用了DATE_FORMAT做格式化改代码时要注意类型转换。我一般会先跑一遍SELECT * FROM flight LIMIT 10确认数据量和字段值再决定要不要改查询逻辑。2.3 用 Maven 命令快速验证项目能否编译在改任何配置之前先执行一次编译确认源代码本身没有语法错误或缺失依赖。# 进入项目根目录执行清理并编译 mvn clean compile -DskipTests # 如果编译通过再打包成 war mvn package -DskipTestsclean清除历史构建产物compile只编译不打包-DskipTests跳过测试用例避免因为测试环境缺少数据库连接而中断。如果编译报错package javax.servlet does not exist说明 Servlet API 依赖没引入或 scope 不对。如果报错程序包 com.mysql.cj.jdbc 不存在检查 MySQL 驱动版本是否与代码中的Class.forName一致。编译通过后target目录下会生成war包直接丢进 Tomcat 的webapps目录就能部署。3. 把 jsp 机票预订系统在本地跑起来的最小步骤编译通过只是第一步真正跑起来还要过数据库、Tomcat 版本和页面路径三关。这一章按顺序把每一步的命令和配置写清楚照着做基本能复现。3.1 数据库初始化与连接参数修改先创建数据库再导入 SQL 脚本。注意字符集用utf8mb4否则中文城市名会乱码。-- 创建数据库并指定字符集 CREATE DATABASE flight_booking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 导入表结构和初始数据在命令行执行 -- mysql -u root -p flight_booking /path/to/flight_booking.sql导入完成后找到项目里的数据库配置文件。原生 Servlet 项目通常在src下有一个db.properties或DBUtil.javaSSM 项目则在jdbc.properties里。# db.properties 典型配置 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/flight_booking?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_passwordserverTimezone必须设置否则 MySQL 8 会报时区错误。useUnicode和characterEncoding保证中文正常读写。改完配置后不要急着启动 Tomcat先用一个简单的 JDBC 测试类验证连接。// 快速验证数据库连接是否可用 public class DBTest { public static void main(String[] args) throws Exception { Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection( jdbc:mysql://localhost:3306/flight_booking?serverTimezoneAsia/Shanghai, root, your_password); System.out.println(连接成功 conn.getMetaData().getURL()); conn.close(); } }如果这里报Access denied检查用户名密码如果报Unknown database说明数据库没创建成功如果报Public Key Retrieval is not allowed在 URL 后面加allowPublicKeyRetrievaltrue。3.2 Tomcat 版本选择与 war 包部署JSP 项目对 Tomcat 版本敏感。如果web.xml声明的是 Servlet 3.1用 Tomcat 9 没问题如果声明的是 Servlet 4.0Tomcat 9 也支持但 Tomcat 10 把javax.servlet改成了jakarta.servlet老项目直接部署会报ClassNotFoundException。我一般会先看web.xml的version属性再决定用哪个 Tomcat。# 查看 web.xml 的 version 属性 grep -o version[0-9.]* web.xml # 如果输出 version4.0用 Tomcat 9 # 如果输出 version3.1Tomcat 8.5 或 9 都可以部署时把target下生成的war包复制到 Tomcat 的webapps目录启动 Tomcat。# 假设 Tomcat 安装在 /opt/tomcat9 cp target/flight-booking.war /opt/tomcat9/webapps/ /opt/tomcat9/bin/startup.sh # 查看启动日志确认 war 包解压和部署成功 tail -f /opt/tomcat9/logs/catalina.out日志里出现Deployment of web application archive ... has finished in ... ms表示部署成功。如果出现SEVERE: Error deploying web application archive往下翻看具体异常常见原因是数据库驱动没放进WEB-INF/lib或者web.xml配置了不存在的 Servlet 类。3.3 访问路径与静态资源映射排查部署成功后访问http://localhost:8080/flight-booking/。如果 404先确认web.xml里的welcome-file是否指向一个存在的 JSP 文件。如果页面能打开但 CSS 和图片加载失败检查 JSP 里引用静态资源的路径是不是用了相对路径。!-- 不推荐相对路径在转发后容易失效 -- link relstylesheet hrefcss/style.css !-- 推荐用 JSTL 的 c:url 或 contextPath -- link relstylesheet href${pageContext.request.contextPath}/css/style.css${pageContext.request.contextPath}会动态获取应用上下文路径无论你部署成flight-booking还是ROOT资源都能正确加载。如果项目里大量用了相对路径部署后页面样式全丢这就是血泪经验——改路径比改业务逻辑还费时间。4. 机票预订核心业务链路的代码走读与改造点跑通之后真正要动的是业务代码。机票预订的核心链路是查询航班 → 选择舱位 → 锁定库存 → 生成订单 → 支付回调 → 更新订单状态。这一章按链路顺序拆解每个环节的代码位置和改造注意事项。4.1 航班查询与座位库存的并发扣减航班查询通常是一个FlightServlet或FlightController接收出发城市、到达城市和日期调用 DAO 层查询flight表。这部分逻辑简单真正容易出问题的是座位库存扣减。// 库存扣减的典型错误写法先查再减并发下会超卖 public boolean deductSeat(int flightId, String seatClass) { int remaining seatDao.queryRemaining(flightId, seatClass); if (remaining 0) { seatDao.updateRemaining(flightId, seatClass, remaining - 1); return true; } return false; }上面这段代码在并发场景下会超卖两个线程同时查到remaining1都判断通过然后都执行减一最终库存变成 -1。正确做法是用数据库行锁或乐观锁。// 推荐用 SQL 的原子更新WHERE 条件保证库存充足才扣减 public boolean deductSeat(int flightId, String seatClass) { String sql UPDATE seat_inventory SET remaining remaining - 1 WHERE flight_id ? AND seat_class ? AND remaining 0; int affected jdbcTemplate.update(sql, flightId, seatClass); return affected 0; }affected 0表示扣减成功否则说明库存不足。这种方式依赖数据库的行锁不需要在 Java 层加 synchronized性能和可靠性都更好。改造时注意如果项目用的是 MyBatis把这条 SQL 写在 Mapper XML 里update标签的parameterType用map或自定义 DTO。4.2 订单生成与支付状态流转订单生成一般分两步先插入order主表拿到order_id再插入payment流水。如果项目里用了事务确保这两步在同一个Transactional方法里。Transactional public Order createOrder(int userId, int flightId, String seatClass) { // 1. 扣减库存 boolean deducted seatService.deductSeat(flightId, seatClass); if (!deducted) { throw new RuntimeException(库存不足); } // 2. 插入订单 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setFlightId(flightId); order.setStatus(PENDING_PAYMENT); orderMapper.insert(order); // 3. 插入支付流水 Payment payment new Payment(); payment.setOrderId(order.getId()); payment.setPayStatus(UNPAID); paymentMapper.insert(payment); return order; }Transactional保证扣库存和插订单要么都成功要么都回滚。如果项目没有用 Spring 事务检查DBUtil里有没有手动setAutoCommit(false)和commit/rollback。支付回调通常是一个PayCallbackServlet接收第三方支付平台的异步通知验证签名后把order.status更新为PAID同时更新payment.pay_status。改造时注意回调接口必须做幂等同一笔订单重复回调不能重复更新状态。4.3 前端 JSP 页面与后端 Servlet 的数据传递JSP 页面通过 EL 表达式和 JSTL 标签展示后端传来的数据。Servlet 里用request.setAttribute存数据然后forward到 JSP。// Servlet 中查询航班列表并转发到 JSP ListFlight flights flightService.search(depCity, arrCity, depDate); request.setAttribute(flights, flights); request.getRequestDispatcher(/WEB-INF/jsp/flight-list.jsp).forward(request, response);!-- flight-list.jsp 中遍历展示 -- c:forEach items${flights} varf tr td${f.flightNo}/td td${f.depCity} - ${f.arrCity}/td td${f.depTime}/td td${f.price}/td tda hrefbook?flightId${f.id}预订/a/td /tr /c:forEach如果页面显示空白先检查request.setAttribute的 key 和 JSP 里${}的变量名是否一致。如果显示javax.el.PropertyNotFoundException说明实体类缺少对应的 getter 方法。JSP 放在WEB-INF下是常见做法这样不能通过 URL 直接访问必须经过 Servlet 转发安全性更好。5. 改造 jsp 机票预订系统时最容易翻车的 5 个坑这一章按「现象 → 原因 → 解决」记录我踩过的坑每个都对应真实排查过程。5.1 中文乱码从数据库到页面的全链路排查现象航班列表里城市名显示为????或æµ·å—。原因乱码可能出现在三个环节——数据库字符集、JDBC 连接编码、JSP 页面编码。任何一环不是utf8或utf8mb4都会导致中文异常。解决数据库用utf8mb4JDBC URL 加useUnicodetruecharacterEncodingutf8JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %如果用了 Maven 打包在pom.xml里配置project.build.sourceEncoding为UTF-8。四个地方都检查一遍缺一不可。5.2 数据库连接池耗尽连接没关的连锁反应现象系统运行一段时间后报Cannot get a connection, pool exhausted页面全部超时。原因DAO 层代码里Connection、Statement、ResultSet没有在finally块里关闭每次请求泄漏一个连接连接池很快被占满。解决把所有 JDBC 操作改成 try-with-resources 写法。// 推荐try-with-resources 自动关闭 try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { // 处理结果 } } catch (SQLException e) { logger.error(查询失败, e); }如果项目用了 MyBatis检查SqlSession有没有在finally里close()。连接池配置里maxActive不要设得太大一般 20 到 50 足够设太大反而容易把数据库拖垮。5.3 订单重复提交刷新页面就多一笔订单现象用户在支付页面按 F5 刷新订单表里多出一条重复记录。原因订单生成的 Servlet 没有做防重提交每次请求都执行插入。解决用 Token 机制或重定向。在进入订单确认页时生成一个唯一 Token 存入 Session提交时校验并删除 Token校验不通过就拒绝。或者生成订单后直接response.sendRedirect到订单详情页避免刷新时重复提交表单。5.4 日期格式转换异常String 和 Date 的边界现象查询航班时报java.text.ParseException: Unparseable date。原因前端传过来的日期是2025-01-15后端用SimpleDateFormat(yyyy-MM-dd HH:mm:ss)解析格式不匹配。解决统一日期格式。查询条件用yyyy-MM-dd数据库存储用datetime实体类用java.util.Date或LocalDateTime。如果项目是 SSM在实体类字段上加DateTimeFormat(pattern yyyy-MM-dd)和JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)。5.5 部署后静态资源 404路径拼接的隐形错误现象本地 IDE 里跑正常部署到 Tomcat 后 CSS、JS、图片全部 404。原因JSP 里用了相对路径css/style.css部署后应用上下文变成/flight-booking浏览器请求的是/flight-booking/css/style.css但实际文件在/flight-booking/static/css/style.css。解决所有静态资源引用统一加${pageContext.request.contextPath}。如果项目用了 Spring MVC 的mvc:resources确认mapping和location配置正确。改完后清浏览器缓存再测试避免旧缓存干扰。6. 用 SQL 慢查询日志反推机票查询接口的性能瓶颈系统跑通、业务改完之后真正决定用户体验的是查询性能。机票预订系统最慢的接口通常是航班搜索因为它涉及多条件过滤、排序和分页。这一章不讲索引原理只讲怎么用 MySQL 慢查询日志定位问题再给出具体的优化动作。先确认慢查询日志是否开启。-- 查看慢查询相关配置 SHOW VARIABLES LIKE slow_query_log; SHOW VARIABLES LIKE long_query_time; SHOW VARIABLES LIKE slow_query_log_file; -- 如果未开启临时开启重启失效 SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;long_query_time 1表示执行超过 1 秒的 SQL 会被记录。跑几轮航班搜索后用mysqldumpslow或直接看日志文件找出耗时最长的 SQL。# 统计慢查询日志中出现次数最多的 SQL mysqldumpslow -s c -t 10 /var/log/mysql/slow.log # 按平均执行时间排序 mysqldumpslow -s at -t 10 /var/log/mysql/slow.log假设发现航班搜索的 SQL 是全表扫描SELECT * FROM flight WHERE dep_city 北京 AND arr_city 上海 AND dep_time BETWEEN 2025-01-15 00:00:00 AND 2025-01-15 23:59:59 ORDER BY price ASC LIMIT 0, 10;用EXPLAIN看执行计划。EXPLAIN SELECT * FROM flight WHERE dep_city 北京 AND arr_city 上海 AND dep_time BETWEEN 2025-01-15 00:00:00 AND 2025-01-15 23:59:59 ORDER BY price ASC LIMIT 0, 10;如果type是ALLrows很大说明没走索引。建一个联合索引ALTER TABLE flight ADD INDEX idx_route_time_price (dep_city, arr_city, dep_time, price);联合索引的字段顺序很关键等值查询字段放前面范围查询字段放后面排序字段放最后。dep_city和arr_city是等值dep_time是范围price是排序这个顺序能让WHERE和ORDER BY都走索引。建完索引再EXPLAINtype应该变成range或refrows大幅下降。还有一个容易被忽略的点分页查询的LIMIT偏移量越大越慢。如果用户翻到第 100 页LIMIT 990, 10会扫描前 1000 行再丢弃。优化方式是先用覆盖索引查出主键再回表。-- 优化前扫描大量无用行 SELECT * FROM flight WHERE dep_city 北京 ORDER BY dep_time LIMIT 990, 10; -- 优化后先用索引查主键再关联取完整行 SELECT f.* FROM flight f INNER JOIN ( SELECT id FROM flight WHERE dep_city 北京 ORDER BY dep_time LIMIT 990, 10 ) AS tmp ON f.id tmp.id;子查询里的SELECT id走覆盖索引不需要回表速度比直接SELECT *快很多。这个技巧在订单列表、用户列表等分页场景同样适用。最后说一个我自己的习惯每次改完 SQL 或索引一定用SHOW STATUS LIKE Handler_read%对比前后变化。Handler_read_rnd_next值越高说明全表扫描越多优化后这个值应该明显下降。不要凭感觉判断优化有没有生效用数据说话。希望帮到你。本文还有配套的精品资源点击获取