☰
JSP银行预约管理系统开发实战:从Servlet原理到war部署
2026/10/1 4:46:31 网站建设 项目流程

简介:面向毕业设计场景的银行预约管理系统,基于 JSP + SQLServer + Tomcat 实现,适合 Java 方向本专科学生作为课程设计或毕设参考,重点演示预约业务、后台管理与数据库连接的完整落地方式。资源包共 499 个文件、9.07MB,核心包括 92 个 JSP 动态页面、31 个 HTML 页面及配套 CSS/JS 前端资源,后台含 Servlet/DAO 相关 Java 源码与编译 class,并配有 SQL 建库脚本、jar 依赖库和 docx 配套报告,覆盖从数据建模、编码实现到部署测试的主要环节。包内大量 GIF 截图可快速查看页面效果与操作流程,PNG/JPG 图片补充界面原型和流程说明,目录按页面、样式、脚本、后台逻辑划分,便于按模块查阅;压缩包内工程文件、数据库脚本和报告相互配套,导入即可对照学习。资源已有 179 人学习浏览,适合需要完整可运行代码、数据库脚本与说明文档,希望快速搭建同类预约管理系统的 Java 学习者。

1. 银行预约管理系统,为什么到了2025年还在用jsp做毕设

把“基于jsp的银行预约管理系统”放进毕设选题名单里,它比图书管理、宿舍管理多了一点真实业务的质感:预约本身包含了身份校验、时段选择、余量控制和取消释放,是一套完整的业务闭环,而不是单纯的四张表增删改查。用jsp做,不是因为它新,而是因为jsp能把你写的每一行页面代码如何变成servlet、如何在tomcat里被请求和编译这件事完整暴露出来,答辩时老师不管问“页面数据哪来的”还是“并发预约怎么防超卖”,你都能指着代码讲明白。这篇文章写给两类人:javaweb刚入门、想找一个能真正跑通又不至于做不完的毕设方向的学生,以及工作中要接手传统jsp项目、想搞懂目录结构、war打包和部署细节的从业者。

2. 技术选型与运行原理:jsp+servlet+mysql这套组合到底怎么跑起来

2.1 jsp不是一门独立语言:它和servlet本质上是一回事

jsp入门阶段最容易绕晕的一点是:jsp文件看起来是带html标签的页面,为什么tomcat把它当程序跑。实际上tomcat第一次收到jsp请求时,会把xxx.jsp翻译成xxx_jsp.java,再编译成class,这个class继承的是HttpJspBase,本质上就是servlet。你写在jsp里的html、el表达式、jstl标签,都会被翻译成out.write()的输出语句;你写在<%%>里的java代码,会被原样塞进_jspService方法。这就是jsp和servlet之间最核心的关系。

所以一个运行中的jsp项目,内存里真正干活的其实是servlet实例。jsp的生命周期由servlet容器管理:第一次请求触发翻译和编译,然后实例化、调用_jspService,容器关闭时销毁。这也是为什么你改了一个jsp文件后,必须重启tomcat才生效——重新翻译和编译发生在重启后的第一次请求。理解了这一点,再去排查“明明改了代码怎么不生效”就有方向。

这里给一个最直接的验证动作:到tomcat的work目录下找到org/apache/jsp/login_jsp.java这个文件,打开看一眼,你能在源码里看到自己在jsp里写的每一行html和scriptlet。这就是jsp被翻译成servlet的物证。jsp页面里还有几个内置对象——request、response、session、application,它们其实是在_jspService方法里预先声明好的变量,所以你在页面里可以直接用,不需要new。

2.2 传统jsp项目的目录结构、war包和请求流转

用maven组织起来的传统jsp项目,目录一般是这样的:

src/main/java // servlet、dao、model、filter src/main/resources // 数据库连接配置、日志配置 src/main/webapp // jsp页面、静态资源 ├── WEB-INF │ ├── web.xml │ └── lib └── index.jsp

