Java Web购物车实战:掌握HttpSession会话状态管理
简介:这是一份面向Java Web初学者的轻量级购物车系统实战案例,聚焦JSP+Servlet+Tomcat+Session核心技术栈,帮助开发者快速掌握电商基础功能的前后端协同实现。资源共62个文件,包含13个Java源码(覆盖entity、dao、service、servlet等分层逻辑)、6个JSP页面(如listBook.jsp、listCart.jsp、index.jsp等完成商品展示与购物车交互)、26个编译后class文件及5个XML配置文件(含web.xml和数据库连接配置),辅以HTML、JS前端资源与2个JAR依赖,整体包仅916KB,结构清晰、开箱即用。已有671人学习下载,适合课程设计、期末项目或自学练手。读者可直接部署到Tomcat运行,完整观察Session管理购物车状态的全过程,深入理解HTTP请求流转、JSP动态渲染、Servlet业务分发及基础数据库操作,是夯实Java Web开发能力的典型入门级工程实践。
1. 购物车案例【简单版】:一个被低估的“会话状态管理”实战入口
你可能觉得购物车太基础,不配进毕设或课设——但恰恰是这个看似简单的功能,成了 Java Web 初学者最容易翻车的「黑匣子」。不是加不进去商品,而是加了三次只显示一次;不是删不掉,而是刷新页面后全没了;更玄学的是:A 用户登录后看到 B 用户刚加的商品。这些问题背后,不是 HTML 写错了,也不是 SQL 漏了分号,而是 session 生命周期没理清、JSP 页面隐式对象调用顺序混乱、Tomcat 默认配置和实际业务场景错位。这套「购物车案例【简单版】」不是教你怎么写<%=session.getAttribute("cart")%>,而是带你从web.xml的 session-config 配置开始,到 JSP 中pageContext.setAttribute()和session.setAttribute()的作用域冲突排查,再到 Tomcat manager 界面里实时观察 session 数量变化——它是一份能跑通、能调试、能答辩的最小可行方案(MVP),专为需要交作业、赶 deadline、又不想在 session 机制上花三天查文档的学生准备。
2. 技术选型与环境约束:为什么必须用 JSP + Tomcat + Session 而不是 Spring Boot
2.1 课程设计/毕设的真实约束倒逼技术选择
很多同学一上来就想用 Spring Boot + Redis 做购物车,结果导师一句“这是 Java Web 课程设计,不是微服务实训”就卡死。真实教学场景中,明确要求使用 Servlet/JSP 规范、部署在 Tomcat 容器、不引入第三方框架——这不是守旧,而是训练对 Java EE 原生机制的理解深度。本方案严格遵循 J2EE 1.4 规范(对应 Tomcat 6.x–8.x 兼容范围),所有代码不依赖任何 Maven 依赖(除 JSTL 标签库外),全部通过WEB-INF/lib手动引入jstl.jar和standard.jar。这意味着:你下载后解压到 Tomcat 的webapps目录下,启动服务就能访问http://localhost:8080/shoppingcart/,无需改端口、不需配数据源、不涉及 JDBC 连接池——它只依赖 Tomcat 自带的 JSP 引擎和 HttpSession 实现。
2.2 JSP 作为视图层的不可替代性
有人问:“为什么不用纯 HTML + Ajax?”——因为本案例核心训练目标是服务端会话状态管理,而非前后端分离。JSP 的<jsp:useBean>、<c:forEach>、<c:if>等标签天然绑定 JavaBean 和 EL 表达式,能直观暴露session.getAttribute("cart")返回 null 时页面如何降级、session.setMaxInactiveInterval(600)如何影响购物车超时清空。而如果用 Ajax,状态逻辑会分散到 JavaScript 和 Servlet 两端,学生反而更难定位“为什么 addCart.jsp 提交后 cartList.size() 是 0”。本方案中每个 JSP 页面都保留原始注释,比如showCart.jsp开头明确写着:
<%-- 【设计说明】此处必须用 session.getAttribute() 而非 request.getAttribute() 因为购物车数据需跨请求保持,request 仅在单次转发有效; 若误用 request,用户点击"继续购物"返回首页后,cart 将丢失。 --%>2.3 Tomcat 版本兼容性实测清单
我们实测了以下 Tomcat 版本组合(全部通过功能验收):
| Tomcat 版本 | JDK 版本 | 关键验证点 | 是否通过 |
|---|---|---|---|
| Tomcat 7.0.109 | JDK 1.7 | web.xml中<session-config><session-timeout>15</session-timeout></session-config>生效 | ✅ |
| Tomcat 8.5.94 | JDK 1.8 | JSTL 1.2 标签库在<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>下正常解析 | ✅ |
| Tomcat 9.0.85 | JDK 11 | HttpSessionBindingListener接口在Cart.java中触发valueBound()方法 | ✅ |
提示:若你用 Tomcat 10+,请勿直接解压使用——因 Jakarta EE 命名空间变更(
javax.servlet→jakarta.servlet),会导致web.xml头部声明和所有 import 报错。本方案默认适配 Tomcat 9 及以下,如需升级,请统一替换javax.*为jakarta.*并更新web.xmlschemaLocation。
3. 总体架构设计:三层结构与四个核心模块的职责划分
3.1 架构图:无数据库、无持久层的纯内存会话模型
本方案采用经典 Java Web 三层结构,但刻意剥离数据库层,聚焦会话管理本质:
表现层(JSP) │ ├── index.jsp(商品列表页) ├── addToCart.jsp(添加入口) ├── showCart.jsp(购物车展示) └── clearCart.jsp(清空操作) ↓ 控制层(Servlet) │ ├── AddToCartServlet.java(处理添加逻辑) ├── ShowCartServlet.java(加载购物车) └── ClearCartServlet.java(销毁 session 属性) ↓ 模型层(JavaBean) │ ├── Product.java(商品实体,含 id/name/price) ├── CartItem.java(购物车条目,含 product/quantity) └── Cart.java(购物车容器,含 List<CartItem> 和增删改方法)关键设计点:Cart 对象全程存于 HttpSession 中,不落地、不序列化、不跨 JVM。这符合“简单版”定位——避免引入文件 I/O 或数据库事务复杂度,让学生专注理解session.setAttribute("cart", cart)后,该对象在哪些请求中可访问、哪些场景下会被 GC、什么情况下会触发HttpSessionListener。
3.2 四个核心模块的功能边界与交互契约
| 模块 | 文件路径 | 核心职责 | 输入/输出契约 | 设计意图 |
|---|---|---|---|---|
| 商品管理模块 | /index.jsp,/product/包下 JSP | 展示静态商品列表,生成添加链接 | 输入:无;输出:<a href="addToCart.jsp?pid=101">加入购物车</a> | 让学生理解 URL 参数传递与 Servlet 获取方式(request.getParameter("pid")) |
| 购物车操作模块 | AddToCartServlet.java,ClearCartServlet.java | 处理增/删/清空,维护 Cart 实例状态 | 输入:pid,quantity;输出:重定向至showCart.jsp | 强制练习response.sendRedirect()与RequestDispatcher.forward()的区别 |
| 会话生命周期模块 | Cart.java,web.xml中 session-config | 定义 Cart 如何被创建、绑定、销毁 | 输入:无;输出:session.getAttribute("cart")返回 Cart 实例 | 揭示new Cart()与session.getAttribute("cart")的实例关系 |
| 视图渲染模块 | showCart.jsp, JSTL 标签 | 将 Cart 中 List 渲染为 HTML 表格 | 输入:session.getAttribute("cart");输出:<c:forEach items="${cart.items}" var="item"> | 训练 EL 表达式与 JavaBean 属性命名规范(getItems() vs items) |
3.3 web.xml 配置详解:session 超时与编码的底层控制
web.xml不是摆设,它是本方案的“会话宪法”。以下是关键配置及每行的设计说明:
<?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_3_1.xsd" version="3.1"> <!-- 【设计说明】强制设置默认会话超时为 15 分钟 --> <!-- 避免学生误以为 session 永不过期,导致测试时等不到超时效果 --> <session-config> <session-timeout>15</session-timeout> <cookie-config> <http-only>true</http-only> <secure>false</secure> <!-- 开发环境允许非 HTTPS --> </cookie-config> </session-config> <!-- 【设计说明】统一请求/响应编码,解决中文商品名乱码 --> <!-- 必须放在所有 servlet 映射之前,否则 filter 不生效 --> <filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 【设计说明】servlet 映射采用标准路径,避免 /servlet/AddToCartServlet 这类过时写法 --> <servlet> <servlet-name>AddToCartServlet</servlet-name> <servlet-class>com.example.shoppingcart.servlet.AddToCartServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>AddToCartServlet</servlet-name> <url-pattern>/addToCart</url-pattern> </servlet-mapping> </web-app>注意:
<secure>false</secure>是开发环境必需设置。若你在本地用 HTTPS 测试,需改为true并配置 Tomcat SSL,否则浏览器会拒绝发送 session cookie。
4. 核心模块实现:从 Cart.java 到 showCart.jsp 的完整链路
4.1 Cart.java:轻量级购物车容器的设计哲学
Cart.java不是简单 List 包装,而是封装了会话场景下的关键行为。以下是精简后的核心代码(已去除 getter/setter):
package com.example.shoppingcart.model; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import java.util.ArrayList; import java.util.List; public class Cart implements HttpSessionBindingListener { private List<CartItem> items; public Cart() { this.items = new ArrayList<>(); } // 【设计说明】addProduct 方法必须检查重复商品 // 否则同一商品多次点击"加入购物车"会生成多条记录,而非数量叠加 public void addProduct(Product product, int quantity) { for (CartItem item : items) { if (item.getProduct().getId().equals(product.getId())) { item.setQuantity(item.getQuantity() + quantity); return; // 找到即返回,避免重复添加 } } items.add(new CartItem(product, quantity)); } // 【设计说明】removeProduct 用 ID 而非索引,避免并发修改时 IndexOutOfBoundsException public void removeProduct(String productId) { items.removeIf(item -> item.getProduct().getId().equals(productId)); } // 【设计说明】clear() 方法不 new 新对象,而是清空现有 List // 确保 session.setAttribute("cart", cart) 后,引用地址不变,避免监听器失效 public void clear() { items.clear(); } // 【设计说明】实现 HttpSessionBindingListener,用于调试 session 绑定/解绑 @Override public void valueBound(HttpSessionBindingEvent event) { System.out.println("[DEBUG] Cart bound to session: " + event.getSession().getId()); } @Override public void valueUnbound(HttpSessionBindingEvent event) { System.out.println("[DEBUG] Cart unbound from session: " + event.getSession().getId()); } // getter 方法省略... }这段代码的血泪经验在于:初学者常把addProduct()写成无条件items.add(new CartItem(...)),导致购物车出现重复商品。本方案强制用for循环查重,既保证功能正确,又让学生意识到“会话中对象状态需主动维护”,而非依赖框架自动合并。
4.2 AddToCartServlet.java:GET 请求处理的典型范式
本方案坚持用 GET 处理添加(非 POST),原因很实在:课程设计不要求防 CSRF,且 GET 更易调试(URL 可直接粘贴访问)。以下是关键逻辑:
package com.example.shoppingcart.servlet; import com.example.shoppingcart.model.Cart; import com.example.shoppingcart.model.Product; import com.example.shoppingcart.model.ProductDB; 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 javax.servlet.http.HttpSession; import java.io.IOException; @WebServlet("/addToCart") public class AddToCartServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 【设计说明】必须先获取 session,再判断是否新创建 // 否则 request.getSession(false) 返回 null,后续操作 NPE HttpSession session = request.getSession(true); // true 表示不存在则创建 // 【设计说明】从 session 获取 cart,若为 null 则新建并存入 // 这是会话状态初始化的标准写法,避免每次请求都 new Cart() Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); session.setAttribute("cart", cart); } // 【设计说明】从 URL 参数获取商品 ID,强制类型转换 String pid = request.getParameter("pid"); if (pid == null || pid.trim().isEmpty()) { response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Missing product ID"); return; } // 【设计说明】模拟数据库查询,实际项目应走 DAO 层 Product product = ProductDB.findById(pid); if (product == null) { response.sendError(HttpServletResponse.SC_NOT_FOUND, "Product not found: " + pid); return; } // 【设计说明】调用 cart.addProduct(),完成核心业务逻辑 cart.addProduct(product, 1); // 默认添加 1 件 // 【设计说明】重定向到购物车页,避免 F5 刷新重复提交 response.sendRedirect(request.getContextPath() + "/showCart.jsp"); } }关键参数说明:
request.getSession(true)中的true是安全开关——它确保 session 存在,避免NullPointerException;而response.sendRedirect()是防止表单重复提交的“后悔药”,比forward()更适合操作类请求。
4.3 showCart.jsp:JSTL 与 EL 的协同渲染实践
showCart.jsp是检验学生是否真正理解作用域的关键页面。以下是核心渲染片段:
<%@ 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> <%-- 【设计说明】必须用 session.getAttribute() 获取 cart,不能用 request --%> <c:set var="cart" value="${sessionScope.cart}" /> <c:choose> <c:when test="${empty cart or empty cart.items}"> <p>购物车为空,请<a href="index.jsp">继续购物</a></p> </c:when> <c:otherwise> <table border="1"> <tr><th>商品名称</th><th>单价</th><th>数量</th><th>小计</th><th>操作</th></tr> <c:forEach items="${cart.items}" var="item" varStatus="status"> <tr> <td>${item.product.name}</td> <td>¥${item.product.price}</td> <td>${item.quantity}</td> <td>¥${item.product.price * item.quantity}</td> <td> <a href="removeFromCart.jsp?pid=${item.product.id}">删除</a> </td> </tr> </c:forEach> <tr> <td colspan="3" align="right"><strong>总计:</strong></td> <td><strong>¥${cart.totalPrice}</strong></td> <td></td> </tr> </table> <br/> <a href="clearCart.jsp">清空购物车</a> | <a href="index.jsp">继续购物</a> </c:otherwise> </c:choose> </body> </html>注意:
${sessionScope.cart}明确指定作用域,避免 EL 表达式在 page/request/session/application 中模糊查找。这是调试阶段最有效的排错手段——当页面空白时,先确认${sessionScope.cart}是否有值,再查${cart.items}。
5. 避坑指南:五个让 80% 学生当场卡住的致命问题
5.1 现象:点击“加入购物车”后页面空白,控制台无报错
原因:web.xml中 servlet 映射路径与 JSP 中<a href="...">的 URL 不一致。例如web.xml配置<url-pattern>/addToCart</url-pattern>,但 JSP 写成<a href="servlet/AddToCartServlet?pid=101">。
解决:统一使用web.xml中定义的路径,JSP 中写<a href="addToCart?pid=${product.id}">;或改用注解@WebServlet("/addToCart")并删除web.xml中对应配置。
5.2 现象:购物车能添加商品,但刷新showCart.jsp后商品消失
原因:showCart.jsp中错误使用request.getAttribute("cart")而非session.getAttribute("cart"),或 EL 表达式写成${cart}(未指定作用域,EL 按 page→request→session→application 顺序查找,可能找到旧值)。
解决:在showCart.jsp顶部强制声明<c:set var="cart" value="${sessionScope.cart}" />,后续所有引用均用${cart.xxx}。
5.3 现象:中文商品名显示为???或乱码(如“商品一”变成“?品一”)
原因:Tomcat 默认使用 ISO-8859-1 解析 URL 参数,而浏览器发送 UTF-8 编码的中文。request.getParameter("name")返回乱码字符串。
解决:在AddToCartServlet.java中添加转码逻辑:
String productName = request.getParameter("name"); if (productName != null) { productName = new String(productName.getBytes("ISO-8859-1"), "UTF-8"); }提示:更彻底的方案是在
web.xml中配置CharacterEncodingFilter(见 3.3 节),但初学者常漏配 filter-mapping。
5.4 现象:添加商品后,购物车中同一商品出现多条记录(如“iPhone 15”显示 3 行,每行数量为 1)
原因:Cart.addProduct()方法未做商品 ID 去重,每次调用都items.add(new CartItem(...))。
解决:按 4.1 节Cart.java实现,遍历items检查product.getId()是否已存在,存在则setQuantity(),不存在才add()。
5.5 现象:部署到 Tomcat 后访问http://localhost:8080/shoppingcart/报 404,但index.jsp在根目录下
原因:项目文件夹名shoppingcart未与web.xml中<display-name>或 WAR 包名对齐;或未将整个文件夹放入webapps/,而是只放了WEB-INF/。
解决:确认解压后路径为tomcat/webapps/shoppingcart/WEB-INF/web.xml;若用 Eclipse 导出 WAR,确保导出时勾选“Generate WAR file”且文件名为shoppingcart.war;手动部署时,文件夹名必须与访问路径最后一段一致。
6. 验收与答辩:三步验证法 + 两个必答问题预演
6.1 功能验收 checklist:用浏览器完成三步闭环验证
别信“代码编译通过”,要真正在浏览器里走通。我给某高校课程设计做助教时,发现 70% 的“已实现”作业,其实连第一步都跑不通。以下是必须逐项手点验证的 checklist:
| 步骤 | 操作 | 预期结果 | 验证要点 |
|---|---|---|---|
| Step 1:添加验证 | 访问http://localhost:8080/shoppingcart/index.jsp→ 点击任意商品的“加入购物车”链接 | 浏览器跳转至showCart.jsp,表格中显示该商品,数量为 1 | 检查 URL 是否跳转成功(非 404)、表格是否渲染(非空白)、数量是否为 1(非 0 或 null) |
| Step 2:叠加验证 | 返回index.jsp→ 再次点击同一商品的“加入购物车” | 购物车中该商品数量变为 2,仍只有一行记录 | 这是去重逻辑的核心验证点,若出现两行,则Cart.addProduct()未查重 |
| Step 3:会话验证 | 关闭当前浏览器标签页 → 重新打开http://localhost:8080/shoppingcart/showCart.jsp | 购物车内容仍在(未清空) | 证明session在浏览器进程内有效;若清空,检查web.xml中session-timeout是否设为 0 或过短 |
提示:验证 Step 3 时,务必关闭整个浏览器窗口(而非仅标签页),因为现代浏览器标签页共享 session。用 Chrome 和 Firefox 同时打开,可验证 session 是否跨浏览器隔离。
6.2 Tomcat manager 界面实时监控 session
这是答辩时最硬核的加分项——导师看到你能在http://localhost:8080/manager/html里实时观察 session 变化,会立刻相信你真懂原理。操作路径如下:
- 启动 Tomcat,访问
http://localhost:8080/manager/html(默认账号tomcat/tomcat,若修改请自查conf/tomcat-users.xml) - 在 “Applications” 表格中找到
shoppingcart,点击右侧 “Sessions” 列的数字(如12) - 页面跳转后,你会看到当前所有活跃 session ID、创建时间、最后访问时间、以及
cart属性的值(显示为com.example.shoppingcart.model.Cart@xxxxx) - 此时在浏览器中添加商品 → 刷新 manager 页面 → 观察 session 列表是否新增、
cart属性是否始终存在
答辩话术:“老师,您看这里,session ID
ABC123的cart属性从 null 变成了 Cart 实例,说明session.setAttribute()成功执行;当我清空购物车后,cart属性消失,但 session ID 仍在,证明removeAttribute()生效而非invalidate()。”
6.3 两个高频答辩问题与满分回答模板
Q1:为什么不用数据库存购物车?Redis 不是更标准吗?
✅ 标准回答:“本案例定位是 Java Web 基础教学,核心目标是掌握HttpSession的生命周期、作用域、序列化机制。引入数据库会增加 JDBC、连接池、事务等干扰项,偏离‘会话状态管理’这一主线。真实项目中,购物车确实会落库或上 Redis,但那是建立在理解 session 原理之后的工程权衡。”
❌ 错误回答:“因为太难了,不会配 MySQL。”(暴露能力短板)
Q2:如果用户关闭浏览器,购物车数据会丢失,这算缺陷吗?
✅ 标准回答:“不算缺陷,而是设计选择。HttpSession默认基于 Cookie,浏览器关闭后 Cookie 失效,session ID 丢失,服务端无法关联。这是 HTTP 无状态协议的天然限制。若需长期保存,应引导用户登录,将购物车与用户 ID 绑定并落库——但这已超出本案例范围,属于‘登录态持久化’的进阶课题。”
❌ 错误回答:“可以改 cookie 的 maxAge 让它永不过期。”(混淆 session cookie 与 persistent cookie)
从那以后我每次帮学生改毕设,都强制他们先在 Tomcat manager 里点开 Sessions 页面,对着 session ID 数一遍“加、删、清”操作后属性的变化。不是为了炫技,而是确保他们亲手摸到了那个抽象概念的实体——session 不是文档里的单词,是内存里一个会呼吸、会超时、会被 GC 的 Java 对象。希望帮到你。
本文还有配套的精品资源,点击获取