纯JSP+Servlet+Session实现购物车:零数据库入门实战

简介:这是一份面向Java Web初学者的轻量级购物车系统实战案例,聚焦JSP+Servlet+Tomcat+Session核心技术栈,帮助开发者快速掌握电商基础功能的前后端协同实现。资源共62个文件,包含13个Java源码(涵盖entity、dao、service、servlet等分层逻辑)、6个JSP页面(listBook.jsp、listCart.jsp等构成完整用户流程)、26个编译后class文件、5个XML配置文件(含web.xml与数据库连接配置)以及HTML、JS、JAR等支撑文件,整体包仅916KB,结构清晰、开箱即用。已有671人学习下载,适合课程设计、期末项目或自学练手。读者可直接部署到Tomcat运行,深入理解Session管理购物车状态的机制,观察从商品展示→添加→结算的完整HTTP请求链路,并基于现有分层结构拓展数据库持久化、用户登录等进阶功能。

1. 一个不用数据库、不碰 Spring、纯 JSP + Servlet + Session 的购物车,居然能撑住 50 个并发加减操作?

你信吗?我上周在某高校实验室帮学生调毕设,看到一个叫购物车案例【简单版】.rar的压缩包——没 README,没 Maven,连web.xml都是手写的,解压后就WebContent/下一堆.jsp和WEB-INF/classes/里几个.class文件。本以为又是教学 Demo 玄学代码,结果本地 Tomcat 9 启动后,用ab -n 500 -c 50 http://localhost:8080/cart/show.jsp压测,商品增删改查全链路没丢一条 session 数据,session ID 也没漂移。它没用任何 ORM、没配数据源、甚至没建一张表,全靠HttpSession.setAttribute()把List<Map<String, Object>>当内存数据库使。适合 Java Web 入门者抠透 session 生命周期,也适合老手回炉重造——当你把session.setMaxInactiveInterval(600)改成120后,发现用户刚加完商品就跳转到结算页却提示“购物车为空”,这种血泪经验,教科书真不写。


2. 项目结构与技术栈:JSP + Servlet + Tomcat 的原始力量

这个压缩包不是玩具,它是一套可运行、可调试、可打断点的最小可行购物车系统。没有花哨框架,所有逻辑都暴露在眼皮底下。你不需要懂 Spring MVC 的 DispatcherServlet 是怎么分发请求的,只需要知道:一个 HTTP 请求进来,经过哪个 Servlet 处理,数据怎么塞进 session,JSP 又怎么从 session 里掏出来渲染。这就是 Java Web 的地基。

2.1 目录结构:拒绝黑匣子,每一层都看得见

解压后目录非常干净,符合 Servlet 规范的最简结构:

购物车案例【简单版】/ ├── WebContent/ │ ├── index.jsp # 首页:展示商品列表(静态 HTML + JSTL) │ ├── show.jsp # 购物车页:显示当前 session 中的商品 │ ├── add.jsp # 添加商品页:接收商品 ID 和数量,跳转到 addServlet │ ├── delete.jsp # 删除商品页:接收商品 ID,跳转到 deleteServlet │ ├── update.jsp # 修改数量页:接收商品 ID 和新数量,跳转到 updateServlet │ └── checkout.jsp # 结算页:清空 session 并跳转成功页 ├── WEB-INF/ │ ├── web.xml # 核心配置:servlet 映射、welcome-file-list │ └── classes/ │ ├── CartServlet.class # 编译后的 Servlet 类(对应 add/delete/update) │ └── Product.class # 商品实体类(id, name, price) └── src/ # (可选)原始 Java 源码(若提供) └── CartServlet.java

提示:src/文件夹不一定存在——很多教学压缩包只给编译好的.class。但别慌,.class可反编译,我们后面会用javap -c看字节码逻辑;更推荐你手动补全CartServlet.java,这是理解流程的关键一步。

2.2 技术栈拆解:为什么只用 JSP/Servlet/Session 就够了?

组件版本/实现方式作用替代方案对比
JSPJSP 2.3(Tomcat 9 默认)渲染页面,用<c:forEach>遍历 session 中的购物车列表若用 Thymeleaf,需额外引入 jar、配置 ViewResolver,学习成本陡增
Servlet手写HttpServlet子类,doPost()处理表单提交接收add?id=101&qty=2这类参数,更新 session 中的 ListSpring MVC 的@PostMapping更简洁,但隐藏了 request/response 的原始交互
Sessionrequest.getSession(true)获取,setAttribute("cart", list)存储全局唯一会话存储,生命周期由setMaxInactiveInterval()控制若用 Redis 存 session,需配连接池、序列化策略,对入门者属于过早优化

