ARTICLE DETAIL

资讯详情

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

JSP+Servlet+MySQL实现车票预订系统:从部署到并发事务解析

JSP+Servlet+MySQL实现车票预订系统:从部署到并发事务解析 简介一套完整的基于WEB的车票预订信息系统项目采用Java、Servlet、JSP与MySQL构建覆盖车次查询、在线预订、订单管理等核心业务模块适合Java Web初学者用于毕业设计或课程设计也适合希望系统练习三层架构开发的学习者。资源包为rar格式共包含1337个文件大小约55.82MB其中JSP页面负责动态展示与用户交互Java类及Servlet处理请求和业务逻辑CSS、JavaScript与图片等前端资源用于界面美化SQL脚本用于初始化数据库jar包为运行依赖此外还包含视频讲解及相关配置文件目录结构较清晰。目前已有1104人浏览学习项目热度不错可作为理解Web应用从页面到数据库完整链路的参考案例。通过阅读源码、观看配套视频并实际操作学习者可以掌握MyEclipse/Tomcat/MySQL环境搭建、数据表设计、JDBC数据库访问、Servlet请求处理和JSP页面渲染等关键技能并能直接复用代码完成车票预订系统的演示与答辩节省从零开发的时间。1. 基于WEB的车票预订信息系统JSPServletMySQL 的完整实现路径基于WEB的车票预订信息系统是 Java 后端入行者几乎都绕不开的课设题正好卡在 Servlet、JSP、JDBC 与 MySQL 的交互位置。我拆过不少网上下载的 web 项目表面完整、真能跑起来的并不多这个 rar 包属于能用的那一类管理员端管车次和订单用户端查余票、在线预订数据走 MySQL没有花哨框架适合当课设底子或练手模板。玩过 Spring Boot 的可以看传统 JSP 的组织方式第一次做课设的可以完整复现。它解决的不是发明轮子而是把轮子装到车上。新手能看清 Servlet 怎么接住表单熟手能快速定位事务和并发扣库存的坑。我拿到 rar 包不会急着解压先扫目录、数据库脚本和配置确认技术栈再动手下面就是按这个习惯拆出来的全过程。2. 技术底座拆解为什么是 JSPServlet 而不是 Spring Boot打开 rar 包后第一件事不是看页面而是看它用什么技术栈。这个项目是极简的 JSPServlet 模式JSP 负责渲染页面Servlet 负责接收请求和转发JDBC 直连 MySQL 做增删改查。没有 Spring、没有 MyBatis连 Maven 都没有全靠把 jar 放进 WEB-INF/lib 里。这种风格放在现在显得有些复古但恰恰是它值得拆的原因——它把 Web 应用最原始的“请求-处理-响应”链路完整暴露出来对理解和排查问题都更直接。2.1 选型取舍课设场景里 JSP 仍是性价比之王为什么不用 Spring Boot我从三个角度给你算笔账。第一学习成本Spring Boot 的自动配置和注解体系会让新手把“理解了怎么写”误当成“理解了怎么运行”但 JSPServlet 的每个环节都可见适合答辩时讲清楚请求生命周期。第二部署成本Spring Boot 项目通常要打包成 jar 再配外部 Tomcat或者用内嵌 Tomcat环境变量和版本不匹配就让人折腾半周而传统 JSP 项目直接丢进 Tomcat webapps 就能跑。第三改造空间把 JSP 项目升级到 Spring Boot 是很好的进阶练习反过来则很少见所以从基础项目入手更划算。但它也有明显边界。第一个边界是页面与 Java 代码容易混在一起JSP 里写业务逻辑的情况在这个资源包里也有后期维护比较痛苦第二个边界是 Servlet 默认单实例多线程多线程安全问题要自己留意第三个边界是没有现成的权限框架登录校验得自己写 Filter。这些都是开发者必须知道的“代价”知道了才不会拿到项目后觉得代码写得烂。2.2 数据模型设计用户、车次、订单三张核心表的字段约束这类系统基本跑不出三张核心表我对比过不少车票预订的源项目字段大同小异。下表是这份资源包里最关键的几张表建表语句都在 sql 目录下。表名主要字段作用t_userid, username, password, real_name, id_card, phone用户登录与身份信息t_trainid, train_no, start_station, end_station, start_time, arrive_time, ticket_price, tickets_remaining车次与余票t_orderid, order_no, user_id, train_id, order_time, status预订记录与状态用户表和订单表通过 user_id 关联车次表和订单表通过 train_id 关联。注意 t_train 的 tickets_remaining 字段类型必须是 int 且建议加非负约束这关系到第 5 章要讲的超卖问题。订单号 order_no 在业务上要求唯一所以我在看脚本时确认过它设置了唯一索引如果脚本里没有我会建议手动补上避免重复下单闹出乌龙。除了主外键还有两个细节值得复现时保留。一个是 password 字段这里直接存明文课设能过但换成正式项目必须做 MD5 或 BCrypt 处理另一个是 id_card 长度要预留 18 位电话预留 11 位char 类型配错长度会导致身份证后几位变 0这个坑我在别的项目里见过好几次。2.3 项目目录拿到 rar 后先看这四类文件解压后别急着运行按下面顺序看四类文件能在十分钟内判断这个项目能不能救活。第一类sql 目录或 db 目录里面是建库建表脚本确认它有没有包含预置的演示数据第二类WEB-INF 下的 web.xml看 servlet 映射、欢迎页、过滤器配置很多部署问题都出在 url-pattern 写错第三类源码里的 DBUtil.java 或 JDBC 工具类看数据库连接的 URL、用户名、密码写死在哪里这是运行报错的第一现场第四类JSP 页面根目录下的登录页和首页确认前端入口路径与 web.xml 中配置的欢迎页一致。常见的情况是脚本里有管理员账号但注释里不告诉你我在这个包里发现管理员默认是 admin密码是 123456用户端可以用普通用户账号注册。如果你在 README 里没找到去 t_user 表的 insert 语句里看一眼就能明白。资源包里的 docs 目录一般还有设计文档和答辩 PPT 雏形这些对毕设用处反而比代码大先存着。我特别提醒一点如果 sql 脚本里有 DROP DATABASE 语句执行前想清楚你本机数据库里有没有同名数据库。我一般都会先把脚本里那句 DROP 注释掉防止手滑把别的东西清了这是保命习惯。3. 把 rar 包变成可运行系统环境配置、数据库脚本与 Tomcat 部署这一章解决“我拿到代码但启动不起来”的问题。你会发现绝大多数失败都不是业务代码的问题而是环境与路径配置的排列组合。我先给出版本组合再一步步执行按这个顺序走成功率最高。3.1 版本匹配JDK、Tomcat、MySQL、驱动版本别混搭这个项目的代码是十年前的写法但兼容性并不差。我实测可用的组合如下表组件推荐版本踩坑注意JDK1.8不要用 17部分 JSP 编译会出兼容问题Tomcat8.5.x9.0 也认但 10 及以上要换成 jakarta 命名空间别试MySQL5.7 或 8.08.0 要额外处理时区MySQL 驱动5.7 用 5.1.498.0 用 8.0.28jar 必须里拷进 WEB-INF/lib版本选择有讲究JDK 1.8 和 Tomcat 8.5 是这个项目最稳的搭配因为代码里大量使用了 JDBC 旧 API 和 javax.servlet 包。如果你用 Tomcat 9 也基本没问题但一旦上升到 Tomcat 10Servlet 包名从 javax 改成 jakartatomcat-jdbc 也会变项目里的 import 全部报红那不是改一两个类能解决的。MySQL 8.0 时区问题在驱动连接串里加参数就能绕过详情看 3.2 节。3.2 数据库初始化执行 SQL 脚本并预置演示数据先把脚本导入 MySQL。假设你的脚本文件是 ticket.sql放在 D:\ticket\ 下打开命令行执行mysql -uroot -p source D:/ticket/sql/ticket.sql;输入密码后进入 MySQL 命令行source 命令执行整个建库脚本。注意路径里不要有中文否则在 Windows 命令行下容易找不到文件。执行成功后用show tables;应该看到 t_user、t_train、t_order 等表再用select * from t_user;确认管理员账号已经被插入。如果你不喜欢命令行用 Navicat 或 DataGrip 也可以新建连接后直接运行整个 SQL 文件。这里有个参数容易漏MySQL 8.0 默认字符集可能是 utf8mb4而脚本里建表语句写的可能是 utf8两者混用通常不会立刻报错但之后 JSP 页面写入中文时可能变成问号。所以我习惯在连接 URL 里显式加上 characterEncodingutf8后面第 5 章还会再提。3.3 Tomcat 部署WebContent 目录结构与 war 包两种方式项目里的 WebContent 或 webroot 就是标准 Web 应用根目录。最简单的部署方法是把这个目录复制并改名为 ticket放到 Tomcat 的 webapps 下。例如把 WebContent 改名为 ticket 后整体粘贴启动 Tomcat访问http://localhost:8080/ticket/login.jsp如果资源包里已经打了 war 包更省事把 ticketing.war 丢进 webappsTomcat 启动时自动解压访问路径就是 war 包文件名。启动 Tomcat 用 bin 目录下的 startup.batWindows或 startup.shLinux不要直接双击 catalina.bat。开发调试阶段我更推荐在命令行跑catalina.bat run这样能直接看到所有启动日志和 Java 异常堆栈比看 catalina.log 实时得多。参数方面注意项目访问路径的上下文名war 包名称决定 URL 前缀不要带版本号如 ticket_v1.0.war 会导致 URL 变成 /ticket_v1.0/和数据库脚本里的库名也没有必然关系。部署成功后在浏览器登录管理员账号能进后台管理就是基本通了。如果这一步卡住九成是数据库连接串的问题去排查 DBUtil.java 里 jdbc:mysql://localhost:3306/ticket 的库名、账号、密码是否和本机一致。4. 核心模块逐行拆解车次查询、下单事务与后台管理能跑通只是第一步接下来要看代码。我把资源包里最值得抄的三段逻辑抽出来分别对应前台查询、用户下单、后台管理这三段读懂了整个系统的骨架就清楚了。4.1 车次查询从 JSP 表单到 JDBC 结果集的完整闭环用户流程很简单首页输入出发站和到达站提交到 ServletServlet 调 DAO 查询把结果放到 Request 域转发给 JSP 显示。关键在 DAO 的查询方法这段代码几乎每个车票系统都要写public ListTrain query(String startStation, String endStation) throws SQLException { Connection conn null; PreparedStatement ps null; ResultSet rs null; ListTrain list new ArrayList(); try { conn DBUtil.getConnection(); String sql SELECT * FROM t_train WHERE start_station ? AND end_station ? AND tickets_remaining 0; ps conn.prepareStatement(sql); ps.setString(1, startStation); ps.setString(2, endStation); rs ps.executeQuery(); while (rs.next()) { Train t new Train(); t.setId(rs.getInt(id)); t.setTrainNo(rs.getString(train_no)); t.setStartStation(rs.getString(start_station)); t.setEndStation(rs.getString(end_station)); t.setStartTime(rs.getString(start_time)); t.setArriveTime(rs.getString(arrive_time)); t.setTicketPrice(rs.getBigDecimal(ticket_price)); t.setTicketsRemaining(rs.getInt(tickets_remaining)); list.add(t); } } finally { DBUtil.close(conn, ps, rs); } return list; }这个方法的逻辑说明用 PreparedStatement 防止 SQL 注入这是老项目里最值得保留的好习惯查询条件带tickets_remaining 0这一步在 SQL 层就过滤了无余票车次减少后端判断。参数说明?序号从 1 开始类型要和表字段对应比如日期字段虽然在这里用 String 接收但 varchar 类型在 MySQL 里必须写成目标格式YYYY-MM-DD HH:mm:ss否则排序和显示都会出错。表单提交到 Servlet 后要记得处理空参数。常见翻车是用户只填出发站没填到达站SQL 用AND拼接会出现查不出数据我一般会在 DAO 方法里用WHERE 11加动态拼接但这份资源里是固定的两个参数所以 Servlet 里要先做非空判断再决定是否查询。这也是你改造时最容易加功能的地方。4.2 下单流程库存校验与订单写入的并发事务预订操作在单机环境下也有并发风险。典型错误是先查出库存判断大于 0再 update 库存最后 insert 订单。如果两个请求同时读到余票为 1会导致两张订单和库存变成 -1。这个包的处理方式是加同步块和事务public synchronized boolean createOrder(int userId, int trainId, int quantity) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); String lockSql SELECT ticket_price, tickets_remaining FROM t_train WHERE id ? FOR UPDATE; PreparedStatement ps conn.prepareStatement(lockSql); ps.setInt(1, trainId); ResultSet rs ps.executeQuery(); if (!rs.next()) return false; int remaining rs.getInt(tickets_remaining); if (remaining quantity) return false; String updateSql UPDATE t_train SET tickets_remaining tickets_remaining - ? WHERE id ?; ps conn.prepareStatement(updateSql); ps.setInt(1, quantity); ps.setInt(2, trainId); int rows ps.executeUpdate(); String orderNo T System.currentTimeMillis(); String insertSql INSERT INTO t_order(order_no, user_id, train_id, order_time, status) VALUES(?,?,?,NOW(),已支付); ps conn.prepareStatement(insertSql); ps.setString(1, orderNo); ps.setInt(2, userId); ps.setInt(3, trainId); ps.executeUpdate(); conn.commit(); return rows 1; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); DBUtil.close(conn, null, null); } }上面这段逻辑说明setAutoCommit(false)开启事务两条写操作要么都成功要么都回滚SELECT ... FOR UPDATE对车次行加锁防止并发时都读到旧库存synchronized在单实例下又加了一层保险。参数说明quantity是购买张数订单号用时间戳生成在并发量低的课设场景够用但如果要更严谨可以改成yyyyMMddHHmmss加随机数。注意 finally 里恢复setAutoCommit(true)否则连接放回池后会让后续操作一直处于事务中这是很多人容易漏的坑。事务正确但性能不行这是这个方案的副作用。FOR UPDATE会让同一车次的查询都排队实际生产会用乐观锁或 Redis 扣库存。不过对课设答辩来说能把“为什么用行锁”讲清楚反而比空谈分布式更有说服力。4.3 后台管理用 BaseServlet 统一下发车次增删改操作后台管理模块最值得借鉴的是它的 Servlet 分发写法。早期项目经常在 doGet/doPost 里写一长串 if/else 判断 action 参数这个包用了一个 BaseServlet 把公共逻辑抽出来public abstract class BaseServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.trim().isEmpty()) { req.getRequestDispatcher(/admin/error.jsp).forward(req, resp); return; } try { Method m this.getClass().getMethod(action, HttpServletRequest.class, HttpServletResponse.class); m.invoke(this, req, resp); } catch (Exception e) { throw new ServletException(action 分发失败, e); } } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } }逻辑说明通过反射调用子类中和 action 参数同名的方法这样新增一个操作只需在子类写一个方法比如addTrain(req, resp)不用再改 if/else。参数说明action 参数名必须与菜单项一致比如表单里写actionaddTrain子类方法就是public void addTrain(...)大小写要对应。反射会有性能开销但在管理端低并发下无所谓。如果你觉得反射难懂也可以退回 if/else 分发只是后续每加一个功能都要动 doPost维护成本高。具体到车次管理子类一般会实现这些方法listTrain分页查询所有车次addTrain插入新班次updateTrain修改票价和停靠站deleteTrain删除前先检查有没有关联订单。删除这个动作要注意外键约束t_order 里有 trainer 关联时直接删会报错所以正确做法是先查或把 status 置为无效而不是物理删除这也是我在这个包里看到的常见简化写法动手时别直接抄。5. 避坑指南部署车票预订系统最容易踩的四个雷区这一章把我在部署和修改这类项目中反复遇见的四类问题写清楚每条都是现象→原因→解决的格式你遇到类似日志可以直接对照。5.1 数据库连接失败驱动类找不到与 URL 时区报错现象一启动 Tomcat 后访问页面报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因驱动 jar 没有放进 WEB-INF/lib只在项目构建路径里引用了运行时 Tomcat 找不到。解决把 mysql-connector-java-xxx.jar 复制到 WebContent/WEB-INF/lib 下再重启。现象二报Access denied for user rootlocalhost。原因DBUtil.java 里写死的账号密码和你本机没对上。解决改成你自己的数据库密码改完记得重新编译 class不要只改源码不重编。现象三MySQL 8.0 下报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因驱动连接串没带时区参数8.x 驱动强制校验时区。解决把 URL 改成jdbc:mysql://localhost:3306/ticket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。5.2 JSP 中文乱码三处编码必须统一现象页面中文正常但往数据库插入后显示问号或者页面本身出现乱码。原因三处编码不一致——JSP 文件本身的 pageEncoding、请求和响应的字符集、数据库连接 URL 中的编码。解决第一保证所有 JSP 第一行写% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%第二如果自己写 Servlet 接收表单参数在 doGet/doPost 开头加request.setCharacterEncoding(UTF-8)第三JDBC URL 上加上useUnicodetruecharacterEncodingutf8。这三处统一后乱码基本绝迹。不要只改一处我见过只改数据库导致页面乱得更厉害的。5.3 Tomcat 端口冲突或内存溢出环境问题的快速定位现象一启动 Tomcat 报SEVERE: Failed to initialize end point associated with ProtocolHandler ... Address already in use: JVM_Bind null:8080。原因另一个进程占用了 8080常见是之前启动的 Tomcat 没杀干净或者 IDEA/Eclipse 内部启动了实例。解决在 Windows 命令行执行netstat -ano | findstr 8080找到 PID 后taskkill /PID pid /F或者改 Tomcat 的 conf/server.xml 把端口换成 8088。现象二长期部署后访问后台卡死日志出现java.lang.OutOfMemoryError: PermGen space老版本 Tomcat 常见。原因JSP 持续编译导致永久代内存不足本质是启动参数没配。解决在 Tomcat 的 bin/catalina.bat 里JAVA_OPTS-Xms256m -Xmx512m -XX:MaxPermSize256mJDK 8 以上换成 Metaspace再重启。这个坑如果不处理越往后越频繁。5.4 下单库存超卖并发场景下的事务边界错误现象用 JMeter 或两个浏览器同时下同一辆车最后一张票最终订单数多于剩余车票库存变成负数。原因常见实现是先select tickets_remaining判断大于 0再update这个检查与扣减之间不是原子的多个线程都通过了检查。解决按第 4 章事务里的做法先select ... for update再判断或者把 update 语句写成UPDATE t_train SET tickets_remaining tickets_remaining - 1 WHERE id ? AND tickets_remaining 0然后检查executeUpdate()返回的行数为 0 就说明没抢到。这两招在单库单机场景下足够应对答辩。注意不要在 Servlet 里用全局锁去锁整个系统那样所有车次都会被堵住正确粒度是锁单条车次或依赖数据库锁。除了上面四条还有一个容易翻车的点JSP 页面里的超链接和表单 action 路径容易带../或绝对路径导致在改上下文名后 404。我拿到项目都会全局搜一遍href/和action/改成${pageContext.request.contextPath}。这类问题不算隐蔽但出现时排查起来很费时间最好一开始就统一写法。6. 进阶用法用两个过滤器解决乱码与未登录访问让项目更像正式产品基础功能跑通后想让答辩演示更专业先加两个 Filter。一个管编码一个管登录代码量不大但能把系统从课设水平拉到接近可用。6.1 过滤器一统一处理请求与响应编码乱码问题多半是因为每个 Servlet 各写各的 setCharacterEncoding漏写就炸。用一个过滤器全局兜底public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); }在 web.xml 里注册映射到/*所有请求先过编码设置。注意与 JSP 的 pageEncoding 不冲突反而更稳。6.2 过滤器二拦截未登录用户并重定向登录页老项目后台直接输 URL 就能进是通病。加登录过滤器public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); if (uri.contains(login.jsp) || uri.endsWith(.js) || uri.endsWith(.css)) { chain.doFilter(req, resp); return; } Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); }逻辑说明登录页和静态资源放行其余请求检查 session 里的 user为空就重定向。放行规则要配全否则 CSS 被拦页面就裸奔了。这两个过滤器加完系统在演示时基本不会出现后台裸奔或乱码的尴尬。我个人的习惯是拿到新源码先看 web.xml 里有没有它们没有就补上再谈业务改造。有一次项目交付前发现后台接口直接能访问领导手动敲了 URL 进了管理页当场社死。从那以后不管多老的 JSP 项目我都强制走一遍过滤器 事务边界检查再交付。希望帮到你。本文还有配套的精品资源点击获取
返回列表