简介:这份购物车案例【简单版】面向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读写→重定向”的箭头图,再动手写代码。图画对了,代码基本不会翻车。希望这套购物车案例的拆解能帮到你,需要完整可运行代码的,按资源名获取即可。
本文还有配套的精品资源,点击获取