关键认知:这个项目不是“落后”,而是“精准克制”。它刻意避开数据库 IO、连接池管理、事务控制、MVC 分层等干扰项,把“用户点击加购 → 服务端记住 → 页面刷新显示”这条主干链路,用最短路径打穿。你改一行session.setAttribute(),刷新页面就能验证效果——这种即时反馈,是大型框架里最难获得的。

2.3 web.xml 配置解析:Servlet 映射的底层真相

WEB-INF/web.xml是整个项目的路由中枢,内容精简到只有 3 个关键块:

<?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"> <!-- 1. 定义 Servlet --> <servlet> <servlet-name>AddServlet</servlet-name> <servlet-class>CartServlet</servlet-class> </servlet> <!-- 2. 映射 URL 到 Servlet --> <servlet-mapping> <servlet-name>AddServlet</servlet-name> <url-pattern>/add</url-pattern> </servlet-mapping> <!-- 3. 设置欢迎页 --> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

注意两点:

  • <servlet-class>写的是CartServlet,但实际处理add、delete、update三个动作的,是同一个类里的不同if-else分支(通过request.getParameter("action")区分),不是三个独立 Servlet。这是教学项目常见做法,降低复杂度。
  • <url-pattern>/add</url-pattern>表示所有以/add开头的请求(如/add?id=101)都会交给CartServlet。Tomcat 在启动时解析此文件,构建内部的ServletMapping表。

提示:如果你看到web.xml里有<load-on-startup>1</load-on-startup>,说明该 Servlet 在 Tomcat 启动时就初始化,而不是首次请求才加载。本项目没加,所以第一次访问/add会稍慢——这是验证 Servlet 生命周期的好机会。


3. 核心模块拆解:CartServlet 的完整请求链路

CartServlet是整个系统的引擎室。它不处理数据库,不调外部 API,只做三件事:读 session、改内存 List、写回 session。下面逐行拆解其核心逻辑(基于反编译或典型教学实现还原)。

3.1 CartServlet.java 主干逻辑:一个方法撑起全部 CRUD

public class CartServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 强制获取 session(如果不存在则创建) HttpSession session = request.getSession(true); // 2. 从 session 中取出购物车 List,若无则新建 @SuppressWarnings("unchecked") List<Map<String, Object>> cart = (List<Map<String, Object>>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); // 首次存入 } // 3. 解析请求参数:action 决定执行哪条分支 String action = request.getParameter("action"); // 常见值:add / delete / update if ("add".equals(action)) { handleAdd(request, cart); } else if ("delete".equals(action)) { handleDelete(request, cart); } else if ("update".equals(action)) { handleUpdate(request, cart); } // 4. 重定向回购物车页,避免重复提交 response.sendRedirect(request.getContextPath() + "/show.jsp"); } private void handleAdd(HttpServletRequest request, List<Map<String, Object>> cart) { String id = request.getParameter("id"); String qtyStr = request.getParameter("qty"); int qty = "null".equals(qtyStr) ? 1 : Integer.parseInt(qtyStr); // 查重:已存在则累加数量,否则新增 boolean found = false; for (Map<String, Object> item : cart) { if (id.equals(item.get("id"))) { int oldQty = (int) item.get("qty"); item.put("qty", oldQty + qty); found = true; break; } } if (!found) { Map<String, Object> newItem = new HashMap<>(); newItem.put("id", id); newItem.put("name", "商品" + id); // 实际应查 DB,此处模拟 newItem.put("price", 99.9); // 同上 newItem.put("qty", qty); cart.add(newItem); } } private void handleDelete(HttpServletRequest request, List<Map<String, Object>> cart) { String id = request.getParameter("id"); // 使用迭代器安全删除(避免 ConcurrentModificationException) Iterator<Map<String, Object>> it = cart.iterator(); while (it.hasNext()) { Map<String, Object> item = it.next(); if (id.equals(item.get("id"))) { it.remove(); // 关键!用 iterator.remove(),不是 cart.remove() break; } } } private void handleUpdate(HttpServletRequest request, List<Map<String, Object>> cart) { String id = request.getParameter("id"); String newQtyStr = request.getParameter("newQty"); int newQty = Integer.parseInt(newQtyStr); for (Map<String, Object> item : cart) { if (id.equals(item.get("id"))) { item.put("qty", newQty); break; } } } }

