拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

JSP+Bean+Servlet网上购书系统:从架构设计到部署验收全解析

JSP+Bean+Servlet网上购书系统:从架构设计到部署验收全解析

简介:基于Jsp+Bean+Servlet实现的简单网上购书系统课程设计资源,面向Java Web初学者与课程设计人群,采用典型B/S结构完成用户注册、登录、信息查询与浏览等核心功能,可帮助理解Servlet+JSP的分层开发与请求响应流程。压缩包共55个文件,以Java源码、JSP页面、SQL脚本及配置文件为主,内含15个Java源文件及对应15个class编译文件、6个JSP页面、2个SQL脚本、2个XML与properties配置,另有PDF说明文档、README笔记和项目工程文件,整体仅1.21MB,轻量且结构清晰。资源附带的Java Web课程综合训练PDF对项目设计做了梳理,配合src、WebRoot、config等目录可逐模块研读,也能直接导入Eclipse运行调试,便于在此基础上扩展购物车、订单等模块。已有172人学习下载,是一份适合课程设计参考和Servlet/JSP动手练习的完整示例。

1. 从课程设计到能跑的 Web 工程:JSP+Bean+Servlet 网上购书系统到底在写什么

如果你正在为一门 JavaWeb 课程设计或毕业设计选题发愁,那基于 Jsp+Bean+Servlet 实现(Web)简单的网上购书系统应该是你搜到的最常见答案之一。它看起来是个老掉牙的选题,但恰恰把 Web 开发的三层骨架全装了进去:JSP 画页面,Servlet 接请求,Bean 装数据。它不花哨,却能让你在答辩时把「一个请求从浏览器到数据库再回到页面的完整路径」讲清楚。我会带着你把数据库设计、Servlet 请求处理、JSP 页面渲染、Tomcat 部署验收整条链路跑一遍,全程给可复现的代码和参数。适合打算用这个题目交课程设计的学生,以及想补 JavaWeb 基础、搞明白 JSP 项目怎么落地的新手。

2. 先拆架构再动代码:JSP、Bean、Servlet 在购书系统里各自分工与选型理由

2.1 这套组合的本质:Servlet 生命周期与手工 MVC 的取舍

JSP+Bean+Servlet 是 JavaWeb 课程设计里最经典的手工 MVC。M 是 Model 层的 Bean,V 是 View 层的 JSP,C 是 Controller 层的 Servlet。三者的边界在购书系统里非常清楚。

Servlet 只做三件事:接收请求参数、调用 Bean 的方法拿到结果、决定把请求转发到哪个 JSP 页面。它不写 HTML,也不直接拼 SQL。JSP 只做一件事:把 Servlet 放进 request 或 session 作用域里的数据用 HTML 展示出来。Bean 则分成实体 Bean 和业务 Bean:实体 Bean(User、Book、CartItem)的字段对应数据库表,业务 Bean(UserService、BookService)封装登录校验、查书列表、加购物车这些动作。

这里必须先说透 Servlet 生命周期,因为很多新手把 Servlet 当成一个碰巧能跑 main 函数的按钮,写完 doGet 就结束了。实际上 Tomcat 容器管理 Servlet 的完整生命周期:加载类并实例化,调用 init() 做初始化,每个请求进入后由容器调用 service(),service() 再按请求类型把工作分发给 doGet 或 doPost,最后在容器关闭时调用 destroy() 回收资源。你在课程设计里几乎不用重写 service(),但要知道 doGet 和 doPost 是谁调进来的——这是后面排查 405 错误的基础。

Servlet 默认是懒加载的,第一次请求到达时才创建实例并执行 init()。如果你在 init() 里初始化数据库连接池或加载配置,希望项目一启动就检查环境,就在 web.xml 里配置<load-on-startup>1</load-on-startup>。数字越小优先级越高,Tomcat 启动时就会把这个 Servlet 实例化好。很多基于 JSP 的毕设选题都会在初始化时机上踩坑:连接池第一次访问才创建,数据库没就绪就直接报错,看起来像代码写错,其实是时机不对。

这套手工 MVC 虽然离企业级 Web 开发的要求还很远,但它把 Spring MVC 在背后做的事完整演了一遍。理解了 request、session、Servlet 这三个对象,再看 Spring Boot 里的过滤器、拦截器和 Controller,你会发现全是同一套骨架换了个壳。

