ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

JavaWeb简易购物车源码拆解:Servlet、JSP与Session协作实战

JavaWeb简易购物车源码拆解:Servlet、JSP与Session协作实战 简介JavaWeb简易购物车源代码是一份面向JavaWeb初学者的完整实战项目包覆盖Servlet与JSP、Session会话管理、MVC分层、JDBC数据库操作等核心技术适合想通过具体案例理解购物车业务流程的开发者。压缩包共54个文件核心为14个Java源码、6个JSP页面、14个编译后的class文件并附带属性配置、数据库脚本与项目安装说明整体仅1.58MB轻量易导入。该资源已有4125人学习下载热度可观。除了可直接运行的购物车增删改查功能还配套笔记资料逐项解释关键代码与数据库表设计目录包含MyShopping、开发文档、.settings等模块便于对照源码研读。借助这份资料开发者能掌握从商品展示、加入购物车到金额统计的完整逻辑对搭建同类Web业务很有帮助。1. 从一份能跑通的 javaWeb 简易购物车源码入手先解决“Servlet JSP Session 怎么协作”的问题搜“java购物车”相关的资料时最常见的两类结果很极端一是完整电商系统源码几百个类先让你把 Maven、Spring、MyBatis 全学一遍二是只丢出几个 JSP 文件连请求是怎么走到 Servlet 的都没交代。这份 javaWeb 简易购物车源代码恰好落在中间核心 Java 类不到十个部署到 Tomcat 后从商品列表、加入购物车、改数量、删除、清空到算总价一条链路完整跑通。它要回答的核心问题是购物车这种必须跨页面保持状态的功能为什么用 HttpSession 而不是 Cookie以及在增删改查的边界逻辑上到底有哪些坑。适合刚学完 Servlet/JSP 基础、想验证自己是否真能的开发者也适合正在准备课程设计参考的导师。接下来每章我都按这份资源的实际结构来拆顺序即是从模型到视图再到排错。2. 把购物车对象模型讲透CartItem、Cart 的取舍与 HttpSession 的选择理由2.1 CartItem 只保留四类字段用商品快照理解单价和数量打开这份源码先看实体类购物车每一行被建模成一个非常朴素的 POJO字段只有四个商品编号、商品名称、单价、购买数量。没有数据库外键没有创建时间也没有图片地址。这不是偷懒而是教学资源的定位决定的——字段越少越能看清购物车这个状态对象到底在传递什么。商品名称和单价不是从数据库实时查出来的而是在用户点击“加入购物车”那一刻被复制进来的。这个细节很多初学者会忽略但它正是真实电商系统里“商品快照”概念的简化版。商品价格在后台可以随时改购物车里已经加好的那行却应该保持用户当时看到的价格。如果每次刷新购物车都重新读一次商品表会出现“加到一半价格变了”的尴尬场景。常见做法是 CartItem 里再存一个快照时间戳甚至把参与促销活动的满减规则也复制一份而这份教学资源的简化方式是从商品列表页跳转时把 price 和 name 作为参数带过来写进 CartItem。package com.demo.cart; public class CartItem { private String productId; // 商品编号购物车容器里的 key private String name; // 商品名称加入时的快照 private double price; // 单价注意这是简化场景下的 double private int quantity; // 购买数量加入购物车时通常是 1 public CartItem() {} public CartItem(String productId, String name, double price, int quantity) { this.productId productId; this.name name; this.price price; this.quantity quantity; } // 每一行的小计由操作层统一调用 public double getSubtotal() { return this.price * this.quantity; } // 以下为 getter / setter public String getProductId() { return productId; } public void setProductId(String productId) { this.productId productId; } public String getName() { return name; } public void setName(String name) { this.name name; } public double getPrice() { return price; } public void setPrice(double price) { this.price price; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity quantity; } }这个类的逻辑说明很直接getSubtotal() 是购物车行小计由 Cart 汇总时循环调用四个字段全部通过 getter 暴露给 JSP 页面使用JSP 里 ${item.name} 实际上就是调用 getName()。我一般会把 setQuantity 方法的入参命名为 quantity 而不是 count是因为数量在购物车语义里强调“当前持有的件数”count 容易和购物车总行数混淆。参数说明放在这里也很重要price 用 double 是这份资源的有意简化真实项目中应使用 BigDecimal我在第 6 章会专门讲替换方案。productId 是 String 而不是 int是为了兼容带前缀的商品编码比如 P001、SKU-A1001 这类格式。2.2 Cart 用 LinkedHashMap 存商品行按添加顺序展示且支持合并Cart 类没有用 List 也没有用普通 HashMap而是选择了 LinkedHashMapString, CartItem。这是一个值得停下来讲半分钟的选型。普通 HashMap 遍历顺序不保证插入顺序用户加了三件商品打开购物车看到的顺序可能随机颠倒体验很怪。List 可以保顺序但“查找某个商品是否已经在购物车里”需要线性遍历每次添加都要 O(n) 扫描代码还得写在 Servlet 层很容易漏掉“数量合并”的分支。LinkedHashMap 在内部用双向链表维护插入顺序key 是商品编号value 是 CartItem。这样 add 方法可以直接 containsKey 判断更新已有行时也是 O(1) 定位。对“简易购物车”这种最多几十行的数据量来说这点内存开销完全可忽略但表达力强很多。package com.demo.cart; import java.util.LinkedHashMap; import java.util.Map; public class Cart { // 用有序 Map 保存购物车行productId 作为唯一键 private MapString, CartItem items new LinkedHashMap(); // 加入商品已存在则数量相加不存在则新增整行 public void add(CartItem item) { if (item null || item.getProductId() null) { return; } if (items.containsKey(item.getProductId())) { CartItem existing items.get(item.getProductId()); existing.setQuantity(existing.getQuantity() item.getQuantity()); } else { items.put(item.getProductId(), item); } } // 删除某一行直接按商品编号移除 public void remove(String productId) { items.remove(productId); } // 修改数量减到 0 或负数时把该行从购物车移除 public void updateQuantity(String productId, int quantity) { CartItem item items.get(productId); if (item null) { return; } if (quantity 0) { items.remove(productId); } else { item.setQuantity(quantity); } } // 结算总价累加每一行的小计 public double getTotalPrice() { double total 0; for (CartItem item : items.values()) { total item.getSubtotal(); } return total; } // 商品总件数把每行的数量累加注意不是行数 public int getTotalCount() { int count 0; for (CartItem item : items.values()) { count item.getQuantity(); } return count; } public MapString, CartItem getItems() { return items; } }这里最关键的方法是 updateQuantity。它的参数约定是“直接把数量改成传入值”这个设计决定了前端“”“-”按钮的 URL 必须带上计算后的新数量比如当前数量是 3点一次“-”就传 qty2点“”就传 qty4。另一种设计是传 delta增减量让 Servlet 自己算两种都能工作但这个教学项目选了前者因为 JSP 页面里 EL 表达式写 ${item.quantity - 1} 非常直观。缺点是每次操作前前端必须知道当前数量所以页面渲染时必须把 quantity 完整输出。getTotalCount 和 getItems().size() 的区别也会有人踩前者是商品件数总和后者是购物车行数。用户加了三个同款商品总件数是 3总行数是 1这不是 bug是两种不同的业务口径。在页面上展示“共 N 件商品”应该调用 getTotalCount()。2.3 购物车放 Session 不放 Cookie 和数据库三者的边界在哪购物车状态到底放哪是理解这份源码最重要的前置问题。这份 javaWeb 购物车把 Cart 对象直接塞进 HttpSessionJSP 里通过 ${sessionScope.cart} 取出来。为什么是 Session第一个理由是安全边界。Session 数据存在服务端浏览器里只有一个 JSESSIONID 的会话标识字符串用户无法直接篡改里面的商品数量和价格。Cookie 虽然也能存购物车但容量上限通常只有 4KB而且如果直接把 Cart JSON 序列化进 Cookie懂行的人改一下价格参数再提交后端如果没做校验就会收到一个异常账单。当然Session 也不是绝对安全如果商品价格快照能被越权修改那是应用层的问题不是容器的问题。第二个理由是生命周期。Session 天然由容器管理默认 30 分钟无操作就失效购物车也跟着消失这正好符合“临时意愿单”的语义。数据库表可以用来做持久化购物车用户登录后跨设备同步但每个页面动作都要读写数据库对这个只有几页的教学项目来说太重了。Cookie 的问题在于跨浏览器清换、不同浏览器行为不一致而且容器对 Cookie 大小有硬限制存不了多少商品。结论是这份资源的 Session 方案本质上是把“购物车是会话级别临时状态”这件事讲清楚了。真实项目里如果要做“用户关闭浏览器再打开购物车还在”的需求才需要引入数据库或 Redis。这里有一个容易混淆的细节Session 超时后购物车空了但用户仍然可以在商品列表重新加购这不是程序崩溃而是容器按照会话超时策略回收了内存对象。我在 5.1 里会专门讲现实遇到的现象和解法。3. 请求链路拆开看商品列表到购物车页的四个动作与参数约定3.1 商品数据源先用内存集合模拟再切 JDBC 连接 MySQL这份源码里的商品数据最简版本是 Servlet 里 static 一个 List 访问商品列表页时循环输出。这能让下载者免去配置数据库的步骤直接部署就跑通。但如果你拿这份资源当课程设计交我建议至少把它换成一个 JDBC 查询的 ProductDao否则答辩时老师问“商品从哪来”会很尴尬。常见的做法是先用一个内存集合模拟把整个链路验证完再替换数据访问层。商品实体类同样用 POJO 表示productId、name、price有需要再加 stock 库存字段。数据源可以是 MySQL 表 product字段命名尽量和下划线风格对齐避免在 JSP 里出现属性映射错误。public class ProductDao { private static final ListProduct PRODUCT_LIST Arrays.asList( new Product(P001, Java 核心技术, 89.00), new Product(P002, Spring 实战, 108.00), new Product(P003, MySQL 必知必会, 59.00) ); // 按商品编号查单个商品 public Product findById(String id) { for (Product p : PRODUCT_LIST) { if (p.getProductId().equals(id)) { return p; } } return null; } // 查全部用于商品列表页 public ListProduct findAll() { return PRODUCT_LIST; } }这段代码的逻辑说明findById 是加入购物车时被调用的Servlet 拿到商品编号后通过这个方法把商品名称、单价取出来封装成 CartItem。使用内存集合时每次请求都会重复 new 这些 Product 对象吗不是static 集合在类加载时初始化一次常驻内存所以多用户同时访问也不会把同一个 Product 实例写坏——CartItem 是每次加入时单独 new 的。参数说明如果把 PRODUCT_LIST 换成 JDBC 查询返回类型保持 List 不变那么商品列表 JSP 和购物车 Servlet 都不用改这是“面向接口”最简单的收益。连接方式一般用 Class.forName(com.mysql.cj.jdbc.Driver) 加 DriverManager.getConnection注意 MySQL 8 之后驱动类名是 com.mysql.cj.jdbc.Driver老文章写的 com.mysql.jdbc.Driver 在 MySQL 8 下会直接 ClassNotFoundException。3.2 一个 Servlet 通过 action 参数分发四大操作购物车所有的增删改操作这份源码里统一交给一个 CartServlet通过请求参数 action 的值来分发而不是拆成 AddCartServlet、RemoveCartServlet 多个类。这是洁癖式开发的早期简化版本也是很多真实项目的做法一个 CartControlleraction 对应一个方法。WebServlet(/cart) public class CartServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { doPost(request, response); } protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (action null || action.isEmpty()) { action list; } // 获取会话中的购物车第一次访问时创建 HttpSession session request.getSession(); Cart cart (Cart) session.getAttribute(cart); if (cart null) { cart new Cart(); session.setAttribute(cart, cart); } switch (action) { case add: String id request.getParameter(id); ProductDao dao new ProductDao(); Product p dao.findById(id); if (p ! null) { cart.add(new CartItem(p.getProductId(), p.getName(), p.getPrice(), 1)); } break; case remove: cart.remove(request.getParameter(id)); break; case update: int qty Integer.parseInt(request.getParameter(qty)); cart.updateQuantity(request.getParameter(id), qty); break; case clear: session.removeAttribute(cart); break; default: break; } // 重定向到购物车页面避免刷新时重复提交 response.sendRedirect(request.getContextPath() /cart.jsp); } }这段是核心链路。doGet 转发给 doPost是因为商品列表页里的“加入购物车”用的是普通超链接GET而真正的数据修改写在 doPost 里统一入口后逻辑不会散。response.sendRedirect 而不是 request.getRequestDispatcher 转发是为了解决浏览器刷新重复提交的问题。distinguish 一个很容易混淆的点if (cart null) 这个判断处理的是“Session 超时后用户又打开商品页点击加入”的情况此时 session 还是同一个但 cart 属性没了重新创建一个是最合理的恢复方式。参数约定说明action 的合法值是 add、remove、update、clear大小写敏感JSP 页面拼 URL 时不要拼成 Add。id 对应商品编号qty 是目标数量而非增量。如果传入的 qty 不是数字Integer.parseInt 会抛 NumberFormatException这是教学源码里常见的裸奔点正式项目要包一层 try-catch 或者用 Integer.parseInt 前先做正则校验。3.3 加入购物车与数量更新边界条件的正确处理加入购物车最容易被忽略的边界条件是“同一商品重复添加”。如果商品列表页的加入按钮每次都无脑 put 一个新对象购物车会出现两行完全相同的商品用户体验极差。正确逻辑已经在 Cart.add 里实现了先 containsKey 判断存在则把数量相加。public void add(CartItem item) { if (item null || item.getProductId() null) { return; } if (items.containsKey(item.getProductId())) { CartItem existing items.get(item.getProductId()); existing.setQuantity(existing.getQuantity() item.getQuantity()); } else { items.put(item.getProductId(), item); } }这段逻辑说明它把“数量合并”封装在容器内部Servlet 层完全不用关心某个商品是否已存在。这样做的一个直接好处是商品 id 的边界校验只写一次不会出现在多个 Servlet 分支里。第二个好处是如果以后要做批量加入购物车直接循环调用 add 方法即可容器自己处理合并。数量更新时updateQuantity 的边界条件是“新数量 0”逻辑选择了移除商品行而不是允许负数量存在。这个设计会直接影响前端 JSP数量为 1 时用户点“-”请求过来 qty0购物车里这一行直接消失。很多人不习惯这个行为觉得应该保留 1 行的最小数量那是另一类购物车的业务约定。这份源码选择了更干净的“减到 0 就移除”因为对教学场景来说多一个“是否允许 0 数量”的判断会让逻辑变厚不值得。还有一个隐藏边界购物车的 update 请求如果传入不存在的商品 idupdateQuantity 内部会先 get发现是 null 直接返回不会抛空指针。这个 guard 条件值得所有初学者抄进自己的代码里。4. JSP 输出与金额展示EL 表达式、JSTL 循环和格式化三件套4.1 从 sessionScope 里取购物车用 c:forEach 遍历 Map购物车页面 cart.jsp 是这份源码的门面它要做的事情只有三个遍历购物车行、显示每个商品的小计、给出加减和删除的入口。这里涉及到的技术点是 EL 表达式中的 sessionScope 访问和 JSTL 的 c:forEach 遍历 Map 结构。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html body h3我的购物车/h3 c:if test${empty sessionScope.cart || empty sessionScope.cart.items} p购物车还是空的去商品列表页挑几件吧。/p /c:if c:if test${not empty sessionScope.cart.items} table border1 tr th商品名称/thth单价/thth数量/thth小计/thth操作/th /tr c:forEach varentry items${sessionScope.cart.items} c:set varitem value${entry.value} / tr td${item.name}/td tdfmt:formatNumber value${item.price} maxFractionDigits2/ 元/td td a href${pageContext.request.contextPath}/cart?actionupdateid${item.productId}qty${item.quantity - 1}-/a ${item.quantity} a href${pageContext.request.contextPath}/cart?actionupdateid${item.productId}qty${item.quantity 1}/a /td tdfmt:formatNumber value${item.subtotal} maxFractionDigits2/ 元/td td a href${pageContext.request.contextPath}/cart?actionremoveid${item.productId}删除/a /td /tr /c:forEach /table p总计fmt:formatNumber value${sessionScope.cart.totalPrice} maxFractionDigits2/ 元/p /c:if /body /html这段 JSP 的逻辑说明分成三层。第一层是空购物车判断${empty sessionScope.cart.items} 这个表达式对空 Map 和 null 都返回 true所以不需要先判断 sessionScope.cart 是否为 null。第二层是遍历 Mapc:forEach 的 items 指向 sessionScope.cart.items每次取出的 entry 是 Map.Entryentry.key 是商品编号entry.value 是 CartItem 对象页面上一开始用的 ${item.name} 其实是 ${entry.value.name} 的简写。第三层是金额格式化fmt:formatNumber 的 maxFractionDigits2 控制最多显示两位小数避免把 89.0 显示成 89 或者把 0.30000000000000004 显示成长尾。参数说明pageContext.request.contextPath 是必须的前缀。如果 JSP 页面里的链接没带这个上下文路径部署在 Tomcat 的根路径下没问题但如果应用打包成 war 后部署在 /demo 子路径链接直接写 /cart?actionremove 会变成请求根应用的 /cart结果 404。这份源码所有链接都带 contextPath这是一种安全习惯不要为了省事省掉。4.2 数量加减链接与删除按钮URL 参数动态拼接的注意点加减按钮的实现方式是“把 URL 写成带表达式的动态 href”而不是用表单提交。好处是简单缺点是 URL 里的参数会直接暴露给前端用户可以手动篡改。对教学源码这不是大问题但对真实项目是个需要注意的边界。另一个常见问题是当 item.quantity 为 1 时${item.quantity - 1} 会计算出 0URL 变成 qty0Servlet 更新时会走 remove 分支这一行被移除。页面表现是“数量为 1 时点减号等于删除该商品”行为符合设计。如果你不想让减号在数量为 1 时变成删除可以在 c:forEach 里加一个 c:if 判断数量为 1 时不输出减号链接或让减号置灰。常见做法是加 ${item.quantity 1 ? : disabled}但 EL 三元表达式在 JSP 里写出来略丑我一般会拆成 c:choose 结构。这个资源里的直接删除逻辑其实更接近真实电商的“数量为 1 时继续点减弹出确认”的第一层简化版。删除按钮同样用带 id 参数的链接没有二次确认弹窗。给初学者看这是清晰给真实项目看这是没防抖。如果用户快速点两次删除第一次 remove 已经生效第二次请求带着同一个 id 再 remove 一次——LinkedHashMap 的 remove 对不存在的 key 不会抛异常只是静默返回所以不会出 bug。但如果换成其他容器最好每次 remove 前都判断 containsKey。4.3 结算金额与购物车总计显示精度和业务口径不能混totalPrice 是 getTotalPrice() 的返回值它累加的每行小计。表面看这只是个求和实际有一个精度坑double 相加的浮点误差。书价 89.00 加 59.00 还好如果商品单价是 0.1三个 0.1 相加会出现 0.30000000000000004。fmt:formatNumber 能遮住显示层的小尾巴但它不会修改已经产生误差的 double 值一旦参与优惠计算或满减误差会被放大。展示层还有一种常见错误把小计算成 price * quantity 后再用 fmt:formatNumber 格式化右下角 totalPrice 循环相加的是“格式化前的原始 double”。两者结果在业务上应一致但因为浮点根的细微差异可能出现合计比手算小 0.01 的现象。遇到这种“合计对不上”的情况第一反应不应该是怀疑四舍五入而是确认计算用的是同一个数值源格式化只发生在最后一层输出之前。JSP 里还有一个容易被忽略的参数约定fmt:formatNumber 默认按当前 Locale 输出三位分节符比如 1080 会显示成 1,080。如果你不希望出现逗号可以加 groupingUsedfalse 或直接字符串拼。本题源里的“元”是硬编码在 JSP 里不是国际化写法做中文项目没问题做多语言项目就要换成资源文件。5. 常见问题排查购物车最容易翻车的五个坑5.1 重启 Tomcat 后购物车内容全部丢失现象开发者把商品加入购物车重启 Tomcat 或 eclipse 的 server 之后再次打开购物车页面里面空空如也。有人以为是项目部署问题反复 clean、rebuild无效。原因这份源码把 Cart 对象放进 HttpSessionSession 数据默认存在 Tomcat 进程的堆内存里。进程一重启内存全部释放购物车自然清空。这不是 bug 是 Session 容器的生命周期决定的。解决教学场景下接受这个行为即可。如果希望重启后购物车还在需要引入持久化方案把购物车落库或存 Redis。常见做法是给 CartItem 增加一个 user_id 字段用户每次加入商品时同步写一张 cart_item 表Session 只作为缓存登录时从库里把购物车重新加载进 Session。对课程设计来说把这句话写进设计说明书里会显得你对 Session 原理有认识。5.2 商品名称在购物车里显示成“???”现象商品列表页中文正常加入购物车后购物车页面里的商品名称变成一串问号英文和数字正常。原因典型的中文编码问题。可能有三个环节断掉JSP 页面本身的 contentType 没写 UTF-8Servlet 里 request 参数没有 setCharacterEncoding(UTF-8)Tomcat 接收请求时 URI 默认编码不是 UTF-8。购物车这里更多是前两个尤其是直接从 URL 里传中文商品名时Tomcat 8 以上默认 URI 编码是 UTF-8Tomcat 7 之前默认 ISO-8859-1。解决三步都做。JSP 顶部写% page pageEncodingUTF-8 %Servlet doGet 入口处调用request.setCharacterEncoding(UTF-8)如果商品名是作为 URL 参数传递的确保调用方 URLEncoder.encode(name, UTF-8) 编码后再拼接。最省心的做法是商品名不要通过 URL 传Servlet 只用商品编号从 ProductDao 里重新取一次名称原始数据源和显示源就统一了。从那以后我每次传中文参数都默认先编码不赌容器默认行为。5.3 同一商品加进购物车后出现两行相同的记录现象AJAX 或者普通链接连续两次点击“加入购物车”购物车页面出现两行一样的商品数量分别是 1 和 1而不是合并成一行数量 2。原因最常见是自己写逻辑时绕过 Cart.add 方法直接在 Servlet 里cart.getItems().put(id, new CartItem(...))每次都覆盖或新增没有走 containsKey 合并分支。另一种可能是在 JSP 里通过c:forEach遍历的 items 是 sessionScope.cart.items 本身而 Cart.add 方法内部修改了 map 结构导致 ConcurrentModificationException 或显示错乱。解决把合并逻辑收敛到 Cart.addServlet 只负责构造 CartItem不直接操作 items Map。如果是从 JSP 页面里调用购物车操作方法那就等于在视图层改模型教学设计里可以接受但工程实践不推荐。排查时在 Cart.add 入口加一行日志打印 productId 和当前 quantity立刻能看出是没走到合并分支还是参数传入错误。5.4 结算总价出现 0.30000000000000004 这种浮点尾巴现象商品单价 0.1买三件页面上的总计显示 0.30000000000000004而不是 0.30。原因这是 double 浮点表示的标准问题二进制无法精确表示 0.1累加三次后误差暴露。fmt:formatNumber 能把显示结果修整成 0.30但如果把这个 double 再传回后端参与优惠折算误差会被正常计算逻辑放大。解决教学源码可以先接受用 fmt:formatNumber 兜底显示。课程设计项目我建议及早换成 BigDecimalCartItem 的 price、Cart 的 totalPrice 全部用 BigDecimal构造时用new BigDecimal(0.10)而不是new BigDecimal(0.10)。注意 JDBC 读取数据库 decimal 字段时也会自动映射为 BigDecimal正好对得上。如果暂时不改类型至少要保证“计算用 double展示一次性 fmt:formatNumber”绝不在展示前反复做四舍五入。5.5 购物车链接 404但商品列表页正常现象商品列表页能打开点“加入购物车”后浏览器地址栏变成 404。列表页里 JSP 和 CSS 都正常唯独 cart 相关路径失效。原因通常是 URL 写错了。两个典型一是 JSP 里的链接写成/cart, 部署后项目上下文路径是 /demo实际变成请求 /cart 而不是 /demo/cart二是 CartServlet 没有正确加WebServlet(/cart)注解或者 web.xml 里 servlet-mapping 写了一个和 JSP 链接不一致的路径。解决统一用${pageContext.request.contextPath}/cart?actionaddid...这种写法不要手写斜杠开头。另外一个隐蔽点是有些 Tomcat 版本对带空格或未转义中文参数的 URL 会直接拒绝导致 404所以所有查询参数都要在 JSP 里用 c:url 或手动编码。排查时先看 404 页面显示的是哪个应用路径再对比 servlet 映射和 JSP 链接两边的值百分之八十的问题出在单边多了个斜杠。6. 再往前一步给简易购物车套上过滤器、BigDecimal 与会话过期提示6.1 登录过滤器拦下未登录访问拿这份源码做课程设计时最常见的要求是“结算前必须登录”。简单的做法是每个 Servlet 里都 getSession 再 getAttribute(user)判断为 null 就重定向但这样写十次就烦了。一劳永逸的方案是写一个 Filter。WebFilter(/cart.jsp) public class LoginCheckFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpSession session req.getSession(false); if (session null || session.getAttribute(user) null) { HttpServletResponse resp (HttpServletResponse) response; resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }逻辑说明Filter 的作用范围是 /cart.jsp也就是说只有访问购物车页面时才校验登录而商品列表页保持开放这符合电商常见的浏览行为。req.getSession(false) 表示不主动创建会话没有会话就直接认为未登录。sendRedirect 前留一个 return 非常重要否则会继续执行 chain.doFilter 导致页面漏放行。6.2 金额类型全面升级到 BigDecimal把 CartItem 和 Cart 里的 double 全部替换成 BigDecimal工程量不大却能把 5.4 这类浮点问题从根源掐断。构造时用字符串new BigDecimal(89.00)加法用 add乘法用 multiply比较用 compareTo 而不是 equals。set 数量还是 int单价和小计、总计是 BigDecimalJSP 里 fmt:formatNumber 依然适用。唯一要适应的是从请求参数读取单价时不再用 Double.parseDouble而是new BigDecimal(request.getParameter(price))这个参数如果为空会抛 NumberFormatException注意 catch。6.3 会话过期提示与回跳Session 超时是购物车的隐形炸弹。真实用户逛着逛着超过 30 分钟没操作回来点击“”突然发现购物车空了。我一般会给这个项目补一个 HttpSessionListener在 sessionDestroyed 时把即将失效的会话 ID 和购物车件数记入日志便于回查。同时做一个友好提示登录后如果检测到 session 里没有 cart就在首页展示“购物车会话已过期请重新加入商品”而不是让用户面对一个冷冰冰的空表格。从那以后我每次给新项目搭购物车都会强制走一遍这三步Filter 校验登录态、金额类型换成 BigDecimal、Session 过期有反馈。哪怕只是一个课程设计这三步也能帮你避开后续真实电商开发里最常被追问的四个点。希望这份简易购物车源码的拆解能帮到你把它下载下来跑通一遍比看十遍教程都管用。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进