尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

jsp+servlet网上书店项目实战:从架构原理到Tomcat部署全解析

发布时间:2026/9/29 12:37:43

资讯中心
01
ARTICLE

jsp+servlet网上书店项目实战:从架构原理到Tomcat部署全解析

jsp+servlet网上书店项目实战:从架构原理到Tomcat部署全解析
简介《jspservlet网上书店》是一份基于Java Web技术的在线图书销售平台完整源码适用于正在学习JSP、Servlet、DAO及MVC架构的Java初学者与课程设计人群。系统以JSP构建视图层、Servlet负责控制器业务分发、DAO完成数据库持久化操作清晰展示了经典三层架构在真实电商场景中的落地路径。rar压缩包共161个文件大小约768KB主体为27个java源码及其对应的class编译文件、17个jsp页面、64个gif界面截图另含sql数据库脚本、xml配置文件、js脚本及tld标签描述文件等可完整还原项目运行环境。当前已有162人学习适合作为毕业设计、课设参考或Java Web入门进阶的实战素材。从内容预览可见项目涵盖用户管理、图书检索、购物车、分页标签、登录过滤等Servlet核心模块读者可通过源码对照快速理解请求处理流程、数据库交互与会话管理机制获得从页面展示到业务逻辑的完整编码思路。1. jspservlet 网上书店是毕设/课设里的常青树用最小闭环讲清 Java Web「jspservlet 网上书店」是计算机专业做基于 JSP 的毕设选题时绕不开的题目。你需要实现用户注册登录、图书分类展示、加入购物车、下单结算这个业务闭环正好覆盖 Java Web 的四个硬核考点JSP 负责页面渲染Servlet 负责请求分发DAO 负责数据访问Session 负责登录状态。对 jsp 入门阶段的人来说把这本书店项目跑通比背一百个doGet概念有用。它适合两类人一类是正被课程设计卡住的在校学生另一类是想从「会写页面」跨到「能独立部署」的初级开发。下面按我自己的落地顺序讲。2. jspservlet 网上书店的架构与运行原理从一次图书列表请求看完整链路在拆代码之前先在心里画一张「请求地图」。网上书店的所有功能都能收敛到一个固定模式浏览器发起请求Tomcat 根据 URL 找到对应 ServletServlet 调 DAO 操作数据再把结果放进 request 或 session最后转发给 JSP 渲染成 HTML。这张地图一旦立住后面项目里几十个文件就不会散成乱麻。2.1 jsp 和 servlet 各自干什么控制逻辑与页面模板的分工JSP 本质上是「带 Java 占位符的 HTML 模板」Servlet 本质上是「能接收请求、返回响应的 Java 类」。两者不是二选一而是配合关系Servlet 里不写 HTMLJSP 里不写业务逻辑。JSP 页面可以用${}和c:forEach做数据展示但别在% %里塞大段循环、SQL 或事务代码。这样分层的好处是改页面不需要碰 Java改逻辑不需要翻 JSP。以「查看图书列表」为例浏览器请求/book/list容器找到BookListServletServlet 调用BookDao.findAll()查出图书集合把集合放进 request 属性再转发到book-list.jspJSP 用 JSTL 标签把图书渲染成表格。下面是这个 Servlet 的最小骨架WebServlet(name BookListServlet, urlPatterns /book/list) public class BookListServlet extends HttpServlet { private BookDao bookDao new BookDao(); Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); ListBook books bookDao.findAll(); request.setAttribute(books, books); request.getRequestDispatcher(/WEB-INF/jsp/book-list.jsp) .forward(request, response); } }这段代码有三个参数和配置值得记一下urlPatterns /book/list是 Servlet 3.0 起的注解式映射省掉web.xml里一对servlet和servlet-mappingrequest.setAttribute(books, books)是当前请求范围内的传值方式数据只在这一次请求里有效getRequestDispatcher(...).forward(...)是服务端转发浏览器地址栏不会变成 JSP 路径刷新页面时会把同一个请求再发一次。/WEB-INF目录下的 JSP 不能通过 URL 直接访问必须在 Servlet 里 forward 过去。这是把 URL 和物理页面解耦的常见做法也能避免用户绕过 Servlet 直接打开没有数据的 JSP 页面。网上书店的登录页、购物车页、订单结果页都建议放进这个保护目录只有首页这种完全静态的页面放在外面。2.2 servlet 生命周期与请求处理模型init 放连接池service 不做重活Servlet 生命周期可以拆成四步加载和实例化、init()初始化、service()服务、destroy()销毁。网上书店项目里只需要盯两个点init()整个生命周期只执行一次适合初始化数据库连接池service()里每个请求由容器分配独立线程执行同一个 Servlet 实例会被多个线程并发使用所以不要在 Servlet 里定义可变的全局成员变量来存请求数据。常见的翻车写法是每次请求都在service()或doGet()里new Connection()。这样做请求量稍微上来数据库连接数会暴涨页面响应越来越慢。正确做法是把DataSource放进init()或者做成静态工具类。注意init()里如果抛出异常这个 Servlet 会被容器标记为不可用后续请求直接 500。所以init()里最好做 try-catch并在控制台把具体异常打出来。JSP 本身其实也是一个 Servlet。第一次访问index.jsp时Tomcat 把 JSP 翻译成.java源文件再编译成.class之后直接复用。本地调试时改了 JSPTomcat 会通过文件时间戳判断是否重新编译但改了 Java 类IDEA 的 hot swap 不一定生效最保险的是重启 Tomcat。2.3 传统 jsp 项目在 tomcat 下是如何变成 servlet 的work 目录里的真相如果你觉得 Servlet 生命周期太抽象直接去 Tomcat 的 work 目录看现场。Tomcat 9 默认把 JSP 翻译后的源码放在work/Catalina/localhost/应用名/org/apache/jsp/下。比如index_jsp.java就是index.jsp翻译过来的 Servlet 源码里面有一个_jspService(HttpServletRequest request, HttpServletResponse response)方法JSP 页面里的静态 HTML 变成write()输出% %里的 Java 代码原样搬进方法体。这个目录能解释很多诡异报错。JSP 编译错误时报的行号经常落在index_jsp.java而不是.jsp打开对应 Java 源文件搜索出错行往往比在 JSP 页面上猜更快。查找命令find work/Catalina/localhost/bookstore/org/apache/jsp -name *.java命令里的bookstore是部署后的应用上下文路径如果打的 war 包叫bookstore.war目录就是bookstore。看到xxx_jsp.java后用文本编辑器打开搜索_jspServiceJSP 里所有脚本片段最后都堆在这个方法里。这个方法没有init()和destroy()JSP 的初始化由父类完成你只需要关注_jspService里怎么把内容写回响应。传统 JSP 项目还有一个容易忽略的点如果项目用的是web.xml配置文件而不是注解Servlet 的加载顺序由load-on-startup决定。这个值不写的话Servlet 默认在第一次请求时才实例化并执行init()。网上书店里的登录 Servlet、图书 Servlet 不需要预加载但如果有全局初始化逻辑建议单独写一个ApplicationInitServlet把load-on-startup1/load-on-startup配好。3. 用 idea 新建 jsp 项目从零把网上书店骨架搭起来原理清楚后下一步是把骨架落到 IDEA。很多人卡在这里不是不会写 Java而是项目类型选错。常见的三类选择是普通 Java 项目、Maven 项目、Maven Webapp 项目。做传统 JSP 项目直接建 Maven Webapp 项目依赖交给 Maven 管理最后能一键打 war省去手动拷贝 jar 的麻烦。3.1 idea 新建 jsp 项目用 maven-archetype-webapp 生成最小骨架IDEA 里的操作路径是File → New → Project左侧选 Maven勾选 Create from archetype选择org.apache.maven.archetypes:maven-archetype-webapp。如果列表里没有先点 Add Archetype填 GroupIdorg.apache.maven.archetypes、ArtifactIdmaven-archetype-webapp、Version1.4。生成后需要手动补 Java 和 resources 目录。在终端里等价命令mvn archetype:generate \ -DgroupIdcom.example \ -DartifactIdbookstore \ -DarchetypeArtifactIdmaven-archetype-webapp \ -DinteractiveModefalse生成后的目录结构应该是bookstore/ ├── pom.xml ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/bookstore/ │ ├── resources/ │ └── webapp/ │ ├── WEB-INF/ │ │ └── web.xml │ └── index.jsp骨架里默认只有index.jsp和web.xmljava、resources目录是空的需要自己创建。artifactId会变成 war 包名和 URL 上下文路径所以叫bookstore比叫test直观得多。接下来在 IDEA 里配置 TomcatRun → Edit Configurations → 左上角加号 → Tomcat Server → Local选择本机 Tomcat 9.x 路径Deployment 标签里把 war exploded 加上Application context 填/bookstore。到这里项目就能启动但访问图书列表还需要依赖。POM 里最少要加三个依赖dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version scoperuntime/scope /dependency这里有几个 scope 参数很关键。javax.servlet-api用provided因为 Tomcat 9 自带 Servlet 4.0 实现如果把它打包进 war部署时会和容器自带版本冲突出现 Servlet 映射不生效的怪问题。jstl是 JSP 页面里c:forEach、c:if的依赖必须随应用一起打包。mysql-connector-java用runtime编译期不直接引用运行时通过 JDBC 驱动 SPI 加载。3.2 图书列表页jsp 页面里用 jstl 渲染 dao 查出来的数据接第 2 章的BookListServlet现在补 DAO 和 JSP。BookDao先用DriverManager跑通理解过程最重要public class BookDao { private static final String URL jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; public ListBook findAll() throws SQLException { ListBook books new ArrayList(); try (Connection conn DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement ps conn.prepareStatement( SELECT id, title, author, price FROM book); ResultSet rs ps.executeQuery()) { while (rs.next()) { books.add(new Book( rs.getLong(id), rs.getString(title), rs.getString(author), rs.getBigDecimal(price))); } } return books; } }try-with-resources保证连接、语句、结果集在方法结束时自动关闭。characterEncodingUTF-8决定数据库连接层的中文编码它和 JSP 页面编码、MySQL 表编码必须一致否则漏掉一层就会出现中文乱码。PASSWORD硬编码只适合本地练习答辩或部署前要改成配置项。页面book-list.jsp% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title图书列表 - 网上书店/title /head body h1图书列表/h1 table trth书名/thth作者/thth价格/thth操作/th/tr c:forEach items${books} varbook tr td${book.title}/td td${book.author}/td td${book.price}/td td a href${pageContext.request.contextPath}/cart?actionaddbookId${book.id} 加入购物车 /a /td /tr /c:forEach /table /body /htmlc:forEach items${books} varbook等价于 Java 的for (Book book : books)${book.title}调的是Book.getTitle()所以Book类必须有对应的 getter。${pageContext.request.contextPath}是部署后的上下文路径bookstore.war部署后自动展开成/bookstore这个写法能保证本地开发和服务器部署时链接都不会写死。3.3 注册登录与 jsp 个人信息展示页面session 和 cookie 怎么配合用户模块最常见的做法是登录成功后在 Session 里放一个user对象后续 JSP 用${sessionScope.user}读取。Session 机制的底层是浏览器 Cookie 里的JSESSIONID浏览器关掉再开会话就变了。要做「记住我」需要额外写 Cookie 或者数据库 Token课程设计做基础版不需要。登录接口的LoginServletWebServlet(/login) public class LoginServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String username request.getParameter(username); String password request.getParameter(password); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { request.getSession().setAttribute(user, user); response.sendRedirect(request.getContextPath() /home); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }doPost接收表单 POST 请求登录密码不应该出现在 URL 上。登录成功用sendRedirect重定向到/home浏览器地址栏会变成/home用户刷新页面不会把登录请求再提交一遍。登录失败用forward回到登录页request.setAttribute(error, ...)在 JSP 里用${error}显示。个人信息展示页home.jsp是 JSP 里最常见的判断结构% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title个人中心/title /head body c:if test${not empty sessionScope.user} p欢迎你${sessionScope.user.nickname}/p p用户名${sessionScope.user.username}/p a href${pageContext.request.contextPath}/logout退出/a /c:if c:if test${empty sessionScope.user} p你还没有登录a href${pageContext.request.contextPath}/login.jsp去登录/a/p /c:if /body /html${sessionScope.user.nickname}等价于((User) session.getAttribute(user)).getNickname()但比 Java 强转简洁。用c:if而不是% if(...) %页面干净答辩时也好讲。这个 jsp 个人信息展示页面几乎可以套用到所有 Java Web 课设里记住not empty这个判断比! null更严谨因为 EL 对空字符串和 null 都能正确处理。4. 网上书店核心业务实现购物车、订单和数据库事务图书列表和用户登录只是外壳真正拿分的是「加入购物车 → 结算 → 生成订单」这条链路。这里要先做一个选型购物车放 Session 还是数据库。常见做法是登录前后都放 Session结算时一次性写入订单表和订单项表。这样入门阶段不用维护购物车表代码量小、好讲上线前再考虑持久化也不迟。4.1 数据库表设计与连接参数价格用 DECIMAL库存别用 int 硬扣直接给一套能建库的表结构CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4; USE bookstore; CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(200) NOT NULL, author VARCHAR(100), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, category VARCHAR(50), description TEXT ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id) ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (book_id) REFERENCES book(id) );三个选型理由值得记价格用DECIMAL(10,2)而不用floatJava 侧用BigDecimal对应避免浮点误差。order_no加UNIQUE唯一约束是防重复下单的最后一道防线。order_item里冗余了下单时的price因为图书价格后续可能调整历史订单必须保留快照。连接池参数用 c3p0 的话是这套ComboPooledDataSource ds new ComboPooledDataSource(); ds.setDriverClass(com.mysql.cj.jdbc.Driver); ds.setJdbcUrl(jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai); ds.setUser(root); ds.setPassword(123456); ds.setInitialPoolSize(5); ds.setMaxPoolSize(20); ds.setCheckoutTimeout(5000);initialPoolSize5是启动时建立的连接数maxPoolSize20是上限网上书店课设级别 5/20 足够。checkoutTimeout5000是等待连接的最长毫秒数超过就抛异常避免请求线程无限等。注意setDriverClass声明了受检异常实际代码要放在 try-catch 或init()里处理。这个DataSource建议在ApplicationInitServlet.init()里创建再放进ServletContext别的 DAO 取出来复用。4.2 加入购物车session 里的 map 比数据库表更直观购物车对象做成一个 JavaBean核心是LinkedHashMapLong, CartItempublic class Cart { private LinkedHashMapLong, CartItem items new LinkedHashMap(); public void add(Long bookId, int quantity) { CartItem item items.get(bookId); if (item null) { items.put(bookId, new CartItem(bookId, quantity)); } else { item.setQuantity(item.getQuantity() quantity); } } public BigDecimal getTotalPrice() { return items.values().stream() .map(CartItem::getSubtotal) .reduce(BigDecimal.ZERO, BigDecimal::add); } }LinkedHashMap保证商品加入购物车的顺序稳定JSP 遍历时不会乱序。CartItem里至少要有bookId、quantity、price、title其中title和price在下单前从图书表查出来回填。getTotalPrice()用map和reduce把所有小计累加最终返回BigDecimal避免 double 精度问题。接收「加入购物车」请求的 ServletWebServlet(/cart) public class CartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String action request.getParameter(action); HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } if (add.equals(action)) { Long bookId Long.valueOf(request.getParameter(bookId)); int quantity 1; if (request.getParameter(quantity) ! null) { quantity Integer.parseInt(request.getParameter(quantity)); } cart.add(bookId, quantity); } response.sendRedirect(request.getContextPath() /cart.jsp); } }这里用doGet接受从图书列表页过来的链接严格来说有副作用的行为应该用 POST但课程设计里很多同学为了少写一个表单直接用链接。如果答辩被问到把doGet改成doPost页面里把链接换成隐藏表单即可。quantity为空时默认 1这个参数兜底很重要不然图书列表页只传bookId会导致空指针。4.3 生成订单的事务边界扣库存和写订单项必须同生共死下单涉及四件事插入订单表、插入订单项、扣减库存、计算总价。四件事要么全成功要么全失败所以必须包在同一个数据库事务里public void createOrder(User user, Cart cart) throws SQLException { try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try { String orderNo BO System.currentTimeMillis(); BigDecimal total cart.getTotalPrice(); int generatedId; try (PreparedStatement ps conn.prepareStatement( INSERT INTO orders(order_no, user_id, total_price) VALUES (?,?,?), Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, orderNo); ps.setLong(2, user.getId()); ps.setBigDecimal(3, total); ps.executeUpdate(); try (ResultSet rs ps.getGeneratedKeys()) { rs.next(); generatedId rs.getInt(1); } } for (CartItem item : cart.getItems().values()) { try (PreparedStatement ps conn.prepareStatement( UPDATE book SET stock stock - ? WHERE id ? AND stock ?)) { ps.setInt(1, item.getQuantity()); ps.setLong(2, item.getBookId()); ps.setInt(3, item.getQuantity()); int updated ps.executeUpdate(); if (updated 0) { throw new SQLException(库存不足 item.getBookId()); } } try (PreparedStatement ps conn.prepareStatement( INSERT INTO order_item(order_id, book_id, quantity, price) VALUES (?,?,?,?))) { ps.setLong(1, generatedId); ps.setLong(2, item.getBookId()); ps.setInt(3, item.getQuantity()); ps.setBigDecimal(4, item.getPrice()); ps.executeUpdate(); } } conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } } }这段代码有两个关键点。第一UPDATE book SET stock stock - ? WHERE id ? AND stock ?是乐观锁写法库存不足时影响行数为 0代码主动抛异常回滚不会出现超卖。第二Statement.RETURN_GENERATED_KEYS用来拿数据库自增主键订单项表的外键order_id依赖它。事务执行顺序也有讲究。扣库存的 SQL 应该尽量靠近commit()因为UPDATE会把对应行锁住如果之后还有耗时的操作锁的持有时间会变长。把订单项插入放在扣库存之前等所有订单项写完了再统一扣库存锁的时间最短。本地调试时可以在 MySQL 里开两个客户端同时下单同一本书验证库存是否正确扣减。5. 网上书店常见问题与避坑指南从本地报错到上线部署下面这几条是 jspservlet 项目里出现频率最高的坑按「现象 → 原因 → 解决」顺序写遇到问题直接对号入座。5.1 servlet 生命周期里的初始化报错No suitable driver found现象Tomcat 启动后第一次访问某个 Servlet控制台报java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/bookstore重启后仍然存在。原因MySQL 驱动 jar 没有出现在应用运行时的 classpath。最常见的是 Maven 依赖 scope 写成了provided导致打 war 时驱动被排除或者手动拷贝 jar 时放在了别的目录而不是WEB-INF/lib。解决把mysql-connector-java的 scope 改成runtime或者直接删掉 scope 标签重新mvn clean package。然后解压 war确认WEB-INF/lib里有mysql-connector-java-8.0.33.jar。手动管理 jar 的项目检查src/main/webapp/WEB-INF/lib这个路径发布时它会原样进入 war。这个问题最容易在init()阶段暴露原因是连接池初始化发生在 Servlet 生命周期早期classpath 不完整会直接导致整个 Servlet 不可用。5.2 JSP 页面中文乱码四个编码必须一致现象页面上的书名变成???或æ±è¯POST 提交的用户名在后台取出来是乱码数据库手工插入的中文正常但页面查出来乱码。原因JSP 文件编码、HTTP 响应编码、数据库连接编码、MySQL 表编码这四层只要有一层不是 UTF-8中文就会在某一环断裂。最常见的是 JSP 文件漏写pageEncodingUTF-8或者 JDBC URL 少了characterEncodingUTF-8。解决JSP 第一行写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %所有 POST 请求在第一次读取参数之前调用request.setCharacterEncoding(UTF-8)JDBC URL 加useUnicodetruecharacterEncodingUTF-8建库时用DEFAULT CHARACTER SET utf8mb4。排查时先按 F12 看浏览器响应头的 charset再看数据库连接串最后看 IDEA 右下角确认 JSP 文件确实被存成了 UTF-8。很多人改了一处乱码就以为好了实际是另外两处仍然不一致所以这个坑要按顺序查。5.3 传统 jsp 项目打包 war 后 404web.xml 版本和 jar 冲突现象IDEA 里运行正常mvn clean package出的 war 放到 Tomcat 后访问 Servlet 报 404访问 JSP 却正常或者启动日志出现ClassNotFoundException: javax.servlet.ServletException。原因javax.servlet-api的 scope 不是provided导致 war 里同时存在容器自带的 Servlet API 和应用打进去的 Servlet API类被重复加载后 Tomcat 可能忽略应用里的 Servlet 映射。另一个常见原因是web.xml版本太老Servlet 3.0 的注解扫描没有开启所有WebServlet静默失效。解决容器自带的servlet-api、jsp-api一律providedweb.xml至少声明为 3.1 版本web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 /web-appMaven 骨架生成的老模板经常是 2.3 版本需要手动替换。如果web.xml里有metadata-completetrue/metadata-complete也要删掉否则容器完全不会扫描注解。这个坑的典型特征是JSP 能打开Servlet 全部 404而且本地 IDEA 正常、部署后才有问题。其实本地 IDEA 也依赖 Tomcat 的编译输出目录只是 IDE 常常帮你清理了旧 jar。5.4 nginx 支持 jsp 吗反代配置里.jsp请求必须交给 Tomcat现象用 nginx 代理 80 端口后访问静态 HTML 正常一打开index.jsp就返回 404或者浏览器直接把 JSP 源文件下载下来。原因nginx 本身不支持解析 JSP。nginx 处理.html、.css、.js这类静态文件没问题但遇到.jsp时不知道交给谁就按静态文件找找不到就 404。解决把.jsp请求反代给 Tomcat静态资源留在 nginx。常见配置server { listen 80; server_name bookstore.example.com; location / { proxy_pass http://127.0.0.1:8080/bookstore; proxy_set_header Host $host; } location ~ \.(css|js|png|jpg|gif|ico)$ { root /opt/bookstore/static; expires 7d; } }location /把所有请求转给 TomcatTomcat 处理 JSP 和 Servlet。第二个location用正则匹配静态文件后缀直接读 nginx 本地磁盘减轻 Tomcat 压力。这里最容易翻车的点项目里静态资源路径如果是/css/style.css那么root应该指向static目录的父目录否则 nginx 会把/css/style.css拼到/opt/bookstore/static/css/style.css导致 404。expires 7d是静态资源缓存头不是必需项但加上能让重复访问快很多。5.5 刷新页面重复提交订单PRG 和唯一索引一起上现象用户提交订单后页面显示成功但过一会儿数据库出现两条一模一样内容的订单浏览器后退再前进又触发一次建单接口。原因表单用 POST 提交到/order/create创建成功后 Servlet 直接forward到成功页。浏览器地址栏仍然是/order/create用户刷新时浏览器重新发送了上一次的 POST 请求于是建单逻辑又执行一遍。解决订单创建成功后不要forward要用response.sendRedirect(/order/success?orderNoxxx)重定向到一个 GET 页面。这就是 Post/Redirect/Get 模式浏览器刷新时只会重新 GET 成功页不会重放 POST。同时保留orders.order_no的唯一索引并在建单前检查是否重复。唯一索引是最后一道兜底PRG 才是体验和正确性都要的做法。6. 把网上书店打包 war 部署到 Tomcat验证功能与性能的最短路径传统 JSP 项目部署到服务器通常绕不开打包 war 和容器配置。把这一步做顺项目才算真正闭环。6.1 用 Maven 打 war 包传统 jsp 项目打包 war 的检查清单在项目根目录执行mvn clean package不报错后在target目录下得到bookstore.war。部署前用命令检查包内容unzip -l target/bookstore.war重点看三样WEB-INF/lib/里有没有 jstl 和 mysql 驱动WEB-INF/classes/里有没有编译后的 dao、servlet、modelWEB-INF/web.xml是不是 3.1 版本。缺少任何一样都别急着部署回到第 3 章检查依赖。6.2 Tomcat 部署与验证顺序先日志后功能把 war 拷贝到 Tomcat 的webapps目录启动后先看logs/catalina.out没有SEVERE再开始功能验证。验证顺序我习惯固定成先访问/bookstore/book/list确认 Servlet 返回图书列表再走一遍注册登录确认 Session 生效最后跑一次购物车和下单确认orders和order_item表都写入了数据。全程用临时测试账号别污染真实数据。6.3 部署后必做的一个 nginx 与 Tomcat 分工技巧如果演示环境只有一个 80 端口对外可以让 nginx 处理静态资源把.jsp和 Servlet 请求交给 Tomcat。这个「动静分离」做法在答辩时是很好的加分点但要注意千万别在 nginx 层面拦截.jsp那是第 5 章里最常见的翻车位置。配置完用curl -I http://127.0.0.1:8080/bookstore/book/list看响应码和后端耗时再打开浏览器看页面。我个人的习惯是每次部署完先跑这个命令因为出问题时它能先告诉你问题是出在网络层还是应用层。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。