纯JSP+Servlet+Session实现购物车:零数据库入门实战 简介这是一份面向Java Web初学者的轻量级购物车系统实战案例聚焦JSPServletTomcatSession核心技术栈帮助开发者快速掌握电商基础功能的前后端协同实现。资源共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()把ListMapString, Object当内存数据库使。适合 Java Web 入门者抠透 session 生命周期也适合老手回炉重造——当你把session.setMaxInactiveInterval(600)改成120后发现用户刚加完商品就跳转到结算页却提示“购物车为空”这种血泪经验教科书真不写。2. 项目结构与技术栈JSP Servlet Tomcat 的原始力量这个压缩包不是玩具它是一套可运行、可调试、可打断点的最小可行购物车系统。没有花哨框架所有逻辑都暴露在眼皮底下。你不需要懂 Spring MVC 的 DispatcherServlet 是怎么分发请求的只需要知道一个 HTTP 请求进来经过哪个 Servlet 处理数据怎么塞进 sessionJSP 又怎么从 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.3Tomcat 9 默认渲染页面用c:forEach遍历 session 中的购物车列表若用 Thymeleaf需额外引入 jar、配置 ViewResolver学习成本陡增Servlet手写HttpServlet子类doPost()处理表单提交接收add?id101qty2这类参数更新 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 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 !-- 1. 定义 Servlet -- servlet servlet-nameAddServlet/servlet-name servlet-classCartServlet/servlet-class /servlet !-- 2. 映射 URL 到 Servlet -- servlet-mapping servlet-nameAddServlet/servlet-name url-pattern/add/url-pattern /servlet-mapping !-- 3. 设置欢迎页 -- welcome-file-list welcome-fileindex.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?id101都会交给CartServlet。Tomcat 在启动时解析此文件构建内部的ServletMapping表。提示如果你看到web.xml里有load-on-startup1/load-on-startup说明该 Servlet 在 Tomcat 启动时就初始化而不是首次请求才加载。本项目没加所以第一次访问/add会稍慢——这是验证 Servlet 生命周期的好机会。3. 核心模块拆解CartServlet 的完整请求链路CartServlet是整个系统的引擎室。它不处理数据库不调外部 API只做三件事读 session、改内存 List、写回 session。下面逐行拆解其核心逻辑基于反编译或典型教学实现还原。3.1 CartServlet.java 主干逻辑一个方法撑起全部 CRUDpublic 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) ListMapString, Object cart (ListMapString, 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, ListMapString, 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 (MapString, 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) { MapString, 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, ListMapString, Object cart) { String id request.getParameter(id); // 使用迭代器安全删除避免 ConcurrentModificationException IteratorMapString, Object it cart.iterator(); while (it.hasNext()) { MapString, Object item it.next(); if (id.equals(item.get(id))) { it.remove(); // 关键用 iterator.remove()不是 cart.remove() break; } } } private void handleUpdate(HttpServletRequest request, ListMapString, Object cart) { String id request.getParameter(id); String newQtyStr request.getParameter(newQty); int newQty Integer.parseInt(newQtyStr); for (MapString, 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 contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle我的购物车/title/head body h2购物车清单/h2 !-- 1. 从 session 取出 cart -- c:set varcart value${sessionScope.cart} / !-- 2. 判断 cart 是否为空 -- c:choose c:when test${empty cart || fn:length(cart) 0} p购物车为空请先a hrefindex.jsp去首页选购/a/p /c:when c:otherwise table border1 trth商品ID/thth名称/thth单价/thth数量/thth小计/thth操作/th/tr !-- 3. 遍历 cart List -- c:forEach varitem 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 actiondelete methodpost styledisplay:inline; input typehidden nameid value${item.id} / input typesubmit value删除 onclickreturn confirm(确定删除) / /form !-- 修改数量表单 -- form actionupdate methodpost styledisplay:inline; input typehidden nameid value${item.id} / input typenumber namenewQty value${item.qty} min1 / input typesubmit value更新 / /form /td /tr /c:forEach /table pstrong总计/strong¥ c:set vartotal value0 / c:forEach varitem items${cart} c:set vartotal value${total item.price * item.qty} / /c:forEach ${total} /p a hrefcheckout.jsp去结算/a /c:otherwise /c:choose /body /html关键细节${sessionScope.cart}明确指定从 session 域取值避免 EL 表达式在 page/request/session/application 四个域中模糊搜索。c:forEach内部的${item.id}item是MapString, ObjectEL 会自动调用get(id)无需写item.get(id)。删除表单用onclickreturn 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 namespacejakarta.servlet.*而本项目是javax.servlet.*直接报ClassNotFoundException浏览器Chrome/Firefox 最新版访问http://localhost:8080显示 Tomcat 欢迎页IE 已淘汰勿试提示下载 Tomcat 后解压到无中文、无空格路径如D:\tomcat9。Windows 用户务必右键“以管理员身份运行”startup.bat否则可能因权限问题无法绑定 8080 端口。4.2 部署步骤复制粘贴级操作指南解压项目到 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\...启动 Tomcat 并验证部署运行tomcat9\bin\startup.batWindows或./startup.shmacOS/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页面。验证 session 是否生效打开浏览器开发者工具F12切换到Application → Storage → Cookies查看localhost域下是否有JSESSIONIDCookie。如果没有检查web.xml中是否漏了session-config或request.getSession(true)是否被注释。如果有但每次刷新都变说明 session 没持久化可能是web.xml中session-configsession-timeout30/session-timeout/session-config设得太小或 Tomcat 的context.xml配置了sessionCookiePath/导致跨上下文失效。4.3 断点调试在 CartServlet 中亲眼看到数据流动用 IntelliJ IDEA 或 Eclipse 调试是最直观的方式导入项目为 Dynamic Web ProjectEclipseFile → New → Other → Web → Dynamic Web Project → 名称填shopping-cart-simple→ Target runtime 选你配置好的 Tomcat 9IDEAFile → New → Project from Existing Sources → 选shopping-cart-simple文件夹 → 选择 “Import project from external model” → Eclipse添加源码src/并关联 class如果压缩包里有src/CartServlet.java直接导入如果没有新建CartServlet.java粘贴上面第 3.1 节的代码。然后右键项目 → Properties → Java Build Path → Source → Add Folder → 选src。这样调试时就能看到源码而不是反编译的乱码。在doPost方法第一行打断点启动 Debug 模式右键项目 → Debug As → Debug on Server在浏览器访问http://localhost:8080/shopping-cart-simple/add?id101qty1IDEA 会停在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.8Dynamic Web Module 4.0。5. 避坑指南5 个真实踩过的坑与解决方案这个项目看似简单但每个环节都有“看似正常、实则埋雷”的细节。以下是我在帮 12 个学生调试时高频出现的 5 个问题按现象→原因→解决三步法写清。5.1 现象点击“添加”按钮没反应浏览器地址栏变成/add?id101qty1但页面空白原因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 varcart value${sessionScope.cart} /后续所有${item.id}都基于这个cart变量不依赖隐式对象搜索。5.3 现象添加同一商品两次购物车里出现两条记录而不是数量累加原因handleAdd()方法中id.equals(item.get(id))比较失败。常见于item.get(id)返回的是Integer类型而request.getParameter(id)是String101.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是MapEL 会调用get(qty)没问题。真正的问题是表单 method 写成了 GET。form actionupdate methodget会导致参数拼在 URL 上Tomcat 默认只处理 POST 请求的doPost()GET 请求会走doGet()本项目未重写所以 405 错误静默失败。解决检查所有表单确保methodpost。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?useSSLfalseserverTimezoneUTC; private static final String USER root; private static final String PASS 123456; public static MapString, 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()) { MapString, 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); // 新代码 MapString, 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 驱动 jarmysql-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-nameLoginFilter/filter-name filter-classLoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/*/url-pattern /filter-mapping6.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 statusWARN Appenders File nameFileOut fileNamelogs/cart.log PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refFileOut/ /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 底层脉络的真实触感。希望帮到你。本文还有配套的精品资源点击获取