JSP购物车案例实战:基于Session的Web开发核心原理与实现
简介这份购物车案例【简单版】面向Java Web初学者与课程设计者用JSP、Servlet、Tomcat、Session与数据库搭建一个可运行的在线购物车帮助理解商品展示、加入购物车、删除商品等基础业务闭环。压缩包共62个文件约916KB包含13个java源码、26个class、6个jsp页面、5个xml配置、4个jar依赖以及少量html、js与工程配置文件覆盖实体、DAO、Service、Servlet分层与前端页面。已有670人学习下载。案例将购物车数据存放在服务端Session中配合JDBC持久化商品信息能直观看到HTTP请求、Servlet处理与JSP渲染的协作过程并涉及Session过期、SQL注入与XSS等安全与性能注意点适合作为Web入门练手或课程设计参考。1. 从一份 JSP 购物车案例说起为什么 session 才是核心做 Web 开发课设或毕设时购物车几乎是绕不开的经典题目。很多同学第一反应是用数据库存购物车数据觉得这样才“正规”。但如果你拿到的是一份基于 JSP Java Tomcat 的简单版购物车案例你会发现它压根没连数据库所有商品条目都挂在 session 里。这不是偷懒而是一个刻意为之的教学设计——它把 HTTP 无状态这个核心矛盾摆在你面前逼你用 session 去解决“服务器怎么记住你是谁”这个问题。这份资源适合正在做 Java Web 课设、需要快速跑通一个完整购物车流程的学生也适合想拿它当模板改造成自己项目底座的开发者。它不追求功能大而全而是把 JSP 页面、Servlet 跳转、session 作用域、HTML 表单提交这条链路串得清清楚楚让你能在一个下午内看到从商品展示到加入购物车、修改数量、删除条目、清空购物车的完整闭环。2. 需求拆解与技术选型为什么是 JSP session 而不是数据库2.1 购物车到底要解决哪几个动作先把需求拆到不能再拆。一个最简购物车用户侧只有四个动作浏览商品列表、把商品加入购物车、修改某件商品的数量、删除某件商品或清空整个购物车。后台侧不需要登录注册不需要订单结算不需要库存扣减。这意味着数据不需要持久化——用户关掉浏览器购物车清空是完全可以接受的。一旦你接受这个前提数据库就成了过度设计。用 session 存一个MapString, Integer或者ListCartItem键是商品 ID值是数量内存里操作页面刷新不丢关掉会话就释放干净利落。2.2 技术栈的职责边界这份案例的技术栈是 JSP Java Tomcat HTML session。分工是这样的HTML 负责静态结构和表单控件JSP 负责在页面里嵌入 Java 代码来读取 session 中的数据并渲染Servlet 或者 JSP 里的 Java 代码块负责处理“加入购物车”“修改数量”这些动作Tomcat 作为 Servlet 容器管理 session 的生命周期。session 是贯穿始终的那根线。很多同学写购物车时把逻辑全堆在 JSP 里用% %脚本段一把梭能跑但没法维护。更合理的做法是把购物车操作封装成一个Cart类JSP 只负责调用和展示。2.3 为什么不用 Cookie 或 ApplicationCookie 存在客户端大小有限制而且用户可以禁用存购物车这种结构化数据很别扭。Application 作用域是整个 Web 应用所有用户共享一个购物车张三加的商品李四能看到这显然不对。session 的作用域是单个用户会话正好匹配“每个人的购物车是独立的”这个业务语义。Tomcat 默认用 JSESSIONID 这个 Cookie 来跟踪 session如果浏览器禁用了 Cookie还可以用 URL 重写兜底但简单版案例通常不处理这个边界情况。提示如果你后续要把这个案例改造成多用户系统session 里应该只存购物车数据用户身份信息单独存两者不要混在一个对象里。3. 总体设计三层结构与 session 数据模型3.1 页面流转与模块划分整个案例可以拆成三个 JSP 页面加一个 Java 类。商品列表页index.jsp展示所有可购买商品每个商品旁边有一个“加入购物车”的表单按钮。购物车展示页cart.jsp读取 session 中的购物车数据用表格渲染出来每行有数量输入框和“更新”“删除”按钮。操作处理页cartAction.jsp或者一个 Servlet 负责接收表单提交修改 session 中的数据然后重定向回cart.jsp。Cart类封装了添加、更新、删除、清空、计算总价这几个方法。页面之间不直接传数据所有状态都通过 session 共享。3.2 session 中购物车的数据结构我一般会用LinkedHashMapString, Integer来存购物车键是商品 ID值是数量。用LinkedHashMap而不是HashMap是为了保持加入顺序展示时不会乱跳。商品本身的名称和单价不存进 session而是放在一个静态的Product列表里展示时根据 ID 去查。这样做的好处是 session 里只存最小必要信息减少内存占用也避免商品信息变更后购物车里的旧数据不一致。// Cart.java —— 购物车核心类挂在 session 中 import java.util.LinkedHashMap; import java.util.Map; public class Cart { // 键为商品ID值为数量LinkedHashMap 保证插入顺序 private MapString, Integer items new LinkedHashMap(); // 添加商品如果已存在则数量累加 public void add(String productId, int quantity) { items.put(productId, items.getOrDefault(productId, 0) quantity); } // 更新指定商品的数量数量小于等于0时移除 public void update(String productId, int quantity) { if (quantity 0) { items.remove(productId); } else { items.put(productId, quantity); } } // 删除某件商品 public void remove(String productId) { items.remove(productId); } // 清空购物车 public void clear() { items.clear(); } // 获取所有条目供 JSP 遍历渲染 public MapString, Integer getItems() { return items; } }这段代码的关键在于add方法用了getOrDefault避免第一次添加时出现空指针。update方法里数量小于等于 0 就删除这样用户在数量框里输入 0 或负数时行为是符合直觉的。getItems直接返回内部 Map简单版可以这样写但如果要考虑封装性可以返回一个不可修改的视图或者副本。3.3 商品数据的组织方式商品列表不放在数据库里而是用一个静态初始化块或者一个单独的Product类来维护。每个商品有 ID、名称、单价三个字段。在index.jsp里遍历这个列表生成 HTML 表格。这样做的好处是案例可以独立运行不需要任何外部依赖解压放到 Tomcat 的 webapps 目录下就能跑。// Product.java —— 商品实体简单版用静态列表模拟数据源 public class Product { private String id; private String name; private double price; public Product(String id, String name, double price) { this.id id; this.name name; this.price price; } // 省略 getter 和 setter // 静态商品列表实际项目中应替换为数据库查询 public static java.util.ListProduct getAll() { java.util.ListProduct list new java.util.ArrayList(); list.add(new Product(P001, 机械键盘, 299.00)); list.add(new Product(P002, 无线鼠标, 89.00)); list.add(new Product(P003, 显示器支架, 159.00)); list.add(new Product(P004, USB集线器, 45.00)); return list; } }商品 ID 用字符串而不是整数是为了将来对接数据库时更灵活。单价用double在简单版里够用但如果涉及金额计算正式项目应该用BigDecimal这一点在课设答辩时如果被问到能答上来是加分项。4. 核心模块实现从加入购物车到数量修改4.1 商品列表页与加入购物车表单index.jsp的任务是展示商品并提供一个提交入口。每个商品行是一个独立的表单提交到cartAction.jsp附带一个隐藏字段actionadd和商品 ID。这样用户点“加入购物车”时页面会跳转到处理页处理完再重定向回来。!-- index.jsp 片段商品列表与加入购物车表单 -- % page contentTypetext/html;charsetUTF-8 languagejava % % page importjava.util.List % % page importcom.example.Product % html headtitle商品列表/title/head body h2商品列表/h2 table border1 cellpadding8 trth商品名称/thth单价/thth操作/th/tr % ListProduct products Product.getAll(); for (Product p : products) { % tr td% p.getName() %/td td% p.getPrice() %/td td form actioncartAction.jsp methodpost input typehidden nameaction valueadd input typehidden nameproductId value% p.getId() % input typesubmit value加入购物车 /form /td /tr % } % /table a hrefcart.jsp查看购物车/a /body /html这里用 JSP 脚本段遍历商品列表生成表格行。每个表单的action隐藏字段告诉处理页要执行什么操作。商品 ID 也通过隐藏字段传递。表单方法用post而不是get因为加入购物车是一个写操作用get会导致刷新页面时重复提交。4.2 购物车操作处理页的逻辑cartAction.jsp是控制层它不输出 HTML只负责处理请求然后重定向。它从 session 中取出Cart对象如果不存在就创建一个新的放进去。然后根据action参数决定调用哪个方法。!-- cartAction.jsp —— 处理购物车操作处理完重定向 -- % page contentTypetext/html;charsetUTF-8 languagejava % % page importcom.example.Cart % % // 从 session 获取购物车没有则新建 Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } String action request.getParameter(action); String productId request.getParameter(productId); if (add.equals(action)) { cart.add(productId, 1); } else if (update.equals(action)) { // 更新数量时从请求中读取 quantity 参数 int quantity Integer.parseInt(request.getParameter(quantity)); cart.update(productId, quantity); } else if (remove.equals(action)) { cart.remove(productId); } else if (clear.equals(action)) { cart.clear(); } // 处理完重定向回购物车页面避免刷新重复提交 response.sendRedirect(cart.jsp); %这段代码里session.getAttribute(cart)是核心。第一次访问时 session 里没有 cart就 new 一个放进去。后续所有操作都基于这个 session 中的对象。sendRedirect而不是forward是为了让浏览器地址栏变成cart.jsp用户刷新时不会重复执行上一次操作。这是一个容易被忽略但很重要的细节。4.3 购物车展示页与数量修改cart.jsp从 session 取出 cart遍历条目根据商品 ID 查出商品信息渲染成表格。每行有一个数量输入框和一个“更新”按钮还有一个“删除”按钮。表格底部显示总价和“清空购物车”按钮。!-- cart.jsp 片段购物车展示与数量修改 -- % page contentTypetext/html;charsetUTF-8 languagejava % % page importcom.example.Cart % % page importcom.example.Product % % page importjava.util.Map % html headtitle购物车/title/head body h2我的购物车/h2 % Cart cart (Cart) session.getAttribute(cart); if (cart null || cart.getItems().isEmpty()) { % p购物车是空的。/p % } else { double total 0; % table border1 cellpadding8 trth商品/thth单价/thth数量/thth小计/thth操作/th/tr % for (Map.EntryString, Integer entry : cart.getItems().entrySet()) { String pid entry.getKey(); int qty entry.getValue(); // 根据 ID 查找商品信息 Product p null; for (Product prod : Product.getAll()) { if (prod.getId().equals(pid)) { p prod; break; } } if (p null) continue; double subtotal p.getPrice() * qty; total subtotal; % tr td% p.getName() %/td td% p.getPrice() %/td td form actioncartAction.jsp methodpost styledisplay:inline; input typehidden nameaction valueupdate input typehidden nameproductId value% pid % input typenumber namequantity value% qty % min0 stylewidth:60px; input typesubmit value更新 /form /td td% subtotal %/td td form actioncartAction.jsp methodpost styledisplay:inline; input typehidden nameaction valueremove input typehidden nameproductId value% pid % input typesubmit value删除 /form /td /tr % } % tr td colspan3总计/td td% total %/td td form actioncartAction.jsp methodpost input typehidden nameaction valueclear input typesubmit value清空购物车 /form /td /tr /table % } % a hrefindex.jsp继续购物/a /body /html这段代码里数量输入框用了typenumber和min0浏览器会做基本校验。更新和删除是两个独立的表单各自提交不同的action。总价在遍历时累加。如果商品 ID 在商品列表中找不到用continue跳过避免空指针。这个页面是用户停留时间最长的页面渲染逻辑要尽量清晰。5. 避坑与常见问题session 购物车最容易翻车的五个点5.1 中文乱码商品名变成问号现象是页面上的中文商品名显示为???或者乱码。原因是 Tomcat 默认用 ISO-8859-1 解码请求参数而 JSP 页面用 UTF-8 编码。解决方式是在每个 JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 %并且在处理 POST 请求的页面里获取参数之前调用request.setCharacterEncoding(UTF-8)。如果用的是 Tomcat 8 以上GET 请求的默认编码已经是 UTF-8但 POST 仍然需要手动设置。5.2 刷新导致重复添加现象是用户在cartAction.jsp处理完后按 F5商品又被加了一次。原因是处理页直接输出内容而没有重定向浏览器刷新时重新提交了上一次的 POST 请求。解决方式就是前面代码里用的response.sendRedirect(cart.jsp)让浏览器发起一个新的 GET 请求刷新时不会重复提交。这个坑几乎每个初学者都会踩一次。5.3 session 丢失或购物车为空现象是明明加了商品跳到购物车页面却显示空的。原因可能有几个一是浏览器禁用了 CookieJSESSIONID 无法传递二是 Tomcat 重启导致 session 丢失三是代码里在某处调用了session.invalidate()或者新建了 session。排查时先在cartAction.jsp里打印session.getId()再在cart.jsp里打印一次看是不是同一个 session。如果 ID 不同说明 session 没有保持住。5.4 数量输入非数字导致 500 错误现象是用户在数量框里输入字母或者空值提交后页面报 500。原因是Integer.parseInt抛出了NumberFormatException。解决方式是在解析之前做校验用 try-catch 包住或者用正则先判断是不是数字。更优雅的做法是在前端用typenumber限制输入但后端仍然要做防御性校验因为用户可以绕过前端。5.5 商品 ID 为空或不存在现象是提交了一个不在商品列表里的 ID购物车里出现一条查不到商品信息的条目页面渲染时小计变成 NaN 或者报错。原因是没有对productId做有效性校验。解决方式是在add之前先检查这个 ID 是否存在于商品列表中不存在就不加入。或者在展示时对找不到的商品做跳过处理就像前面cart.jsp里用continue那样。注意session 默认超时时间是 30 分钟可以在web.xml里用session-config调整。如果课设要求“购物车保持更久”可以改成 60 分钟但不要设成无限否则服务器内存会被占满。6. 验收与答辩怎么证明你的购物车真的跑通了验收这个案例不需要复杂的测试工具但要有清晰的检查路径。我一般会按下面这个顺序走一遍每一步都截图或者录屏答辩时直接展示。检查项操作步骤预期结果商品展示访问index.jsp四件商品全部显示中文无乱码加入购物车点击某商品“加入购物车”跳转到购物车页该商品出现数量为 1重复加入再次点击同一商品“加入购物车”该商品数量变为 2不新增行修改数量将数量改为 5点“更新”数量变为 5小计和总计同步更新删除商品点击某商品“删除”该行消失总计减少清空购物车点击“清空购物车”所有行消失显示“购物车是空的”会话隔离打开另一个浏览器或无痕窗口访问购物车为空与第一个浏览器互不影响刷新防重在购物车页按 F5页面正常刷新不会重复添加商品答辩时老师最常问的几个问题session 和 Cookie 的区别是什么为什么购物车不用数据库如果两个用户同时访问购物车会不会混前两个问题前面已经讲清楚了。第三个问题的答案是每个用户有独立的 JSESSIONIDTomcat 根据这个 ID 分配不同的 session 对象所以不会混。如果老师追问“那 session 什么时候销毁”答“默认 30 分钟无活动后销毁或者调用 invalidate 手动销毁”。还有一个容易被忽略的点cartAction.jsp处理完重定向到cart.jsp但如果用户直接在地址栏输入cartAction.jsp而不带任何参数代码里的action是 null所有 if 分支都不进最后仍然重定向到cart.jsp不会报错。这个边界情况在答辩时如果被问到能说出来说明你考虑得比较周全。从那以后我每次做这种 session 相关的课设都会在验收前强制走一遍“两个浏览器互不干扰”的测试因为这是最容易翻车的地方——本地开发时往往只开一个浏览器觉得没问题一到答辩现场老师开两个窗口就露馅了。希望这份拆解能帮你把购物车案例真正跑通也能在答辩时答到点子上。本文还有配套的精品资源点击获取