关键参数说明:

  • request.getSession(true):true表示“如果 session 不存在就创建一个”,这是购物车功能的前提。若传false,首次访问会返回null,导致 NPE。
  • session.setAttribute("cart", cart):"cart"是 session 属性名,必须和 JSP 中session.getAttribute("cart")的 key 严格一致,大小写敏感。
  • response.sendRedirect(...):必须用重定向而非request.getRequestDispatcher().forward()。因为 forward 是服务器内部跳转,URL 不变,用户刷新会导致表单重复提交(即“确定要重新提交表单?”弹窗)。重定向后 URL 变成/show.jsp,刷新安全。

3.2 show.jsp:如何从 session 渲染动态购物车

show.jsp是用户看到的最终页面,它不写 Java 逻辑,只负责取数据、展数据:

<%@ 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> <!-- 1. 从 session 取出 cart --> <c:set var="cart" value="${sessionScope.cart}" /> <!-- 2. 判断 cart 是否为空 --> <c:choose> <c:when test="${empty cart || fn:length(cart) == 0}"> <p>购物车为空,请先<a href="index.jsp">去首页选购</a></p> </c:when> <c:otherwise> <table border="1"> <tr><th>商品ID</th><th>名称</th><th>单价</th><th>数量</th><th>小计</th><th>操作</th></tr> <!-- 3. 遍历 cart List --> <c:forEach var="item" items="${cart}"> <tr> <td>${item.id}</td> <td>${item.name}</td> <td>¥${item.price}</td> <td>${item.qty}</td> <td>¥${item.price * item.qty}</td> <td> <!-- 删除按钮:提交到 deleteServlet --> <form action="delete" method="post" style="display:inline;"> <input type="hidden" name="id" value="${item.id}" /> <input type="submit" value="删除" onclick="return confirm('确定删除?')" /> </form> <!-- 修改数量表单 --> <form action="update" method="post" style="display:inline;"> <input type="hidden" name="id" value="${item.id}" /> <input type="number" name="newQty" value="${item.qty}" min="1" /> <input type="submit" value="更新" /> </form> </td> </tr> </c:forEach> </table> <p><strong>总计:</strong>¥ <c:set var="total" value="0" /> <c:forEach var="item" items="${cart}"> <c:set var="total" value="${total + item.price * item.qty}" /> </c:forEach> ${total} </p> <a href="checkout.jsp">去结算</a> </c:otherwise> </c:choose> </body> </html>

关键细节:

  • ${sessionScope.cart}:明确指定从 session 域取值,避免 EL 表达式在 page/request/session/application 四个域中模糊搜索。
  • <c:forEach>内部的${item.id}:item是Map<String, Object>,EL 会自动调用get("id"),无需写item.get("id")。
  • 删除表单用onclick="return confirm(...)":前端拦截,防止误点。后端handleDelete仍需校验id非空,前后端校验缺一不可。
  • 总价计算用<c:set>累加:JSTL 不支持for循环变量自增,这是标准解法。

4. 运行与调试:从零部署到断点验证

别被“简单版”骗了——它跑不起来,90% 是环境或路径问题。我整理了一套实测有效的启动流程,覆盖 Windows/macOS/Linux,且每步都带验证命令。

4.1 环境准备:Tomcat 8/9 是底线,JDK 版本有坑

组件推荐版本验证命令常见翻车点
JDKJDK 8u291 或 JDK 11.0.12java -version输出含1.8.0_或11.0.12JDK 17+ 会报Unsupported major.minor version错误,因CartServlet.class是用 JDK 8 编译的
TomcatApache Tomcat 9.0.83catalina version输出Server version: Apache Tomcat/9.0.83Tomcat 10+ 使用 Jakarta EE namespace(jakarta.servlet.*),而本项目是javax.servlet.*,直接报ClassNotFoundException
浏览器Chrome/Firefox 最新版访问http://localhost:8080显示 Tomcat 欢迎页IE 已淘汰,勿试

提示:下载 Tomcat 后,解压到无中文、无空格路径,如D:\tomcat9。Windows 用户务必右键“以管理员身份运行”startup.bat,否则可能因权限问题无法绑定 8080 端口。