功能代码在src/main/java里,页面在src/main/webapp里,缺一不可。webapp目录会被原样打进war包,WEB-INF下的web.xml是servlet容器找映射关系的地方。如果你用maven构建,WEB-INF/lib是打包时自动生成的,不要手动往里面拷jar,否则会出现版本冲突。

请求流转的顺序是:浏览器发起http请求,tomcat根据web.xml或注解里的映射找到servlet,调用service方法,实际会分发到doGet或doPost,然后servlet操作dao层访问数据库,结果放进request或session,最后forward到jsp渲染或redirect到另一个url。forward和redirect容易混:forward是服务器内部跳转,浏览器地址栏不变;redirect是返回302,浏览器重新发一次请求,地址栏会变。答辩时被问到登录成功怎么跳转,答清楚这个就够了。

web.xml里的核心映射长这样:

<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.bank.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

如果用@WebServlet注解,web.xml这段可以不写,容器启动时自动扫描加了注解的类。两种方式效果一样,但同一个项目里不要混用同一份映射,否则会出现“改了web.xml但接口还是404”的怪问题。

2.3 为什么jsp要跑在tomcat而不是nginx上

“nginx支持jsp吗”是这类项目里高频出现的问题。很多人把jsp文件直接丢到nginx目录下,访问时要么看到源码要么404。nginx本身只处理静态文件和反向代理,它不理解jsp,不识别servlet。jsp的翻译、编译、执行必须交给支持jsp的容器,比如tomcat、jetty。如果非要用nginx,也是在它前面做静态资源服务或反向代理,把jsp请求转发给后面的tomcat,而不是让nginx直接执行jsp。毕设项目里完全可以只用tomcat,不需要引入nginx。

2.4 为什么银行预约场景适合拿jsp来写

银行预约管理系统处理的核心是预约单的状态流转:用户选网点、选业务、选时段,提交后锁定一个名额;到了预约时间用户来办业务,状态变成已完成;用户临时有事取消预约,名额要释放回时段里。这里最有价值的是“名额别被超卖”和“同一个人别重复约同一个时段”,这两件事正好能用数据库事务和唯一索引解决,逻辑直接,不绕弯。

jsp+servlet虽然老,但调试链路短:浏览器发请求、servlet接到、sql执行、页面渲染,每一步都能从日志里看到。spring boot引入的依赖和自动配置太多,出了问题排查成本高,作为毕设反而不容易讲清楚。如果答辩时老师问“为什么不用spring boot”,回答思路是:这个项目的核心是预约约束和事务控制,jsp+servlet能把这些过程完整呈现,结构简单,部署到tomcat就能跑,技术选型匹配系统规模。

对比项jsp+servletspring boot
上手成本低,需要理解servlet生命周期高,需要理解自动配置和依赖管理
部署方式war包放进tomcat/webapps内嵌tomcat,可jar启动也可war
调试可见性请求链路直观,日志好定位链路长,依赖多,排查绕
毕设答辩风险低,jsp转译和sql事务都能讲中,框架原理容易被追问

3. 用idea新建jsp项目到跑通第一个预约页面

3.1 用idea新建jsp工程:maven webapp骨架与三处关键配置

打开idea的new project,选maven archetype,勾选maven-archetype-webapp。这个骨架会给一个最小的webapp目录和index.jsp,但src/main/java目录是缺的,需要手动右键新建,否则后面放servlet时找不到地方。创建完成后pom.xml默认是空的,需要自己加servlet、jsp、mysql驱动依赖。选择maven骨架而不是idea自带的webapp模板,是因为maven的目录结构和打包方式更接近真实项目,到war包部署那一步时不会因为缺目录而翻车。

pom.xml里的依赖按下面的来:

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>jsp-api</artifactId> <version>2.2</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> </dependencies>

两个provided是故意的,因为tomcat自带了servlet和jsp的运行时,打包war时如果把这些jar也打进去,会跟tomcat自带的重复,启动时容易出现Listener报错或ClassCastException。mysql驱动没有provided,需要打包进war的WEB-INF/lib,这样部署到别的机器上才能连数据库。如果你连的是mysql8,可以把connector版本换成8.0.x,驱动类也要相应改成com.mysql.cj.jdbc.Driver。

