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

发布时间:2026/10/1 18:11:44
JSP+Bean+Servlet网上购书系统:从架构设计到部署验收全解析 简介基于JspBeanServlet实现的简单网上购书系统课程设计资源面向Java Web初学者与课程设计人群采用典型B/S结构完成用户注册、登录、信息查询与浏览等核心功能可帮助理解ServletJSP的分层开发与请求响应流程。压缩包共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 工程JSPBeanServlet 网上购书系统到底在写什么如果你正在为一门 JavaWeb 课程设计或毕业设计选题发愁那基于 JspBeanServlet 实现Web简单的网上购书系统应该是你搜到的最常见答案之一。它看起来是个老掉牙的选题但恰恰把 Web 开发的三层骨架全装了进去JSP 画页面Servlet 接请求Bean 装数据。它不花哨却能让你在答辩时把「一个请求从浏览器到数据库再回到页面的完整路径」讲清楚。我会带着你把数据库设计、Servlet 请求处理、JSP 页面渲染、Tomcat 部署验收整条链路跑一遍全程给可复现的代码和参数。适合打算用这个题目交课程设计的学生以及想补 JavaWeb 基础、搞明白 JSP 项目怎么落地的新手。2. 先拆架构再动代码JSP、Bean、Servlet 在购书系统里各自分工与选型理由2.1 这套组合的本质Servlet 生命周期与手工 MVC 的取舍JSPBeanServlet 是 JavaWeb 课程设计里最经典的手工 MVC。M 是 Model 层的 BeanV 是 View 层的 JSPC 是 Controller 层的 Servlet。三者的边界在购书系统里非常清楚。Servlet 只做三件事接收请求参数、调用 Bean 的方法拿到结果、决定把请求转发到哪个 JSP 页面。它不写 HTML也不直接拼 SQL。JSP 只做一件事把 Servlet 放进 request 或 session 作用域里的数据用 HTML 展示出来。Bean 则分成实体 Bean 和业务 Bean实体 BeanUser、Book、CartItem的字段对应数据库表业务 BeanUserService、BookService封装登录校验、查书列表、加购物车这些动作。这里必须先说透 Servlet 生命周期因为很多新手把 Servlet 当成一个碰巧能跑 main 函数的按钮写完 doGet 就结束了。实际上 Tomcat 容器管理 Servlet 的完整生命周期加载类并实例化调用 init() 做初始化每个请求进入后由容器调用 service()service() 再按请求类型把工作分发给 doGet 或 doPost最后在容器关闭时调用 destroy() 回收资源。你在课程设计里几乎不用重写 service()但要知道 doGet 和 doPost 是谁调进来的——这是后面排查 405 错误的基础。Servlet 默认是懒加载的第一次请求到达时才创建实例并执行 init()。如果你在 init() 里初始化数据库连接池或加载配置希望项目一启动就检查环境就在 web.xml 里配置load-on-startup1/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 MapString, 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 ApplicationIDEA 会自动生成 web/WEB-INF/web.xml。也可以用 Maven 骨架创建目录结构更规范。无论哪种方式最终目录结构应该是这样src/main/java/com/bookshop/ ├── bean/ # 实体 BeanUser.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.jspweb.xml 是整个 Web 项目的注册中心Servlet 必须在这里声明后才能通过 URL 访问。一个最小的配置?xml version1.0 encodingUTF-8? 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_4_0.xsd version4.0 servlet servlet-nameLoginServlet/servlet-name servlet-classcom.bookshop.servlet.LoginServlet/servlet-class load-on-startup1/load-on-startup /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping /web-app逻辑说明servlet声明 Servlet 的类名和显示名servlet-mapping把对外访问的 URL 映射到内部名字。load-on-startup1/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 { ListBook books bookService.findAllBooks(); request.setAttribute(bookList, books); request.getRequestDispatcher(/book_list.jsp).forward(request, response); } }逻辑说明BookService.findAllBooks()返回 ListBook通过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); ListCartItem cart (ListCartItem) 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 contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle图书列表/title/head body h2全部图书/h2 c:if test${empty bookList} p暂无图书请先到后台添加。/p /c:if c:if test${not empty bookList} table border1 trth书名/thth作者/thth价格/thth库存/thth操作/th/tr c:forEach varbook 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 methodpost input typehidden namebookId value${book.id}/ input typesubmit value加入购物车/ /form /td /tr /c:forEach /table /c:if /body /html逻辑说明c:if test${empty bookList}处理空列表避免表格只有表头没有数据。c:forEach varbook items${bookList}遍历 Servlet 放进来的集合每次迭代把当前元素赋给变量 book页面用${book.title}直接取字段值。EL 表达式会自动调用 Bean 的 getter 方法Book 类里必须有 getTitle()否则取不到值。加入购物车按钮包在一个小表单里隐藏域传 bookId提交到 /cart 由 CartServlet 处理。参数说明JSP 页面顶部必须写contentTypetext/html;charsetUTF-8否则页面中文容易乱码。JSTL 需要把对应的 jar 包放进 WEB-INF/lib并且 taglib 的 uri 要和你使用的 JSTL 版本匹配版本不一致会出现标签找不到的错误。4.2 个人信息展示页面与购物车页面的数据回显技巧JSP 里最容易被问到的就是表单回显。注册页面用户填了用户名密码错了被退回时用户名应该还在输入框里而不是清空让用户重敲。做法是在 input 的 value 里写 EL 表达式。form action${pageContext.request.contextPath}/register methodpost 用户名input typetext nameusername value${param.username}/ 密码input typepassword namepassword/ span stylecolor:red${error}/span input typesubmit 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 页面跳转方式转发与重定向怎么选路径为什么老 404JSP 项目的跳转就两种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 contentTypetext/html;charsetUTF-8 %Servlet 里在读取任何 getParameter 之前执行request.setCharacterEncoding(UTF-8)JDBC URL 末尾加useUnicodetruecharacterEncodingUTF-8MySQL 表字符集用 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?useSSLfalseserverTimezoneAsia/Shanghai。数据库连接信息不要写死在代码里放到 src 下的 db.properties用 ClassLoader 读取换环境只改配置文件不用改代码。5.3 表单提交后 405doGet/doPost 没写全或者映射配错现象点击登录或注册按钮浏览器弹出 405 Method Not Allowed。原因表单 methodpost但 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 一直 404IDEA 控制台也没报错误。原因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 ArchiveBuild 之后会生成 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 日志里有没有红色异常。这套流程我用了很多年基本没在现场演示时翻过车。希望帮到你。本文还有配套的精品资源点击获取