news 2026/10/16 4:01:18

JSP+Session购物车案例:不登录不依赖数据库实现跨请求购物车

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Session购物车案例:不登录不依赖数据库实现跨请求购物车

简介:这份购物车案例【简单版】面向Java Web初学者与课程设计需求者,用JSP、Servlet、Tomcat、Session与数据库搭建了一个可运行的电商购物车最小闭环,帮助理解从商品展示、加入购物车到状态保持的完整请求链路。压缩包共62个文件,约916KB,包含13个java源文件与26个class编译产物、6个jsp页面、5个xml配置、4个jar依赖,另有html、js等前端资源,覆盖entity、dao、service、servlet分层结构,便于对照源码梳理MVC组织方式。目前已有670人学习下载。案例重点演示Session如何在多页面跳转间维持购物车数据,以及Servlet接收HTTP请求后更新业务状态、再由JSP渲染结果的协作过程,同时涉及数据库持久化商品与购物车信息的基本思路。读者可借此掌握JSP与Servlet整合、Tomcat部署及简单安全与性能注意事项,适合作为入门练手或课程作业参考。

1. 从一次课堂演示翻车说起:这个购物车案例到底解决什么问题

某高校计科专业的一次Web课程设计中期检查上,A同学演示自己的电商小项目,点击“加入购物车”后页面刷新,商品数量确实变了,但浏览器一关再打开,购物车空空如也。导师问了一句“你的购物车数据存在哪”,A同学答“存在request里”。这就是典型的把购物车当一次性请求数据处理了。购物车案例【简单版】.rar 这套资源要解决的核心问题很明确:用最基础的 jsp + java + tomcat + html + session 技术栈,把“用户在不登录、不依赖数据库的前提下,也能拥有一个跨请求、跨页面的购物车”这件事讲透。它适合正在做Web课设、需要交一份能跑通、能讲清原理的购物车模块的学生,也适合想快速回顾session机制怎么落到真实页面交互上的开发者。整套方案不追求高并发和分布式,追求的是每一行代码都能在答辩时说出为什么这么写。

2. 需求拆解与技术选型:为什么用session而不是cookie或数据库

2.1 购物车的三个硬需求

先把需求立住,后面写代码才不会飘。一个最简购物车,至少要满足三条:

第一,商品列表页能展示若干商品,每个商品有加入购物车的入口。第二,购物车页面能显示已加入的商品、单价、数量和小计,并且能删除某一项。第三,用户在不登录的情况下,购物车内容在多次请求之间保持住,关闭标签页再打开同一个会话仍然能看到。

这三条里,第三条是灵魂。前两条是页面和循环的事,第三条决定了你选什么存储方案。很多同学一上来就建数据库表,结果课设时间一半花在配数据源和写DAO上,购物车逻辑反而没时间打磨。

2.2 三种存储方案的取舍

方案数据位置生命周期适用场景本案例是否采用
request服务端单次请求请求结束即销毁转发传参否
cookie客户端浏览器可设置过期时间小量、非敏感、需持久否
session服务端会话会话超时或浏览器关闭(默认)临时状态、敏感数据是
数据库服务端持久化永久登录用户、跨设备否

选session的理由很直接:购物车数据是“当前这个用户这次访问期间”的临时状态,不需要永久保存,也不适合放在客户端被随意篡改。cookie虽然能存,但容量小、明文可见,存商品ID列表还行,存价格和数量就容易出安全问题。数据库对简单版来说太重。session正好卡在中间:服务端存对象,客户端只拿一个JSESSIONID,天然隔离。

提示:session默认存在Tomcat内存里,重启Tomcat会话丢失,这是正常现象,不是bug。课设演示时不要重启服务器。

2.3 技术栈版本与目录结构约定

资源基于 jsp + java + tomcat + html + session,不引入任何框架。通常的做法是,Tomcat选8.5或9.0这类稳定版,JDK用8,因为jsp里写Java片段对高版本JDK的兼容性偶尔会出玄学问题。目录结构按标准Web应用来:

shopping-cart/ ├── src/ │ └── com/example/cart/ │ ├── Goods.java // 商品实体 │ └── CartItem.java // 购物车条目实体 ├── WebContent/ │ ├── index.jsp // 商品列表页 │ ├── cart.jsp // 购物车展示页 │ ├── addToCart.jsp // 加入购物车处理 │ ├── removeFromCart.jsp // 删除条目处理 │ └── WEB-INF/ │ └── web.xml