4.2 部署步骤:复制粘贴级操作指南

  1. 解压项目到 Tomcat 的 webapps 目录
    将购物车案例【简单版】.rar解压,得到文件夹(假设名为shopping-cart-simple),直接复制整个文件夹到tomcat9\webapps\下。不要重命名,不要嵌套子目录。
    ✅ 正确路径:tomcat9\webapps\shopping-cart-simple\WEB-INF\web.xml
    ❌ 错误路径:tomcat9\webapps\shopping-cart-simple\src\shopping-cart-simple\WEB-INF\...

  2. 启动 Tomcat 并验证部署
    运行tomcat9\bin\startup.bat(Windows)或./startup.sh(macOS/Linux),观察控制台输出:

    INFO [main] org.apache.catalina.startup.HostConfig.deployDirectory Deployment of web application directory [D:\tomcat9\webapps\shopping-cart-simple] has finished in [234] ms

    出现Deployment ... has finished即部署成功。此时访问http://localhost:8080/shopping-cart-simple/应看到index.jsp页面。

  3. 验证 session 是否生效
    打开浏览器开发者工具(F12),切换到Application → Storage → Cookies,查看localhost域下是否有JSESSIONIDCookie。

    • 如果没有:检查web.xml中是否漏了<session-config>,或request.getSession(true)是否被注释。
    • 如果有但每次刷新都变:说明 session 没持久化,可能是web.xml中<session-config><session-timeout>30</session-timeout></session-config>设得太小,或 Tomcat 的context.xml配置了sessionCookiePath="/"导致跨上下文失效。

4.3 断点调试:在 CartServlet 中亲眼看到数据流动

用 IntelliJ IDEA 或 Eclipse 调试是最直观的方式:

  1. 导入项目为 Dynamic Web Project

    • Eclipse:File → New → Other → Web → Dynamic Web Project → 名称填shopping-cart-simple→ Target runtime 选你配置好的 Tomcat 9
    • IDEA:File → New → Project from Existing Sources → 选shopping-cart-simple文件夹 → 选择 “Import project from external model” → Eclipse
  2. 添加源码(src/)并关联 class
    如果压缩包里有src/CartServlet.java,直接导入;如果没有,新建CartServlet.java,粘贴上面第 3.1 节的代码。然后右键项目 → Properties → Java Build Path → Source → Add Folder → 选src。这样调试时就能看到源码,而不是反编译的乱码。

  3. 在doPost方法第一行打断点
    启动 Debug 模式(右键项目 → Debug As → Debug on Server),在浏览器访问http://localhost:8080/shopping-cart-simple/add?id=101&qty=1,IDEA 会停在HttpSession session = request.getSession(true);这行。

    • 按 F8 单步执行,观察session.getId()值是否稳定;
    • 执行到session.getAttribute("cart")时,看 Variables 窗口里cart是null还是ArrayList;
    • 执行完handleAdd()后,展开cart变量,确认ArrayList里多了一个HashMap。

提示:如果断点不生效,检查项目 Facets 是否启用了Java和Dynamic Web Module,且版本匹配(Java 1.8,Dynamic Web Module 4.0)。


5. 避坑指南:5 个真实踩过的坑与解决方案

这个项目看似简单,但每个环节都有“看似正常、实则埋雷”的细节。以下是我在帮 12 个学生调试时,高频出现的 5 个问题,按现象→原因→解决三步法写清。

5.1 现象:点击“添加”按钮没反应,浏览器地址栏变成/add?id=101&qty=1,但页面空白

原因:CartServlet的doPost方法未被调用,Tomcat 找不到/add对应的 Servlet。根本原因是web.xml中<servlet-mapping>的<url-pattern>写成了/add.jsp或/addServlet,而非/add。
解决:打开WEB-INF/web.xml,确认<url-pattern>/add</url-pattern>与表单的action="/add"完全一致。注意:<url-pattern>是匹配 URL 路径,不是匹配文件名。

5.2 现象:购物车页面显示“购物车为空”,但控制台打印cart.size()=3

原因:show.jsp中用了${cart}而非${sessionScope.cart}。EL 表达式默认按 page → request → session → application 顺序搜索,如果其他域(如 request)里有同名变量,就会取错。
解决:强制指定域:<c:set var="cart" value="${sessionScope.cart}" />,后续所有${item.id}都基于这个cart变量,不依赖隐式对象搜索。

5.3 现象:添加同一商品两次,购物车里出现两条记录,而不是数量累加

原因:handleAdd()方法中id.equals(item.get("id"))比较失败。常见于item.get("id")返回的是Integer类型,而request.getParameter("id")是String,"101".equals(101)永远为false。
解决:统一转为字符串比较:"101".equals(String.valueOf(item.get("id"))),或在存入Map时就put("id", String.valueOf(id))。

5.4 现象:修改数量后,页面刷新,数量又变回原来的值

原因:handleUpdate()方法里更新了item.put("qty", newQty),但show.jsp中显示的是${item.qty},而item是Map,EL 会调用get("qty"),没问题。真正的问题是:表单 method 写成了 GET。<form action="update" method="get">会导致参数拼在 URL 上,Tomcat 默认只处理 POST 请求的doPost(),GET 请求会走doGet()(本项目未重写,所以 405 错误静默失败)。
解决:检查所有表单,确保method="post"。add.jsp、delete.jsp、update.jsp的表单必须都是 POST。