idea配置tomcat的步骤:Run菜单打开Edit Configurations,点+选Tomcat Server,Local,然后选本地tomcat路径。Deployment标签页里点+选Artifact或Exploded,Application context这里我一般填/bank,这样访问路径就是http://localhost:8080/bank。注意,web.xml里所有url-pattern以及表单里所有action路径,都要跟你这个context保持一致,否则启动后随手一个404。

3.2 写一个数据库连接工具:连接串参数和驱动加载

连接层用一个DBUtil工具类,静态代码块加载驱动,getConnection返回连接。这个类在service和dao里都会被调用,是整个项目的地基。

package com.bank.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/bank_app?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

URL末尾的useUnicode=true和characterEncoding=UTF-8,负责让数据库连接的字符集跟页面保持一致,少了它们,中文入库会变成问号。serverTimezone是给mysql5.7以上版本用的,老驱动不写这个会报时区错误。毕设本地用DriverManager直连问题不大,但每个请求都新建物理连接,并发高了会显得慢,后面可以换dbcp连接池,这里先不展开。

3.3 跑通登录到预约的完整链路,含jsp个人信息展示页面

第一个页面是登录页login.jsp,表单用post提交。提交地址不要写死/login,写成${pageContext.request.contextPath}/login,这样打包部署后无论contextPath改成什么都能对上。

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <html> <body> <form action="${pageContext.request.contextPath}/login" method="POST"> <label>用户名:<input type="text" name="username"/></label><br/> <label>密码:<input type="password" name="password"/></label><br/> <button type="submit">登录</button> </form> </body> </html>

pageEncoding是jsp源码文件的编码,contentType里的charset是响应给浏览器的编码,两个都写UTF-8才不会在乱码上翻车。表单字段的name,就是下一步servlet里getParameter要取的名字,前后必须一致。

LoginServlet接到请求后,取参数、查数据库、判断结果,然后决定是跳转到预约列表还是回到登录页。

package com.bank.servlet; import com.bank.dao.UserDao; import com.bank.model.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/appointmentList"); } else { resp.sendRedirect(req.getContextPath() + "/login.jsp?error=1"); } } }

这里有个容易被忽略的点:doPost里第一行就要setCharacterEncoding,这个调用必须在读取参数之前。如果先getParameter再设置编码,里面的中文已经按默认的ISO-8859-1读进去了,后面再怎么设置都救不回来。sendRedirect用的地址也要带contextPath,否则会跳到错误的应用下。

登录成功后的jsp个人信息展示页面userInfo.jsp,从session里把用户对象取出来,顺便展示最近的预约记录。这个页面在答辩时是加分项,因为它把session的作用讲清楚了:session能跨请求保存用户身份,只要浏览器不关,同一会话内所有jsp都能拿到这个user。

<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <% User user = (User) session.getAttribute("loginUser"); %> <div>姓名:<%=user.getRealName()%></div> <div>手机号:<%=user.getPhone()%></div> <div>最近预约:<%=user.getLastAppointment()%></div>

这段scriptlet只做展示,不写业务逻辑。jsp里少写java,写多了页面会变得难读,servlet也失去意义。但如果毕设时间紧,在页面里适度用<%= %>取session值是可以接受的,答辩时能解释清楚就行。

对应LoginServlet调用的UserDao和User,也要一起补上。User是一个纯model类,字段和t_user表对应:

package com.bank.model; public class User { private Integer id; private String username; private String password; private String realName; private String phone; private String idCard; // 省略getter/setter }

UserDao里用PreparedStatement执行带参查询:

package com.bank.dao; import com.bank.model.User; import com.bank.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql = "SELECT * FROM t_user WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setRealName(rs.getString("real_name")); user.setPhone(rs.getString("phone")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }

PreparedStatement的两个作用要记住:一是参数用?占位,避免字符串拼接sql;二是防止sql注入。答辩时问到登录模块安全性,这个点很加分。密码在毕设里可以先存明文,但如果你有余力,建议用md5摘要再存,代码量不大,答辩又能多讲一句。

到这里,登录链路就通了:login.jsp提交到LoginServlet,LoginServlet调用UserDao查表,查到后把User放进session并跳转到预约列表,查不到则回到登录页。下一步就是建库建表,把预约业务写完整。

4. 银行预约的核心业务:数据库表设计、时段余量与并发控制

4.1 数据库表结构:用户、业务、时段、预约、取消记录

先建数据库和五张表。设计上按职责拆开,不搞一张大表包打天下。

CREATE DATABASE IF NOT EXISTS bank_app DEFAULT CHARACTER SET utf8mb4; USE bank_app; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(20), phone VARCHAR(11), id_card VARCHAR(18), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_business ( id INT PRIMARY KEY AUTO_INCREMENT, biz_name VARCHAR(50) NOT NULL ); CREATE TABLE t_time_slot ( id INT PRIMARY KEY AUTO_INCREMENT, slot_date DATE NOT NULL, start_time VARCHAR(5) NOT NULL, end_time VARCHAR(5) NOT NULL, total_count INT DEFAULT 20, remain_count INT DEFAULT 20 ); CREATE TABLE t_appointment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, biz_id INT NOT NULL, slot_id INT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0已预约 1已完成 2已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_slot (user_id, slot_id, status) ); CREATE TABLE t_cancel_log ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, cancel_time DATETIME DEFAULT CURRENT_TIMESTAMP );

表结构按职责拆分:t_user存用户,t_business存业务类型,t_time_slot是可预约的时段以及每个时段的剩余名额,t_appointment是预约记录,t_cancel_log是取消留痕。唯一键uk_user_slot是防重复预约的关键。注意这里把status放进唯一键,是因为status会从0变成1或2,用户历史预约记录完结后,可以重新预约新的时段;如果唯一键只包含user_id和slot_id,这个约束会一直卡着历史记录,导致用户再也约不了同一个时段的号。

4.2 预约提交的减名额操作:一条update防超卖

预约业务的核心不在insert预约记录,而在于先把t_time_slot的remain_count减掉,并且只在还有余量的时候才允许减。这里不能用“先select出remain_count再判断大于0再update”,因为两个请求并发查到的可能是同一个剩余数,两条update都会执行,名额就超卖了。正确写法是带条件的update:

public boolean reduceRemainCount(int slotId) { String sql = "UPDATE t_time_slot SET remain_count = remain_count - 1 " + "WHERE id = ? AND remain_count > 0"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, slotId); return ps.executeUpdate() == 1; } catch (SQLException e) { e.printStackTrace(); return false; } }

executeUpdate返回1表示有一行被更新,说明扣减成功;返回0表示没有匹配到remain_count大于0的行,说明这个时段已经约满。整个过程只用一个sql,数据库的行锁保证同一时刻只有一个请求能成功扣减。

预约提交的servlet这样组织:

@WebServlet("/appointment/submit") public class AppointmentSubmitServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("UTF-8"); HttpSession session = req.getSession(); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } int slotId = Integer.parseInt(req.getParameter("slotId")); int bizId = Integer.parseInt(req.getParameter("bizId")); AppointmentDao appointmentDao = new AppointmentDao(); boolean reduced = appointmentDao.reduceRemainCount(slotId); if (reduced) { boolean inserted = appointmentDao.insertAppointment(loginUser.getId(), bizId, slotId); if (inserted) { resp.sendRedirect(req.getContextPath() + "/appointmentList"); } else { resp.sendRedirect(req.getContextPath() + "/appointment/submit.jsp?error=duplicate"); } } else { resp.sendRedirect(req.getContextPath() + "/appointment/submit.jsp?error=full"); } } }

