尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Java Web开发实战:基于Servlet+JSP+JDBC的电影售票系统设计与实现

Java Web开发实战:基于Servlet+JSP+JDBC的电影售票系统设计与实现 简介Java Web开发是构建企业级应用的核心技术栈其底层原理基于HTTP协议与服务器端处理模型。Servlet作为Java EE规范中的核心组件充当HTTP请求的控制器负责接收、处理和响应客户端请求JSP则实现了服务器端动态页面渲染结合JDBC标准接口完成与MySQL等关系型数据库的交互形成完整的数据处理闭环。这套经典技术组合的价值在于帮助开发者深入理解Web应用从请求到响应的完整生命周期掌握MVC架构模式的底层实现逻辑为后续学习Spring等高级框架奠定坚实基础。在典型的电商、管理类应用场景中如电影院售票系统需要处理高并发事务、数据一致性等工程挑战。本文将以数据库连接池优化和事务处理机制为切入点详细解析如何通过Druid连接池提升数据库访问性能并利用JDBC事务保证购票业务的原子性为Java Web初学者提供可落地的项目实践指南。1. 项目概述一个经典技术栈的实战演练“电影院售票管理系统”这个项目标题一出来对于很多Java学习者来说就像一张熟悉的地图。它指向的不是一个前沿的、酷炫的技术组合而是一个经典的、经过时间考验的“Java Web开发”标准套餐Java Servlet JSP JDBC MySQL。这个组合常被戏称为“JSP/Servlet时代”的基石至今仍在许多高校的课程设计、企业遗留系统以及新手入门的实战项目中扮演着核心角色。为什么这个组合如此经典因为它清晰地勾勒出了一个Web应用从用户交互到数据落地的完整闭环。用户通过浏览器看到的动态页面由JSP负责渲染页面提交的请求由Servlet这个“总调度中心”接收和处理处理过程中需要存取的数据则通过JDBC这个“标准接口”与后端的MySQL数据库进行对话。整个流程不依赖任何复杂的框架纯粹使用Java EE现Jakarta EE的基础规范这对于理解Web开发的底层原理至关重要。这个项目适合谁如果你是正在学习Java Web开发的学生或者是从业初期希望夯实基础的开发者那么这个项目就是为你量身定做的。它能帮你彻底弄懂一个Web请求的生命周期理解MVC模式的雏形虽然这里可能比较原始并亲手完成从数据库设计、后端逻辑到前端展示的全过程。通过它你不仅能学会技术更能建立起对系统分层和模块化设计的初步认知。接下来我将以一个“过来人”的身份拆解这个项目的设计思路、实现细节以及那些只有踩过坑才知道的实操要点。2. 项目整体设计与架构思路拆解在动手敲代码之前理清思路比盲目开干重要十倍。我们这个售票系统核心目标很明确模拟电影院前台或用户自助购票的业务流程。这意味着我们需要管理电影信息、放映场次、影厅座位并处理用户的购票、退票等操作。2.1 技术栈选型背后的逻辑为什么是这套组合拳我们逐一分析Java: 作为后端主力语言其强类型、面向对象和丰富的生态是业务逻辑稳健实现的保障。Servlet: 它是Java处理HTTP请求/响应的基础组件。你可以把它理解为一个控制器Controller。所有来自浏览器的请求如“查询今天电影”、“购买A厅3排5座”都会先到达对应的Servlet由它来决定调用哪个业务方法并最终决定返回什么结果给浏览器。在Spring MVC流行之前Servlet就是MVC中的那个“C”。JSP: 它的全称是Java Server Pages本质上是Servlet的“语法糖”。它允许我们在HTML中嵌入Java代码片段% ... %和标签用于动态生成页面内容。在这个项目中JSP主要承担**视图View**的角色用于展示电影列表、座位图、订单信息等。虽然现在更流行前后端分离但学习JSP能让你理解服务器端渲染SSR的基本原理。JDBC: Java Database Connectivity是Java官方定义的数据库连接标准。它是一组接口具体的实现由数据库厂商如MySQL提供的驱动包来完成。它的作用就是建立Java程序与MySQL数据库之间的桥梁执行SQL语句并处理结果。它是数据访问层的核心所有对数据库的增删改查都通过它进行。MySQL: 关系型数据库的选择。对于售票系统我们需要存储结构化的数据如用户、电影、订单并且它们之间存在复杂的关系如一个订单对应多个座位关系型数据库非常合适。MySQL因其开源、易用、性能不错而成为首选。这个架构可以概括为浏览器请求 - Servlet接收并处理 - 调用JavaBean业务逻辑/模型 - 通过JDBC操作MySQL - 处理结果返回给Servlet - Servlet转发给JSP渲染 - HTML返回给浏览器。这是一个典型且直观的Model 1JSPJavaBean或初级Model 2JSPServletJavaBean架构是理解更复杂框架如Spring的绝佳起点。2.2 数据库设计一切的基础数据库设计是项目的基石设计不好后期编码会处处掣肘。根据业务我们至少需要以下几张核心表film(电影表): 存储电影基本信息。id(主键),name,director,actors,duration,type,description,poster_url等。schedule(放映计划表): 这是核心纽带关联电影、影厅和时间。id(主键),film_id(外键关联film.id),hall_id(外键关联hall.id),start_time,end_time,price。hall(影厅表): 存储影厅信息特别是座位布局。id(主键),name,total_seats,seat_layout。这里seat_layout可以是一个文本字段用JSON或特定格式如“5x8”存储行列信息更规范的做法是拆出seat表。seat(座位表): 推荐拆出来更灵活。关联影厅和放映场次。id(主键),hall_id,schedule_id,row_num,col_num,status(如0可用1已售2锁定)。user(用户表): 简单存储用户信息。id(主键),username,password(务必加密存储),phone,email。order(订单表): 记录购票订单。id(主键订单号),user_id,schedule_id,total_amount,status(如0待支付1已支付2已取消),create_time。order_item(订单明细表): 记录一个订单中包含哪些座位。因为一个订单可能买多张票。id(主键),order_id,seat_id。注意这里关于座位的设计有两种常见思路。一是将座位作为schedule的一个属性如一个seat_status文本记录本场次所有座位状态简单但不灵活难以处理并发。二是拆出独立的seat表并关联schedule_id这样每个场次的座位状态独立更利于实现“选座锁定”等高并发场景。对于学习项目我强烈建议采用第二种虽然稍复杂但更贴近实战。2.3 项目包结构规划清晰的包结构能让代码维护性大增。在IDE如IntelliJ IDEA或Eclipse中一个典型的包结构如下com.cinema ├── controller // Servlet类存放处负责请求分发 ├── service // 业务逻辑层接口和实现 │ ├── impl ├── dao // 数据访问层接口和实现封装所有JDBC操作 │ ├── impl ├── model // 实体类JavaBean与数据库表对应 ├── util // 工具类如数据库连接工具、加密工具 ├── filter // 过滤器用于处理编码、权限验证等 └── webapp // Web应用根目录 ├── WEB-INF │ ├── web.xml // 部署描述符 │ └── lib // 依赖jar包如mysql-connector-java ├── css ├── js └── *.jsp // JSP页面这种分层结构Controller - Service - Dao - Model是软件工程中“高内聚、低耦合”思想的体现即使在这个小项目中实践也对培养良好编程习惯大有裨益。3. 核心模块实现与关键技术点解析有了设计图我们就可以开始“施工”了。我会挑几个最核心、最容易出问题的模块带你深入实现细节。3.1 数据库连接池告别原始的DriverManager新手最常写也最忌讳的代码就是在每个Dao方法里这样写Class.forName(com.mysql.cj.jdbc.Driver); Connection conn DriverManager.getConnection(url, user, password); // ... 执行SQL conn.close();这种方式每次操作都建立和关闭物理连接性能极差。在生产环境或严肃项目中必须使用数据库连接池。这里我推荐使用阿里的Druid它功能强大监控齐全。实操步骤添加依赖在项目的WEB-INF/lib目录下放入druid-1.2.x.jar和mysql-connector-java-8.0.x.jar。创建工具类在com.cinema.util包下创建DruidUtils。public class DruidUtils { private static DataSource dataSource; static { try { Properties prop new Properties(); // 从配置文件读取参数更灵活 InputStream is DruidUtils.class.getClassLoader().getResourceAsStream(druid.properties); prop.load(is); dataSource DruidDataSourceFactory.createDataSource(prop); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { // 反向关闭资源 try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } // 这里close是归还连接给连接池 catch (SQLException e) { e.printStackTrace(); } } }配置文件druid.properties:放在src或资源根目录下。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/cinema_db?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai usernameroot passwordyour_password initialSize5 maxActive20 maxWait60000实操心得serverTimezone参数在MySQL 8.0至关重要不设置可能导致“时区错误”。useUnicode和characterEncoding确保中文不乱码。连接池参数如initialSize、maxActive需要根据应用负载调整初期按默认或稍小值即可。3.2 Servlet与JSP的协作数据传递与页面跳转这是整个系统运转的枢纽。以“查看电影列表”功能为例。Servlet (FilmListServlet) 处理请求WebServlet(/filmList) // 使用注解配置URL映射比web.xml方便 public class FilmListServlet extends HttpServlet { private FilmService filmService new FilmServiceImpl(); // 注入业务层 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListFilm filmList filmService.getAllFilms(); // 调用业务方法获取数据 request.setAttribute(filmList, filmList); // 关键将数据存入请求域 request.getRequestDispatcher(/filmList.jsp).forward(request, response); // 转发到JSP } }JSP (filmList.jsp) 渲染页面% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !-- 使用JSTL标签库 -- html headtitle电影列表/title/head body h2正在热映/h2 table border1 trth海报/thth片名/thth导演/thth操作/th/tr c:forEach items${filmList} varfilm !-- 从请求域取出数据并循环 -- tr tdimg src${film.posterUrl} width100/td td${film.name}/td td${film.director}/td tda hrefscheduleList?filmId${film.id}查看场次/a/td /tr /c:forEach /table /body /html关键点解析request.setAttribute()和${}是Servlet向JSP传递数据的标准方式。数据存储在“请求域”中一次转发内有效。forward是服务器内部跳转地址栏不变。对于处理完数据后展示页面用forward。对于处理完数据后需要重定向到另一个地址如提交表单后跳回列表用response.sendRedirect()但注意重定向会导致request域数据丢失。强烈建议在JSP中使用JSTL和EL表达式(${}) 来替代传统的% %脚本片段这能使JSP页面更干净、更易于维护也更安全避免脚本注入。3.3 JDBC的规范操作与资源管理在Dao层JDBC操作必须规范否则极易导致资源泄漏或数据库连接耗尽。一个标准的查询模板在FilmDaoImpl中Override public Film getFilmById(int id) { Connection conn null; PreparedStatement pstmt null; ResultSet rs null; Film film null; try { conn DruidUtils.getConnection(); // 从连接池获取 String sql SELECT * FROM film WHERE id ?; // 使用预编译语句防SQL注入 pstmt conn.prepareStatement(sql); pstmt.setInt(1, id); // 设置参数 rs pstmt.executeQuery(); if (rs.next()) { film new Film(); film.setId(rs.getInt(id)); film.setName(rs.getString(name)); // ... 设置其他属性 } } catch (SQLException e) { e.printStackTrace(); // 实际项目应记录日志而非打印 throw new RuntimeException(查询电影失败, e); // 抛出自定义运行时异常 } finally { DruidUtils.close(conn, pstmt, rs); // 确保在任何情况下资源都被关闭 } return film; }关键点解析必须使用PreparedStatement它预编译SQL性能更好且能有效防止SQL注入攻击。永远不要用字符串拼接来构造SQL。必须关闭资源Connection、Statement、ResultSet都必须在finally块中关闭且顺序与创建顺序相反。使用我们上面写的工具类可以简化这一步。异常处理不要简单地e.printStackTrace()然后吞掉异常。应该捕获后记录日志并根据业务需求选择抛出受检异常或运行时异常。在Service层进行统一的异常处理是更好的做法。3.4 事务处理保证购票的原子性购票是系统的核心事务它涉及多个步骤1. 查询座位状态2. 锁定/更新座位状态3. 创建订单4. 创建订单明细。这些步骤必须作为一个整体要么全部成功要么全部失败回滚。在Service层实现事务public class OrderServiceImpl implements OrderService { private OrderDao orderDao new OrderDaoImpl(); private SeatDao seatDao new SeatDaoImpl(); Override public boolean purchaseTickets(int userId, int scheduleId, ListInteger seatIds) { Connection conn null; try { conn DruidUtils.getConnection(); conn.setAutoCommit(false); // 1. 开启事务关闭自动提交 // 2. 检查座位是否可用悲观锁SELECT ... FOR UPDATE for (int seatId : seatIds) { Seat seat seatDao.getSeatForUpdate(conn, seatId, scheduleId); if (seat null || seat.getStatus() ! 0) { throw new RuntimeException(座位[ seatId ]不可用); } } // 3. 更新座位状态为“已锁定” for (int seatId : seatIds) { seatDao.updateSeatStatus(conn, seatId, scheduleId, 2); // 2代表锁定 } // 4. 创建订单需要计算总价 Order order new Order(); order.setUserId(userId); order.setScheduleId(scheduleId); order.setTotalAmount(calculateTotalPrice(scheduleId, seatIds.size())); int orderId orderDao.createOrder(conn, order); // 获取自增ID // 5. 创建订单明细 for (int seatId : seatIds) { OrderItem item new OrderItem(); item.setOrderId(orderId); item.setSeatId(seatId); orderDao.createOrderItem(conn, item); } // 6. 模拟支付成功更新订单状态和座位状态为“已售” orderDao.updateOrderStatus(conn, orderId, 1); for (int seatId : seatIds) { seatDao.updateSeatStatus(conn, seatId, scheduleId, 1); // 1代表已售 } conn.commit(); // 7. 提交事务 return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); // 8. 任何一步出错回滚事务 } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { DruidUtils.close(conn, null, null); // 归还连接注意这里只关连接 } } // ... 其他方法 }注意事项这是一个简化版的事务处理。在高并发场景下仅靠数据库事务和FOR UPDATE行锁可能还不够可能会遇到“超卖”问题。更完善的方案需要引入分布式锁如Redis、乐观锁版本号或使用消息队列进行异步削峰填谷。但对于学习项目理解这个基于连接的事务边界和回滚机制已经足够了。4. 前端交互与用户体验关键实现虽然重点是后端但一个可用的系统离不开基本的前端交互。这里主要涉及JSP页面和简单的JavaScript。4.1 动态座位图选座功能这是售票系统的亮点和难点。我们需要根据hall表的seat_layout例如“8排10列”动态生成一个座位图并实时显示选中状态。JSP页面中的座位图生成%-- 从后台获取影厅布局信息 --% c:set varrows value${hall.rowCount}/ c:set varcols value${hall.colCount}/ div idseatMap c:forEach begin1 end${rows} varrow div classseat-row span classrow-label第${row}排/span c:forEach begin1 end${cols} varcol % // 这里可以调用后端方法检查该座位在当前场次的状态 // 但更优做法是通过Ajax加载座位状态避免JSP内嵌复杂逻辑 int seatStatus seatService.getSeatStatus(scheduleId, row, col); String seatClass seat; String disabled ; if (seatStatus 1) { seatClass sold; disabled disabled; } else if (seatStatus 2) { seatClass locked; disabled disabled; } % input typecheckbox idseat_${row}_${col} nameselectedSeats value${row}-${col} ${disabled} class${seatClass} onclickupdateSelection(this)/ label forseat_${row}_${col}${col}/label /c:forEach /div /c:forEach /div div已选座位span idselectedSeatsDisplay/span 总计span idtotalPrice0/span元/divJavaScript处理选中逻辑let selectedSeats []; let pricePerSeat ${schedule.price}; // 从JSP EL表达式获取单价 function updateSelection(checkbox) { const seatId checkbox.value; if (checkbox.checked) { if (selectedSeats.length 6) { // 限制最多选6张 alert(最多选择6个座位); checkbox.checked false; return; } selectedSeats.push(seatId); } else { const index selectedSeats.indexOf(seatId); if (index -1) { selectedSeats.splice(index, 1); } } document.getElementById(selectedSeatsDisplay).innerText selectedSeats.join(, ); document.getElementById(totalPrice).innerText selectedSeats.length * pricePerSeat; // 同步更新一个隐藏域用于表单提交 document.getElementById(selectedSeatsInput).value selectedSeats.join(;); }实操心得上述JSP中混用了JSTL和Scriptlet并不是最佳实践。更好的方式是在Servlet中预先查询好本场次所有座位的状态封装成一个MapString, IntegerKey为“行-列”Value为状态传到JSP然后JSP中直接用EL和JSTL判断。或者使用Ajax在页面加载后异步获取座位状态这样页面更纯净体验也更动态。4.2 表单验证与防止重复提交用户提交购票订单时前端和后端都需要验证。前端JS验证function validateForm() { const selectedSeats document.getElementById(selectedSeatsInput).value; if (!selectedSeats) { alert(请至少选择一个座位); return false; } // 可以添加更多验证如手机号格式等 // 防止重复提交禁用提交按钮 const submitBtn document.getElementById(submitBtn); submitBtn.disabled true; submitBtn.value 处理中...; return true; }后端Servlet防重前端禁用按钮是基础但网络请求可能重复发出。后端可以利用Session或Token机制。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 生成或获取Session中的Token String clientToken request.getParameter(formToken); String sessionToken (String) request.getSession().getAttribute(FORM_TOKEN); if (clientToken null || !clientToken.equals(sessionToken)) { request.setAttribute(errorMsg, 表单已过期或重复提交); request.getRequestDispatcher(/error.jsp).forward(request, response); return; } // 2. 处理业务逻辑... // 3. 处理完成后移除或更新Session中的Token使旧Token失效 request.getSession().removeAttribute(FORM_TOKEN); }在展示表单的JSP页面需要生成一个随机Token埋入表单隐藏域% String token UUID.randomUUID().toString(); session.setAttribute(FORM_TOKEN, token); % form ... onsubmitreturn validateForm() input typehidden nameformToken value%token% !-- 其他表单域 -- /form5. 项目部署、调试与常见问题排查开发完成后将项目部署到服务器如Tomcat并运行是最后一步也是问题高发区。5.1 项目部署到Tomcat打包在IDE中通常可以导出为WAR包Web Application Archive。在IntelliJ IDEA中你可以使用Build - Build Artifacts。或者直接将整个项目webapp目录包含WEB-INF复制到Tomcat的webapps目录下并重命名为cinema你的应用上下文路径。配置数据库确保Tomcat服务器所在的机器可以访问你的MySQL数据库且druid.properties中的连接信息尤其是url中的主机名是正确的。如果是本地localhost即可如果是远程服务器需改为服务器IP并确保MySQL允许远程连接。启动与访问启动Tomcat执行bin/startup.bat或startup.sh在浏览器访问http://localhost:8080/cinema/假设上下文路径是cinema。5.2 常见问题与排查技巧实录以下是我在开发和教学过程中学生最常遇到的几个“坑”问题1启动Tomcat后访问JSP页面出现“HTTP Status 500 - java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver”原因MySQL的JDBC驱动jar包没有放在正确的位置。解决确保mysql-connector-java-8.0.x.jar位于项目的WEB-INF/lib目录下并且已经部署到了Tomcat。在IDE中运行时需要将依赖库添加到项目的“Artifact”输出中或Tomcat的类路径下。问题2JSP页面显示中文乱码。原因字符编码不统一。全面解决策略JSP页面头部确保有% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8%。Servlet中在doGet/doPost方法最开始设置请求和响应的编码。request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8);数据库连接JDBC URL中加上参数?characterEncodingUTF-8。数据库和表创建数据库和表时指定字符集为utf8mb4。Tomcat服务器配置可选在server.xml的Connector标签中增加URIEncodingUTF-8。问题3使用JDBC插入数据时中文变成问号“?”但查询正常。原因通常是数据库连接URL缺少字符集参数或者数据库/表本身的字符集不是UTF-8。解决检查并确保上述“全面解决策略”中的第3、4点已配置。对于MySQL 8.0完整的URL可能像jdbc:mysql://localhost:3306/cinema_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai。问题4在JSP中使用EL表达式${filmList}但页面不显示数据。排查步骤检查Servlet中是否执行了request.setAttribute(filmList, filmList)。检查请求是否真的转发forward到了这个JSP页面而不是重定向sendRedirect或跳转到了其他页面。在JSP页面顶部尝试添加% page isELIgnoredfalse %确保EL表达式没有被忽略旧版本或某些配置可能默认忽略。在JSP中简单写一个% request.getAttribute(filmList) %看看是否为null以确定数据是否成功传递。问题5购票时出现“座位已被占用”的并发问题。原因多个用户同时查询到同一个座位可用然后都去更新它。解决方案在现有项目基础上增强悲观锁已实现在事务中使用SELECT ... FOR UPDATE先锁定选中的座位行。这是最直接的方式。乐观锁在seat表增加一个version版本号字段。更新时带上版本号条件UPDATE seat SET status?, versionversion1 WHERE id? AND version?。如果更新影响行数为0说明版本号不对被其他人修改过则回滚事务让用户重试。队列化对于真正的高并发可以将购票请求放入消息队列如RabbitMQ、Kafka由后台服务单线程顺序处理这是最彻底但最复杂的方案。学习项目中悲观锁结合短事务通常就够了。问题6部署到Linux服务器后一切正常但运行缓慢。可能原因Tomcat或JVM内存配置不足。排查与解决检查Tomcat日志catalina.out看是否有OutOfMemoryError。调整Tomcat的JVM参数。编辑bin/catalina.shLinux或catalina.batWindows找到JAVA_OPTS设置增加内存参数例如JAVA_OPTS$JAVA_OPTS -Xms512m -Xmx1024m -XX:MaxMetaspaceSize256m-Xms是最小堆内存-Xmx是最大堆内存。根据服务器物理内存调整。检查数据库服务器的性能是否因查询未加索引导致慢查询。完成这个项目你收获的不仅仅是一个可以运行的系统更是一套完整的、基于经典Java EE技术的Web开发方法论。从数据库设计、分层架构、事务处理到前端交互和部署调试每一个环节都充满了值得深究的细节。虽然现在Spring Boot等现代框架大大简化了开发但理解这些底层原理能让你在遇到问题时更加从容在技术选型时更有底气。建议你在实现基本功能后尝试添加更多特性比如用户登录状态管理Session/Cookie、电影评分评论、票房统计报表甚至尝试将JSP前端替换为Thymeleaf或Vue.js体验前后端分离这会让你的技能树更加完整。本文还有配套的精品资源点击获取
返回列表