5.5 现象:Tomcat 启动时报java.lang.ClassNotFoundException: CartServlet

原因:CartServlet.class不在WEB-INF/classes/目录下,或包路径错误。例如CartServlet.java声明了package com.example;,但 class 文件放在WEB-INF/classes/CartServlet.class(没建com/example/子目录)。
解决:

  • 方案 A(推荐):删掉CartServlet.java的package声明,让它成为默认包,class 文件放WEB-INF/classes/根目录;
  • 方案 B:保留package com.example;,则CartServlet.class必须放在WEB-INF/classes/com/example/CartServlet.class,且web.xml中<servlet-class>改为com.example.CartServlet。

6. 二次开发实战:从内存购物车到可落地的轻量级改造

这个项目最大的价值,不是它现在能做什么,而是它为你留出了哪些“可插拔”的改造接口。我带过的学生里,有 3 人把它改成了课程设计,1 人上线了小型电商后台。下面给你一条清晰的升级路径:不推倒重来,只在原结构上缝合增强能力。

6.1 第一步:把硬编码商品换成真实数据库查询

当前handleAdd()里写死item.put("name", "商品" + id),这显然不能上线。但你不需要重写 DAO 层,只需加一个ProductDAO类,用 JDBC 最简方式查 MySQL:

// src/ProductDAO.java public class ProductDAO { private static final String URL = "jdbc:mysql://localhost:3306/shop?useSSL=false&serverTimezone=UTC"; private static final String USER = "root"; private static final String PASS = "123456"; public static Map<String, Object> getProductById(String id) { String sql = "SELECT id, name, price FROM product WHERE id = ?"; try (Connection conn = DriverManager.getConnection(URL, USER, PASS); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Map<String, Object> product = new HashMap<>(); product.put("id", rs.getString("id")); product.put("name", rs.getString("name")); product.put("price", rs.getDouble("price")); return product; } } } catch (SQLException e) { e.printStackTrace(); } return null; // 未找到 } }

然后在handleAdd()里替换两行:

// 原代码: // newItem.put("name", "商品" + id); // newItem.put("price", 99.9); // 新代码: Map<String, Object> dbProduct = ProductDAO.getProductById(id); if (dbProduct != null) { newItem.put("name", dbProduct.get("name")); newItem.put("price", dbProduct.get("price")); } else { throw new RuntimeException("商品不存在: " + id); }

提示:MySQL 驱动 jar(mysql-connector-java-8.0.33.jar)必须放到WEB-INF/lib/下,否则Class.forName("com.mysql.cj.jdbc.Driver")会报ClassNotFoundException。Tomcat 9 默认不带 JDBC 驱动,这是新手最容易忘的一步。

6.2 第二步:用 Filter 实现登录拦截,保护购物车

现在任何人都能访问/show.jsp,应该只允许登录用户查看。加一个LoginFilter:

// src/LoginFilter.java public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 白名单:登录页、静态资源、登录接口放行 String path = request.getRequestURI(); if (path.contains("/login.jsp") || path.contains("/login") || path.endsWith(".css") || path.endsWith(".js")) { chain.doFilter(req, resp); return; } // 检查 session 中是否有 user HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

并在web.xml末尾追加:

<filter> <filter-name>LoginFilter</filter-name> <filter-class>LoginFilter</filter-class> </filter> <filter-mapping> <filter-name>LoginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

6.3 第三步:用 Log4j2 记录关键操作,告别 System.out

把System.out.println("Added item: " + id)换成结构化日志:

<!-- WEB-INF/lib/log4j-api-2.20.0.jar + log4j-core-2.20.0.jar --> <!-- src/log4j2.xml --> <Configuration status="WARN"> <Appenders> <File name="FileOut" fileName="logs/cart.log"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Root level="info"> <AppenderRef ref="FileOut"/> </Root> </Loggers> </Configuration>

在CartServlet顶部加:

private static final Logger logger = LogManager.getLogger(CartServlet.class);

在handleAdd()末尾加:

logger.info("User {} added item {} x{}", session.getAttribute("user"), id, qty);

日志会自动写入tomcat9/logs/cart.log,比控制台滚动日志好追踪 10 倍。

从那以后我每次接手教学项目,都强制走一遍「加日志 → 加数据库 → 加拦截」三板斧。不是为了炫技,而是让每个功能点都有迹可循、有据可查、有错可溯。这份购物车案例【简单版】就像一块未经雕琢的璞玉,它的价值不在成品,而在你亲手刻下第一刀时,感受到的那份对 Java Web 底层脉络的真实触感。希望帮到你。

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