两步操作之间不是原子的,如果insert失败,名额已经被扣掉却不产生预约记录,需要把扣减回滚,或者把两步放在同一个事务里。最简单靠谱的做法是把reduce和insert放在同一个Connection里,先setAutoCommit(false),最后统一commit,出现异常就rollback,这样不会再出现“名额没了记录却没有”这种灵异事件。

4.3 插入预约记录时的唯一索引兜底

update防了时段余量超卖,但同一个用户还是可能通过双开浏览器对同一个时段提交两次。第一次请求把余量扣掉了,第二次会多扣,或者在同一事务里产生两条预约记录。这时候靠的是t_appointment表上的唯一索引uk_user_slot。

业务里建议先做一次查询:

public boolean hasActiveAppointment(int userId, int slotId) { String sql = "SELECT COUNT(*) FROM t_appointment WHERE user_id = ? AND slot_id = ? AND status = 0"; // 返回count == 0才允许继续 }

查询是第一道防线,真正兜底的是数据库唯一索引。如果两个请求同时插入,索引会让后插入的那个抛DuplicateKeyException。捕获这个异常后,把提示改成“您已预约该时段”,而不是让页面直接500。注意别把异常吞掉后返回一个让人摸不着头脑的空白页。

另外提醒一点,插入前要判断slotId对应的时段日期是不是今天或未来。常见做法是查询可用时段时直接过滤:

SELECT id, slot_date, start_time, end_time, remain_count FROM t_time_slot WHERE slot_date >= CURDATE() AND remain_count > 0 ORDER BY slot_date, start_time;

这个sql把日期过滤和余量过滤一起完成,页面展示时直接用resultSet渲染。预约列表同理,只展示status等于0且时段日期在今天的记录,避免把过期未到场的单子长期挂在用户面前。

4.4 取消预约后余量怎么释放

用户点取消时,业务上是两步:把预约记录status改成2,再把对应时段的remain_count加回1。

public boolean cancelAppointment(int appointmentId) { String updateStatus = "UPDATE t_appointment SET status = 2 WHERE id = ? AND status = 0"; String releaseSlot = "UPDATE t_time_slot SET remain_count = remain_count + 1 WHERE id = ? "; // 查出该预约对应的slotId,然后在同一个事务里执行这两条sql }

这里同样要用事务,否则可能出现状态改成已取消但余量没加回来,或者反过来。取消是预约系统最容易漏写的功能,很多毕设只做了提交和列表,答辩时老师问一句“预约成功后不去柜台怎么办”,整个设计就断了。把取消补上,系统的闭环才算完整。

5. jsp银行预约系统最常见的5个坑:从404翻车到中文乱码

5.1 表单提交后404:路径写错却一直以为是tomcat坏了

现象:本地启动tomcat,打开login.jsp正常,填完用户名密码点登录,地址栏跳到http://localhost:8080/login,页面404。

原因:表单的action写成了绝对路径/login,而项目部署的上下文是/bank。tomcat把/login当成根应用下的接口,自然匹配不到。

解决:把form的action改成${pageContext.request.contextPath}/login,@WebServlet("/login")保持不变。contextPath读取出来是/bank,拼上/login之后,tomcat才能准确匹配到LoginServlet。这个坑属于路径问题,不是tomcat配置问题,排查时先看地址栏路径再想别的。

5.2 中文乱码:请求、页面、数据库三个环节全乱

现象:表单里输入“张三”,入库变成问号,页面上显示乱码,日志里也是一堆看不懂的字符。

原因:三个环节编码不一致。jsp页面没声明UTF-8,servlet接收请求时没设置request编码,mysql连接串缺字符集参数,任何一个环节断了都会乱。

解决:三个位置都要处理。写一个EncodingFilter过滤器统一处理request和response编码,比在每个servlet里重复写req.setCharacterEncoding省事得多。

@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); chain.doFilter(request, response); } }

这个过滤器放行所有请求,进来先设置编码再执行后续逻辑。jsp文件第一行也要写pageEncoding和contentType,连接串上保留characterEncoding=UTF-8。三件事做齐,中文乱码基本绝迹。