把处理逻辑单独放jsp而不是Servlet,是简单版的刻意选择:减少web.xml配置量,让初学者把注意力放在session操作上。如果课设要求必须用Servlet,把addToCart.jsp里的逻辑搬到doGet里即可,session的用法完全一样。

3. 总体设计:两个实体类加四个页面的职责划分

3.1 实体类设计:Goods与CartItem

实体类不复杂,但字段设计决定了后面页面好不好写。Goods代表商品本身,CartItem代表“这个商品在我的购物车里加了几件”。

// Goods.java —— 商品基础信息,模拟数据用 package com.example.cart; public class Goods { private int id; // 商品唯一标识 private String name; // 商品名称 private double price; // 单价 public Goods(int id, String name, double price) { this.id = id; this.name = name; this.price = price; } // getter/setter 省略,实际资源里是完整的 public int getId() { return id; } public String getName() { return name; } public double getPrice() { return price; } }
// CartItem.java —— 购物车中的一条记录 package com.example.cart; public class CartItem { private Goods goods; // 关联的商品 private int quantity; // 购买数量 public CartItem(Goods goods, int quantity) { this.goods = goods; this.quantity = quantity; } // 小计金额,页面直接调用,避免在jsp里算 public double getSubtotal() { return goods.getPrice() * quantity; } public Goods getGoods() { return goods; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } }

把getSubtotal放在CartItem里而不是jsp里,是个小习惯:jsp里少写Java表达式,页面更干净,也方便以后改折扣逻辑。quantity用int而不是String,加入购物车时如果商品已存在就累加,不存在就新建CartItem。

3.2 session中的数据结构选择

购物车在session里到底存什么?常见有两种:存Map<Integer, CartItem>,key是商品ID;或者存List 。简单版推荐用Map,因为“判断商品是否已在购物车”这个操作,Map是O(1),List要遍历。

// 在addToCart.jsp中获取或初始化购物车 Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<Integer, CartItem>(); session.setAttribute("cart", cart); }

这里有个类型转换的警告,编译时会有unchecked提示,简单版可以忽略,但要知道原因:session.getAttribute返回Object,泛型信息在运行时擦除了。如果课设要求消除警告,加@SuppressWarnings("unchecked")即可。

3.3 四个页面的跳转关系

页面流转设计成单向加回环,避免死循环:

  • index.jsp(商品列表)→ 点击“加入” → addToCart.jsp(处理)→ 重定向回 index.jsp
  • index.jsp → 点击“查看购物车” → cart.jsp(展示)
  • cart.jsp → 点击“删除” → removeFromCart.jsp(处理)→ 重定向回 cart.jsp

处理页用response.sendRedirect而不是forward,是为了避免刷新页面时重复提交。这是血泪经验:用forward的话,用户按F5会再执行一次加入操作,数量翻倍。

4. 核心模块实现:从加入购物车到删除条目的完整链路

4.1 商品列表页的数据准备

index.jsp不连数据库,商品数据在页面顶部用Java片段初始化一个List。真实项目当然从数据库来,但简单版的重点是购物车,商品数据静态化能减少干扰。

<%@ page import="java.util.*, com.example.cart.Goods" %> <% // 模拟商品数据,实际项目替换为DAO查询 List<Goods> goodsList = new ArrayList<Goods>(); goodsList.add(new Goods(1, "机械键盘", 299.0)); goodsList.add(new Goods(2, "无线鼠标", 89.0)); goodsList.add(new Goods(3, "显示器支架", 159.0)); request.setAttribute("goodsList", goodsList); %>

用request.setAttribute而不是session,因为商品列表是每次请求都重新生成的,放session反而占内存。页面下半部分用JSTL或Java片段遍历输出,每个商品后面跟一个链接:

<a href="addToCart.jsp?id=1">加入购物车</a>

id通过URL参数传给处理页,处理页用request.getParameter("id")取。

4.2 加入购物车的session操作

addToCart.jsp是核心逻辑所在,完整流程是:取id → 查商品 → 取session购物车 → 判断是否存在 → 存在则数量加一,不存在则新建 → 重定向。