提示:Servlet 是单实例多线程,这一点最容易出问题。写在 Servlet 里的成员变量会被所有请求共享,购物车这种按用户隔离的数据绝不能直接做成成员变量。标准做法是把购物车放进 session 作用域,每个用户拿到自己的 HttpSession,数据自然隔离。

2.2 数据库设计:图书表、用户表、订单表要建到什么程度

网上购书系统虽然叫「简单」,但数据库至少要有四张表:用户表 users、图书表 books、购物车表 cart_items、订单表 orders。如果时间紧,有人会把购物车和订单合并成一张已购记录表,但答辩时很容易被「订单状态怎么表示」「购物车和订单关系是什么」这类问题问住。我建议按最小可演示但结构完整的程度建表,工作量不会多多少。

先建用户表和图书表:

CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, email VARCHAR(100), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE books ( 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, description TEXT, cover_url VARCHAR(255) );

逻辑说明:id 一律用自增主键,不要用业务字段当主键,后面建订单表和购物车表时都要引用图书 id,数字主键做外键关联最省事。books 表里 title 的 200 长度足够容纳中英文书名。password 字段预留 64 位,是为了存 SHA-256 哈希后的十六进制字符串,而不是明文。这是 web 安全里最基础的一条,哪怕是课程设计,也建议别用明文存密码。

参数说明:price 用 DECIMAL(10,2) 而不是 FLOAT,浮点数在金额计算上会有精度误差,这是所有涉及钱的系统的底线要求。stock 设置 NOT NULL DEFAULT 0,避免插入测试数据时忘记填库存导致空指针。cover_url 允许为空,演示时不强制每本书都有封面图。

再建购物车表和订单表:

CREATE TABLE cart_items ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_cart_book FOREIGN KEY (book_id) REFERENCES books(id) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, order_time DATETIME DEFAULT CURRENT_TIMESTAMP, status VARCHAR(20) DEFAULT 'paid', FOREIGN KEY (user_id) REFERENCES users(id) );

逻辑说明:cart_items 用外键约束 user_id 和 book_id,让数据库自身保证引用完整性。很多初学者怕外键拖慢速度,其实这种量级的小系统完全不受影响,反而能防止插入 user_id 不存在的脏数据。orders 表把 book_id、quantity、total_price 冗余存进去,订单不会因为以后改图书价格而变化——这是电商订单设计的基本习惯。参数说明:status 字段预留订单状态,即使现在只做「已支付」一个值也建议留出来;演示时往 orders 表手动插一条数据展示订单列表,比对着代码讲更有说服力。

2.3 Bean 的实例化与属性注入:三种常见写法怎么选

Bean 的实例化方式在购书系统里有三种常见写法。第一种是直接 new,最简单直观,LoginServlet 里new UserService(),用完即走。第二种是单例模式,把 UserService 和 UserDao 设计成单例,避免每个请求都重新创建对象。第三种是静态工厂,用一个 BeanFactory 按类名反射创建对象,这已经是 Spring IOC 容器的雏形。课程设计里用前两种就够了,第三种适合想在答辩里讲「解耦」的同学。

一个简化的 BeanFactory 大概长这样:

public class BeanFactory { private static Map<String, Object> beans = new HashMap<>(); public static Object getBean(String className) throws Exception { if (!beans.containsKey(className)) { Class<?> clazz = Class.forName(className); beans.put(className, clazz.getDeclaredConstructor().newInstance()); } return beans.get(className); } }

逻辑说明:getBean 先查缓存,缓存没有就反射创建,创建完放进 Map。这叫「懒加载 + 单例池」,Spring 容器里最核心的机制也就是这两行思路。参数说明:这里要求 Bean 必须有无参构造器,否则反射调用getDeclaredConstructor().newInstance()会失败。真实项目会换成构造器注入,但理解这个雏形对回答「Bean 的实例化方式有哪些」这类问题足够。Bean 的生命周期在这个手写场景里就是「创建、使用、被 JVM 回收」,没有 Spring 容器管理的那些回调钩子,但思路是通的。