5.3 登录成功但跳回登录页:session丢失

现象:登录后明明看到逻辑走到success分支了,跳转之后又被拦截回login.jsp,或者在个人信息展示页面刷新一下就回到登录页。

原因:session保存和取出的key不一致,或者浏览器禁用了cookie,导致JSESSIONID没有随请求带上。还有一种情况是用了两个不同的浏览器环境,前一个登录的session在另一个环境里不存在。

解决:确认登录成功时用的是req.getSession().setAttribute("loginUser", user),后面所有地方都取同一个key"loginUser"。跳转用sendRedirect,不要用request.getRequestDispatcher。如果你在多个servlet之间来回跳,session始终是同一个,问题多半出在key写错了。

5.4 mysql连接失败或时区报错:驱动版本和连接串参数不匹配

现象:启动或第一次提交时,抛Communications link failure,或者报"The server time zone value ... is unrecognized"这类异常。

原因:使用的是老版驱动连接高版本mysql,或者连接串缺了serverTimezone参数。mysql8以后默认时区是UTC,老驱动不知道当前时区,就抛异常。

解决:连接串加serverTimezone=Asia/Shanghai,驱动版本和数据库版本要匹配。用mysql8的话,驱动升级到8.0.x,驱动类换成com.mysql.cj.jdbc.Driver,DBUtil里的Class.forName也要同步改。这个问题在毕设答辩前的演示环节最容易出现,因为教室里的mysql版本跟家里不一定一样。

5.5 本地正常、war包部署后页面全挂

现象:idea里点运行一切正常,把target/bank.war丢到服务器tomcat的webapps里,启动后登录页能开,但登录后所有链接404,或者css、图片全都加载不出来。

原因:本地运行时contextPath和war包部署后的contextPath不一致,或者代码里用了相对路径写静态资源。war包名字是bank.war,部署后的访问路径就是/bank,这个由tomcat自动决定。页面里如果写了src="/css/style.css",加载时会跑到http://ip/css/style.css,而不是/bank/css/style.css,自然404。

解决:所有静态资源引用和跳转路径统一用${pageContext.request.contextPath}拼前缀。打包前执行mvn clean package,解压war包检查WEB-INF/lib里有没有mysql驱动jar,否则部署到没有maven的机器上会报ClassNotFound。这些坑说到底就是路径和编码两个老问题,jsp项目里尤其明显,遇到别凭感觉改,先看日志再看地址栏。

6. 把传统jsp项目打包war部署到干净tomcat:五个验证动作

6.1 部署前检查这5项

检查项具体操作
jdk版本与tomcat匹配java -version确认,tomcat8.5用jdk7+,tomcat9配jdk8足够
数据库初始化mysql -uroot -p < bank_app.sql,执行后select确认表存在
war包内容mvn clean package,检查target下bank.war和WEB-INF/lib
编码过滤器全局搜索@WebFilter,确认已拦截所有请求
contextPath统一全局搜索action=和href=,确认都带${pageContext.request.contextPath}

6.2 启动tomcat看catalina.out,而不是只看页面

部署后先用bin/catalina.sh run前台启动,观察启动日志。前台模式能把控制台输出直接打到屏幕上,sql报错、类加载失败、端口占用一眼就能看到。确认没有Exception之后,按ctrl+c停掉,再用bin/startup.sh正式后台启动。启动异常绝大多数集中在数据库连接串和war包里的jar缺失,日志里都会给出明确的行号。

6.3 并发预约验证

最后做一个简单但能说明问题的验证:把某个时段的remain_count改成1,开两个浏览器窗口同时对它发起预约,结束后查询结果。正确的结果应该是只有一条成功预约,另一条提示“该时段已约满”。这个验证能同时证明update防超卖和唯一索引都真正生效了。

我自己的习惯是每次改完代码准备部署,都先把catalina.out里堆的exception看一遍再动服务器。做一个jsp项目,真正的问题大多不在jsp本身,而在路径、编码和事务边界这三件事上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询