<%@ page import="java.util.*, com.example.cart.*" %> <% // 1. 取参数 String idStr = request.getParameter("id"); if (idStr == null) { response.sendRedirect("index.jsp"); return; } int id = Integer.parseInt(idStr); // 2. 根据id找商品(简单版从模拟数据里找) Goods target = null; List<Goods> all = new ArrayList<Goods>(); all.add(new Goods(1, "机械键盘", 299.0)); all.add(new Goods(2, "无线鼠标", 89.0)); all.add(new Goods(3, "显示器支架", 159.0)); for (Goods g : all) { if (g.getId() == id) { target = g; break; } } if (target == null) { response.sendRedirect("index.jsp"); return; } // 3. 取或建购物车 Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<Integer, CartItem>(); } // 4. 已存在则累加,否则新建 if (cart.containsKey(id)) { CartItem item = cart.get(id); item.setQuantity(item.getQuantity() + 1); } else { cart.put(id, new CartItem(target, 1)); } // 5. 写回session并跳转 session.setAttribute("cart", cart); response.sendRedirect("index.jsp"); %>

第4步的containsKey判断是购物车逻辑的关键。如果不判断直接put,同一种商品会被覆盖,数量永远是1。第5步的sendRedirect必须写在所有out输出之前,否则会报“response已提交”的异常。这也是常见翻车点:jsp开头有换行或空格输出,重定向就失效。

4.3 购物车展示页的遍历与计算

cart.jsp从session取Map,遍历values,输出表格。总价在遍历时累加。

<%@ page import="java.util.*, com.example.cart.*" %> <% Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); double total = 0.0; %> <table border="1"> <tr><th>商品</th><th>单价</th><th>数量</th><th>小计</th><th>操作</th></tr> <% if (cart != null && !cart.isEmpty()) { for (CartItem item : cart.values()) { total += item.getSubtotal(); %> <tr> <td><%= item.getGoods().getName() %></td> <td><%= item.getGoods().getPrice() %></td> <td><%= item.getQuantity() %></td> <td><%= item.getSubtotal() %></td> <td><a href="removeFromCart.jsp?id=<%= item.getGoods().getId() %>">删除</a></td> </tr> <% } } else { %> <tr><td colspan="5">购物车为空</td></tr> <% } %> <tr><td colspan="4">总计</td><td><%= total %></td></tr> </table>

total在循环外声明、循环内累加,这是标准写法。注意cart为null和为空要分开判断,null说明从没加过,空说明加过但删光了,提示语可以不同,简单版统一处理也行。

4.4 删除条目的实现与重定向

removeFromCart.jsp逻辑比加入简单:取id,从Map里remove,写回session,重定向回cart.jsp。

<% String idStr = request.getParameter("id"); if (idStr != null) { int id = Integer.parseInt(idStr); Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart != null) { cart.remove(id); session.setAttribute("cart", cart); } } response.sendRedirect("cart.jsp"); %>

这里有个细节:remove之后要不要判断cart是否为空再决定是否removeAttribute?简单版不判断,因为空Map和null在展示页都能处理。但如果课设要求“购物车清空后释放session”,就在remove后加if(cart.isEmpty()) session.removeAttribute("cart")。

5. 避坑与常见问题排查:session购物车最容易翻车的五个点

5.1 刷新页面数量翻倍

现象:在addToCart.jsp按F5,购物车数量一直涨。原因:处理页用了forward或者直接输出,刷新时浏览器重新提交了上一次的请求。解决:所有处理逻辑页最后必须用response.sendRedirect跳转,且jsp开头不能有任何HTML输出,包括空行。我一般会在处理页第一行就写<%@ page trimDirectiveWhitespaces="true" %>。

5.2 中文商品名乱码

现象:商品名显示成问号或方块。原因:Tomcat默认按ISO-8859-1解析GET参数,而页面用UTF-8。解决:在接收参数的jsp顶部加request.setCharacterEncoding("UTF-8"),并且Tomcat的server.xml里Connector加URIEncoding="UTF-8"。两个都做才稳。

5.3 session取出来是null但明明加过

现象:cart.jsp显示购物车为空,但addToCart确实执行了。原因:浏览器禁用了cookie,JSESSIONID传不过去,每次请求都是新会话。解决:课设演示时换一个没禁用cookie的浏览器,或者用URL重写response.encodeURL()。简单版通常不处理,但答辩时被问到要能答出来。

5.4 类型转换警告和ClassCastException

现象:Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart")报黄色警告,或者运行时报ClassCastException。原因:泛型擦除,或者session里存的根本不是Map。解决:确保setAttribute和getAttribute的类型一致,加@SuppressWarnings("unchecked")消除警告。如果报ClassCastException,检查是不是在别处往"cart"这个key里存了别的类型。

