简介: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 1(JSP+JavaBean)或初级Model 2(JSP+Servlet+JavaBean)架构,是理解更复杂框架(如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(或资源根目录)下。
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/cinema_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username=root password=your_password initialSize=5 maxActive=20 maxWait=60000实操心得:
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 { List<Film> filmList = filmService.getAllFilms(); // 调用业务方法获取数据 request.setAttribute("filmList", filmList); // 关键!将数据存入请求域 request.getRequestDispatcher("/filmList.jsp").forward(request, response); // 转发到JSP } }- JSP (
filmList.jsp) 渲染页面:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!-- 使用JSTL标签库 --> <html> <head><title>电影列表</title></head> <body> <h2>正在热映</h2> <table border="1"> <tr><th>海报</th><th>片名</th><th>导演</th><th>操作</th></tr> <c:forEach items="${filmList}" var="film"> <!-- 从请求域取出数据并循环 --> <tr> <td><img src="${film.posterUrl}" width="100"></td> <td>${film.name}</td> <td>${film.director}</td> <td><a href="scheduleList?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, List<Integer> 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 var="rows" value="${hall.rowCount}"/> <c:set var="cols" value="${hall.colCount}"/> <div id="seatMap"> <c:forEach begin="1" end="${rows}" var="row"> <div class="seat-row"> <span class="row-label">第${row}排</span> <c:forEach begin="1" end="${cols}" var="col"> <% // 这里可以调用后端方法,检查该座位在当前场次的状态 // 但更优做法是通过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 type="checkbox" id="seat_${row}_${col}" name="selectedSeats" value="${row}-${col}" ${disabled} class="${seatClass}" onclick="updateSelection(this)"/> <label for="seat_${row}_${col}">${col}</label> </c:forEach> </div> </c:forEach> </div> <div>已选座位:<span id="selectedSeatsDisplay"></span> 总计:<span id="totalPrice">0</span>元</div>JavaScript处理选中逻辑:
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中预先查询好本场次所有座位的状态,封装成一个
Map<String, Integer>(Key为“行-列”,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 ... onsubmit="return validateForm()"> <input type="hidden" name="formToken" value="<%=token%>"> <!-- 其他表单域 --> </form>5. 项目部署、调试与常见问题排查
开发完成后,将项目部署到服务器(如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的类路径下。
问题2:JSP页面显示中文乱码。
- 原因:字符编码不统一。
- 全面解决策略:
- JSP页面头部:确保有
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8"%>。 - Servlet中:在
doGet/doPost方法最开始,设置请求和响应的编码。request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); - 数据库连接:JDBC URL中加上参数
?characterEncoding=UTF-8。 - 数据库和表:创建数据库和表时指定字符集为
utf8mb4。 - Tomcat服务器配置(可选):在
server.xml的<Connector>标签中增加URIEncoding="UTF-8"。
- JSP页面头部:确保有
问题3:使用JDBC插入数据时,中文变成问号“?”,但查询正常。
- 原因:通常是数据库连接URL缺少字符集参数,或者数据库/表本身的字符集不是UTF-8。
- 解决:检查并确保上述“全面解决策略”中的第3、4点已配置。对于MySQL 8.0,完整的URL可能像:
jdbc:mysql://localhost:3306/cinema_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。
问题4:在JSP中使用EL表达式${filmList},但页面不显示数据。
- 排查步骤:
- 检查Servlet中是否执行了
request.setAttribute("filmList", filmList)。 - 检查请求是否真的转发(
forward)到了这个JSP页面,而不是重定向(sendRedirect)或跳转到了其他页面。 - 在JSP页面顶部尝试添加
<%@ page isELIgnored="false" %>,确保EL表达式没有被忽略(旧版本或某些配置可能默认忽略)。 - 在JSP中简单写一个
<%= request.getAttribute("filmList") %>看看是否为null,以确定数据是否成功传递。
- 检查Servlet中是否执行了
问题5:购票时,出现“座位已被占用”的并发问题。
- 原因:多个用户同时查询到同一个座位可用,然后都去更新它。
- 解决方案(在现有项目基础上增强):
- 悲观锁(已实现):在事务中,使用
SELECT ... FOR UPDATE先锁定选中的座位行。这是最直接的方式。 - 乐观锁:在
seat表增加一个version版本号字段。更新时带上版本号条件:UPDATE seat SET status=?, version=version+1 WHERE id=? AND version=?。如果更新影响行数为0,说明版本号不对,被其他人修改过,则回滚事务让用户重试。 - 队列化:对于真正的高并发,可以将购票请求放入消息队列(如RabbitMQ、Kafka),由后台服务单线程顺序处理,这是最彻底但最复杂的方案。学习项目中,悲观锁结合短事务通常就够了。
- 悲观锁(已实现):在事务中,使用
问题6:部署到Linux服务器后,一切正常但运行缓慢。
- 可能原因:Tomcat或JVM内存配置不足。
- 排查与解决:
- 检查Tomcat日志
catalina.out,看是否有OutOfMemoryError。 - 调整Tomcat的JVM参数。编辑
bin/catalina.sh(Linux)或catalina.bat(Windows),找到JAVA_OPTS设置,增加内存参数,例如:JAVA_OPTS="$JAVA_OPTS -Xms512m -Xmx1024m -XX:MaxMetaspaceSize=256m"-Xms是最小堆内存,-Xmx是最大堆内存。根据服务器物理内存调整。 - 检查数据库服务器的性能,是否因查询未加索引导致慢查询。
- 检查Tomcat日志
完成这个项目,你收获的不仅仅是一个可以运行的系统,更是一套完整的、基于经典Java EE技术的Web开发方法论。从数据库设计、分层架构、事务处理到前端交互和部署调试,每一个环节都充满了值得深究的细节。虽然现在Spring Boot等现代框架大大简化了开发,但理解这些底层原理,能让你在遇到问题时更加从容,在技术选型时更有底气。建议你在实现基本功能后,尝试添加更多特性,比如用户登录状态管理(Session/Cookie)、电影评分评论、票房统计报表,甚至尝试将JSP前端替换为Thymeleaf或Vue.js,体验前后端分离,这会让你的技能树更加完整。
本文还有配套的精品资源,点击获取