属性注入和构造器注入的选择,在购书系统的 OrderService 里能直接感受到。构造器注入把依赖关系写死:new OrderService(cartDao, bookDao)一步完成,对象一创建就完整可用。setter 注入先 new 再逐个 set,灵活但有可能在装配完之前被调用,出现空指针。手写 JDBC 项目我一般倾向构造器注入,因为少写代码、依赖清楚。等你以后用 Spring Boot 会发现,bean 的注解注入本质上还是「先声明依赖,再交给容器装配」,思路完全一致。

3. 用 Servlet 把请求接住:登录、注册、图书列表与购物车的核心链路

3.1 项目目录结构、web.xml 配置与 IDEA 新建 JSP 项目的关键选项

在 IDEA 2024 版本里新建 JSP 项目的常见做法是:先创建普通 Java 项目,右键模块选择「添加框架支持」,勾选 Web Application,IDEA 会自动生成 web/WEB-INF/web.xml。也可以用 Maven 骨架创建,目录结构更规范。无论哪种方式,最终目录结构应该是这样:

src/main/java/com/bookshop/ ├── bean/ # 实体 Bean:User.java, Book.java, CartItem.java ├── dao/ # 数据访问:UserDao.java, BookDao.java ├── service/ # 业务逻辑:UserService.java, BookService.java └── servlet/ # 控制器:LoginServlet.java, BookListServlet.java src/main/webapp/ ├── WEB-INF/web.xml ├── login.jsp ├── register.jsp ├── book_list.jsp └── cart.jsp

web.xml 是整个 Web 项目的注册中心,Servlet 必须在这里声明后才能通过 URL 访问。一个最小的配置:

<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.bookshop.servlet.LoginServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> </web-app>

逻辑说明:<servlet>声明 Servlet 的类名和显示名,<servlet-mapping>把对外访问的 URL 映射到内部名字。<load-on-startup>1</load-on-startup>表示容器启动时就实例化这个 Servlet,适合在 init() 里做连接池初始化。<url-pattern>/login</url-pattern>写成不带后缀的简洁路径,前端表单直接提交到这个地址。