5.5 重启Tomcat购物车丢失

现象:演示到一半重启服务器,购物车清空。原因:session默认存在内存,Tomcat停止时序列化到work目录,但简单版没配持久化。解决:演示前不要重启,或者提前跟导师说明这是session的预期行为。如果课设要求持久化,把session里的Map序列化到文件或数据库,但那就不是简单版了。

6. 验收与答辩:怎么证明你的购物车真的用对了session

验收不是把页面点一遍就完事,要能拿出证据说明session在起作用。我一般会让学生按下面这张表逐项自检,每项都能在答辩时当场演示:

验收项操作预期结果考察点
跨请求保持加入商品后跳转再回购物车商品还在session生命周期
同商品累加同一商品点两次加入数量为2,不是两条记录Map的containsKey
删除生效删除某商品后刷新该商品消失,总价减少remove与重定向
空车处理删光所有商品显示“购物车为空”,不报错null与empty判断
会话隔离换一个浏览器访问购物车为空session按会话隔离
刷新不重复在处理页按F5数量不变sendRedirect防重复提交

答辩时最容易被问的三个问题,提前准备好答案:第一,“为什么用session不用cookie”,答数据在服务端更安全,且不需要持久化。第二,“session什么时候销毁”,答默认30分钟超时或调用invalidate,浏览器关闭不一定立即销毁,取决于cookie是否持久化。第三,“如果两个用户同时访问会不会串”,答不会,每个浏览器会话有独立JSESSIONID,session互相隔离。

从那以后我每次带课设,都强制学生先在纸上画出“请求→session读写→重定向”的箭头图,再动手写代码。图画对了,代码基本不会翻车。希望这套购物车案例的拆解能帮到你,需要完整可运行代码的,按资源名获取即可。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 4:00:48

内网离线安装Docker与Docker Compose实战指南

简介&#xff1a;这份资源面向需要在无外网环境下部署容器服务的内网运维与开发人员&#xff0c;提供一套完整的离线安装方案&#xff0c;解决服务器无法访问公网、依赖缺失导致 Docker 与 Docker Compose 装不上的问题。压缩包共 22 个文件&#xff0c;约 120.91MB&#xff0c…

作者头像 李华
网站建设 2026/10/16 3:59:57

字节码指令全解析:从javap到JVM性能调优实战

调试线上问题的时候&#xff0c;我习惯先看一眼javap -c的输出。这不是强迫症&#xff0c;而是很多看起来玄乎的性能问题&#xff0c;最后都能在字节码指令这一层找到最原始的答案。class 文件里的字节码指令&#xff0c;是 Java 源码和 JVM 运行时之间的桥梁&#xff0c;也是 …

作者头像 李华
网站建设 2026/10/16 3:59:51

Agora Web SDK NG版接入实战:Web音视频通话落地关键决策指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 3:59:39

typed.js实战:前端打字机效果从入门到生产环境避坑

最近接了个落地页优化需求&#xff0c;对方提了一嘴&#xff1a;品牌 Slogan 能不能像打字机一样一个字一个字敲出来&#xff0c;敲完再删掉换下一句。说实话&#xff0c;这种效果在营销站、个人主页、产品 Landing Page 上太常见了&#xff0c;但真到自己动手写的时候才发现&a…

作者头像 李华
网站建设 2026/10/16 3:59:03

睁闭眼分类实战:小样本、低算力下的工业级视觉模型设计

1. 项目概述&#xff1a;为什么一个“睁闭眼分类”值得认真做一遍你有没有在某个深夜调试模型时&#xff0c;盯着训练日志里那条忽高忽低的val_accuracy曲线发呆&#xff1f;明明数据就几百张图&#xff0c;网络结构也就五六层&#xff0c;为什么loss就是卡在0.35上不往下走&am…

作者头像 李华
网站建设 2026/10/16 3:59:03

面向过程与面向对象的本质区别:状态管理方式决定工程选择

1. 这不是概念辨析&#xff0c;而是两种思维模式的实战分水岭你有没有遇到过这样的情况&#xff1a;写一个简单的计算器程序&#xff0c;面向过程的代码三下五除二就跑通了&#xff1b;可一旦要加个“历史记录”功能、再加个“皮肤切换”、最后还要支持“多人协作计算”&#x…

作者头像 李华