参数说明:url-pattern 必须以 / 开头,写 /login 只匹配该路径,写 /login/* 匹配它下面的所有子路径。如果映射写成 *.do 这种后缀形式,请求路径就变成 /login.do。两种风格选一种即可,别混着写,否则容器会报路径冲突。

3.2 登录与注册的 Servlet 实现:取参、校验、转发与错误回显

登录是整个购书系统的入口。一个完整的 LoginServlet 既要在 GET 请求下把登录页面打开,又要在 POST 请求下处理表单提交。常见做法是只重写 doGet 和 doPost,把公共逻辑拆到私有方法里。

package com.bookshop.servlet; import com.bookshop.bean.User; import com.bookshop.service.UserService; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getRequestDispatcher("/login.jsp").forward(request, response); } @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"); if (username == null || username.trim().isEmpty()) { request.setAttribute("error", "用户名不能为空"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } User user = userService.login(username, password); if (user == null) { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); return; } HttpSession session = request.getSession(); session.setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/bookList"); } }

逻辑说明:doGet 里直接转发到 login.jsp,这样访问 /login 就能看到登录页,不用在地址栏敲 /login.jsp。doPost 里先调request.setCharacterEncoding("UTF-8"),而且必须在读取任何参数之前调用,否则中文用户名直接变乱码。登录成功后把 User 对象放进 session,之后所有页面都能通过${sessionScope.loginUser.username}拿到当前登录人。

参数说明:response.sendRedirect(request.getContextPath() + "/bookList")里的 getContextPath() 会自动拼上项目部署名,解决写死 /bookList 导致部署名变化后 404 的问题。userService.login()返回 null 表示账号或密码校验失败,这里把具体错误信息放进 request 而不是 session,是因为转发时 request 生命周期能覆盖整个请求,而且错误信息不该跨请求残留。

注册逻辑和登录类似,差别是注册成功不要自动登录,直接重定向到登录页让用户手动登录一次。这个细节答辩时能讲出理由:注册和登录是两个动作,用户应该清楚自己处在哪个状态。

3.3 图书列表与购物车的 Servlet 实现:查数据、存 session、再渲染

BookListServlet 负责把数据库里的图书查出来放进 request,转发给 book_list.jsp 渲染。这里 dao 层用简单的 JDBC 查询,service 层把连接关闭的杂活收掉。

package com.bookshop.servlet; import com.bookshop.bean.Book; import com.bookshop.service.BookService; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; public class BookListServlet extends HttpServlet { private BookService bookService = new BookService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Book> books = bookService.findAllBooks(); request.setAttribute("bookList", books); request.getRequestDispatcher("/book_list.jsp").forward(request, response); } }

逻辑说明:BookService.findAllBooks()返回 List<Book>,通过request.setAttribute("bookList", books)把数据放进 request 作用域。forward 到 JSP 时,同一个请求对象被带到页面,JSP 里直接用${bookList}就能拿到。forward 是服务端跳转,URL 地址栏不会变,适合列表展示场景。

购物车的核心逻辑不在数据库,而在 session。用户加购时,先从 session 里取购物车 List,取不到就新建一个再放回 session。每次加购往这个 List 里塞一个 CartItem,最后结算时把购物车数据批量插入订单表。

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { response.sendRedirect(request.getContextPath() + "/login"); return; } int bookId = Integer.parseInt(request.getParameter("bookId")); CartItem item = new CartItem(); item.setBookId(bookId); item.setQuantity(1); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } cart.add(item); response.sendRedirect(request.getContextPath() + "/cart.jsp"); }

逻辑说明:先从 session 拿 loginUser 做登录校验,没登录直接踢回登录页,这就形成了「未登录加购被拦截」的闭环。getSession() 会创建新 session;不想为匿名用户创建 session,就用 getSession(false) 并判空。参数说明:quantity 暂时固定为 1,去重和数量累加放到购物车页面用循环处理,课程设计做到这里已经能演示完整流程。cart 放在 session 里,意味着关掉浏览器再打开,购物车会丢,这是 session 的天然特性,答辩时可以说「正式系统会把购物车持久化到数据库」,不丢分。

4. 在 JSP 里把数据渲染出来:EL 表达式、JSTL 循环与表单回显

4.1 用 EL + JSTL 展示图书列表,而不是在 JSP 里写 Java 代码

很多新手在 JSP 里写<% for (int i = 0; i < list.size(); i++) { ... } %>这种脚本片段循环。在只有一两个页面的课程设计里能跑,但页面一多就会出现变量覆盖、标签嵌套混乱的问题。更可靠的做法是用 EL 表达式取值,JSTL 标签做循环和判断。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>图书列表</title></head> <body> <h2>全部图书</h2> <c:if test="${empty bookList}"> <p>暂无图书,请先到后台添加。</p> </c:if> <c:if test="${not empty bookList}"> <table border="1"> <tr><th>书名</th><th>作者</th><th>价格</th><th>库存</th><th>操作</th></tr> <c:forEach var="book" items="${bookList}"> <tr> <td>${book.title}</td> <td>${book.author}</td> <td>${book.price}</td> <td>${book.stock}</td> <td> <form action="${pageContext.request.contextPath}/cart" method="post"> <input type="hidden" name="bookId" value="${book.id}"/> <input type="submit" value="加入购物车"/> </form> </td> </tr> </c:forEach> </table> </c:if> </body> </html>

逻辑说明:<c:if test="${empty bookList}">处理空列表,避免表格只有表头没有数据。<c:forEach var="book" items="${bookList}">遍历 Servlet 放进来的集合,每次迭代把当前元素赋给变量 book,页面用${book.title}直接取字段值。EL 表达式会自动调用 Bean 的 getter 方法,Book 类里必须有 getTitle(),否则取不到值。加入购物车按钮包在一个小表单里,隐藏域传 bookId,提交到 /cart 由 CartServlet 处理。

参数说明:JSP 页面顶部必须写contentType="text/html;charset=UTF-8",否则页面中文容易乱码。JSTL 需要把对应的 jar 包放进 WEB-INF/lib,并且 taglib 的 uri 要和你使用的 JSTL 版本匹配,版本不一致会出现标签找不到的错误。

4.2 个人信息展示页面与购物车页面的数据回显技巧

JSP 里最容易被问到的就是表单回显。注册页面用户填了用户名,密码错了被退回时,用户名应该还在输入框里,而不是清空让用户重敲。做法是在 input 的 value 里写 EL 表达式。

<form action="${pageContext.request.contextPath}/register" method="post"> 用户名:<input type="text" name="username" value="${param.username}"/> 密码:<input type="password" name="password"/> <span style="color:red">${error}</span> <input type="submit" value="注册"/> </form>

逻辑说明:${param.username}直接读取请求参数里名为 username 的值,注册失败回到注册页时,用户名自动回显。password 字段绝不回显,这是 web 安全的基本习惯。${error}显示 Servlet 放进 request 的错误提示,一次请求结束后自动消失,不会污染下次访问。

提示:表单回显时,如果值是 null,页面上会出现字符串 "null"。用${param.username != null ? param.username : ''}先做一次判断,能避免这种尴尬。

购物车页面的回显更复杂一点:购物车 CartItem 列表放在 session 里,页面遍历每个 item,再取 bookId、quantity。如果想显示书名而不是数字 id,常见做法是在 CartItem 里冗余一个 bookTitle 字段,加购时一并写入;不要在每个 item 里再查一次数据库,页面会出现明显的卡顿。课程设计现场演示时,这个细节很能体现工程意识。

4.3 页面跳转方式:转发与重定向怎么选,路径为什么老 404

JSP 项目的跳转就两种:request.getRequestDispatcher("/xx.jsp").forward(request, response)是转发,response.sendRedirect("/xx")是重定向。选哪种不是玄学,规则很简单。

转发不改变浏览器地址栏,请求对象能带着 request 里的数据到下一个页面,适合列表展示、登录失败回显错误这种场景。重定向改变地址栏,重新发起一次新请求,request 里的数据全部丢失,但能避免表单重复提交,适合登录成功后跳首页、加购后跳购物车这种场景。

路径 404 的原因八成是绝对路径写错。在 JSP 里写死 /bookList,项目部署名是 bookshop,真实路径就变成 /bookshop/bookList。解决办法只有一个:所有重定向都用request.getContextPath()拼路径,所有 JSP 里的链接和表单都用${pageContext.request.contextPath}拼路径。这两个写法会自动带上部署名,换机器部署也不会 404。

还有一个容易被忽视的场景:刷新页面导致表单重复提交。用户在注册成功页面按 F5,浏览器会重新提交上一次的 POST 请求,产生重复记录。所以注册成功、登录成功这类操作一定要用重定向,不要用转发;而登录失败这种需要回显错误信息的情况才用转发。

5. 避坑:购书系统从「能编译」到「能演示」的 5 个翻车现场

5.1 中文乱码:JSP、Servlet、数据库三处编码不一致

现象:页面上的中文变成乱码,或者数据库里存储的中文变成问号。

原因:三处编码没有统一。JSP 文件本身的编码、Servlet 读取请求参数时的解码、JDBC 连接数据库的 charset,任何一处不一致都会在传输过程中产生乱码。最常见的是只设置了 JSP 编码,忘了在 Servlet 里调用 setCharacterEncoding,或者连接 URL 没加编码参数。

解决:JSP 顶部写<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 里在读取任何 getParameter 之前执行request.setCharacterEncoding("UTF-8");JDBC URL 末尾加useUnicode=true&characterEncoding=UTF-8;MySQL 表字符集用 utf8mb4。四件事都做了,中文问题不会再出现。另外要注意 setCharacterEncoding 必须在 getParameter 之前调用,放在后面等于白写,这个顺序问题出现的频率比想象中高。

5.2 数据库连接失败:JDBC 驱动版本与 Class.forName 的坑

现象:运行时报ClassNotFoundException: com.mysql.jdbc.Driver,或者Communications link failure。

原因:驱动类名写错,或者驱动 jar 没放进 WEB-INF/lib。新版 MySQL 驱动类的包名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver,这是网上购书系统里翻车率最高的一处。

解决:先确认驱动 jar 在 WEB-INF/lib 目录下。MySQL 8 及以上写Class.forName("com.mysql.cj.jdbc.Driver"),URL 用jdbc:mysql://localhost:3306/bookshop?useSSL=false&serverTimezone=Asia/Shanghai。数据库连接信息不要写死在代码里,放到 src 下的 db.properties,用 ClassLoader 读取,换环境只改配置文件,不用改代码。

5.3 表单提交后 405:doGet/doPost 没写全或者映射配错

现象:点击登录或注册按钮,浏览器弹出 405 Method Not Allowed。

原因:表单 method="post",但 Servlet 里只重写了 doGet,没写 doPost;或者 url-pattern 映射到了错误路径。

解决:补上 doPost,哪怕 doPost 里只是调一下 doGet,也要让容器找到处理入口。另一种常见情况是映射路径写在某个子目录下,表单 action 却写成根路径,两者对不上。可以用启动日志里打印的映射信息确认实际注册的地址,再对照表单 action 检查。

5.4 JSP 里直接写 Java 代码能跑,页面一多就崩

现象:最开始用<% %>在页面里写循环和 if,单页能正常显示;加了购物车页面之后,报变量找不到,或者 HTML 结构错乱。

原因:脚本片段里的变量作用域是整个 JSP 翻译出来的 Java 类,多个脚本片段共享变量,一不小心就互相覆盖。更隐蔽的是 try-catch 脚本片段和 HTML 混排,JSP 翻译时生成的代码会打乱声明顺序,报错行号根本对应不上源码行号。

解决:循环和判断换成 JSTL 标签,取值换成 EL 表达式。JSP 页面里只留 HTML 和标签,Java 逻辑全部上移到 Servlet。这不是风格问题,是 JSP 项目能不能往下追加页面的底线。课程设计如果只做一个页面,怎么写都能跑;要做到登录、列表、购物车三四个页面,脚本片段的维护成本会明显上升。

5.5 IDEA 部署到 Tomcat 后 404:部署名与请求路径不匹配

现象:启动 Tomcat 后访问 http://localhost:8080/bookList 一直 404,IDEA 控制台也没报错误。

原因:Tomcat 的部署名是 bookshop_war_exploded,默认根路径是 /bookshop_war_exploded/,访问路径少了部署名。

解决:打开 Run Configuration 里的 Deployment,把 Application context 改成 /,直接以根路径访问;或者所有页面链接统一加${pageContext.request.contextPath}前缀。我自己的习惯是本地开发改成 /,省事;多项目共存时保留部署名,统一用 getContextPath 拼路径。还有一个很隐蔽的问题:修改 JSP 后 Tomcat 没有重新编译,页面还是旧的,这种情况把 Tomcat 的 work 目录删掉再重启即可。

6. 验证与进阶:用 WAR 打包和功能验收清单让购书系统真正收尾

到这一步,系统应该已经在本地跑通了。但课程设计或毕设验收时,老师通常会要求能部署到一台独立的 Tomcat 上。常见做法是把传统 JSP 项目打包成 war:在 IDEA 里打开 Project Structure,选中 Artifacts,新增 Web Application Archive,Build 之后会生成 war 文件。如果是 Maven 项目,在 pom.xml 里设置打包方式为 war,然后执行:

mvn clean package

生成的 war 在 target 目录下,把它扔到 Tomcat 的 webapps 目录,启动 Tomcat 就会自动解压部署。这个动作本身很简单,但能一次性暴露部署名和路径问题。

部署完成后按清单过一遍功能,缺什么立刻能看出来:

功能项操作路径预期结果
注册访问 /register.jsp,填入用户名密码提示注册成功,跳转登录页
登录输入正确账号密码跳转图书列表,显示用户名
登录失败输入错误密码登录页回显错误提示
图书列表登录后访问 /bookList展示全部图书及库存
加入购物车点击任意图书的加购按钮购物车页出现该图书
未登录加购退出登录后直接访问加购链接被踢回登录页
中文数据注册中文用户名并浏览页面全程无乱码

再往深走一步,有几个低成本的进阶点值得做:在 web.xml 里加一个 CharacterEncodingFilter 过滤器,把请求编码统一处理掉,比在每个 Servlet 里手写 setCharacterEncoding 更接近真实工程;给密码加盐哈希,不要用明文;把购物车数据持久化到数据库,而不是只放 session。这三个点做完,答辩时老师问「你的系统有什么可以改进的」,你直接能给出具体方案。

最后说一个我自己的习惯:每次改完 JSP 或 Servlet,不要只在 IDEA 内置浏览器里点一遍,一定要用浏览器无痕窗口走一遍完整的注册到购物车流程,因为无痕窗口不带旧 session,能暴露缓存和登录状态的问题。实际验收前再看一遍 Tomcat 日志里有没有红色异常。这套流程我用了很多年,基本没在现场演示时翻过车。希望帮到你。

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

返回列表