☰
JavaWeb购物车源码解析:Servlet+JSP+Session实现与二次开发指南
2026/11/6 3:58:02 网站建设 项目流程

简介:这份JavaWeb简易购物车源代码面向具备Java基础、正在学习Web开发的学生与初级开发者,通过一个可运行的在线购物车系统串联Servlet与JSP、HTTP请求处理、Session会话管理、JDBC数据库交互、MVC分层设计以及JSTL与EL表达式等核心知识点,帮助读者理解购物车增删改查与金额统计等业务在真实项目中的落地方式。压缩包共54个文件,约1.58MB,以14个java源文件与14个class编译文件为主体,辅以6个jsp页面、2个xml配置、2个properties配置及jar依赖等,结构完整可直接导入IDE运行。资源附有开发笔记与项目安装说明,涵盖关键代码解释、数据库表设计与Tomcat部署步骤,并涉及异常处理与SQL注入防范等实践细节。目前已有4130人学习下载,适合作为课程设计参考或Web入门练手项目。

1. 从一份 JavaWeb 购物车源码说起:它到底能帮你解决什么

很多同学学完 Servlet 和 JSP,教材上的例子能跑,一到自己动手写就卡壳——尤其是购物车这种「有状态、有交互、有数据流转」的功能。这份 JavaWeb 简易购物车源代码,正好卡在这个痛点上:它把商品浏览、加入购物车、修改数量、删除条目、结算展示这条完整链路用最朴素的 Servlet + JSP + Session 实现了一遍,没有 Spring、没有 MyBatis、没有前端框架,纯原生 JavaWeb 技术栈。适合谁?适合刚学完 Servlet 生命周期、想找一个能跑通、能看懂、能改得动的小项目练手的人;也适合需要给课程设计或实训作业找一个干净底稿的开发者。它不追求工程化,追求的是把「购物车到底怎么在服务端记住你买了什么」这件事讲透。

2. 项目结构与技术选型:为什么是 Servlet + JSP + Session

2.1 目录骨架与各层职责

拿到源码先别急着跑,先把目录翻一遍。这类简易购物车项目通常是一个标准 Maven Web 工程,结构大致如下:

shopping-cart/ ├── pom.xml └── src/ └── main/ ├── java/ │ └── com/example/cart/ │ ├── model/ # 实体类:Product、CartItem │ ├── dao/ # 数据访问:ProductDao │ ├── servlet/ # 控制器:ProductServlet、CartServlet │ └── util/ # 工具类:编码过滤、常量 ├── resources/ └── webapp/ ├── WEB-INF/ │ └── web.xml ├── index.jsp # 商品列表页 ├── cart.jsp # 购物车页 └── static/ # css、js、图片

这个分层很薄,但五脏俱全。model放实体,dao负责从内存或数据库取商品,servlet接请求做分发,webapp下是视图。没有 Service 层,业务逻辑直接写在 Servlet 里——对教学项目来说这是合理的取舍,少一层就少一层理解成本。我一般会建议读者先按这个骨架对照自己的 IDE 目录树看一遍,确认包名和路径对得上,再动手配置。

技术选型上,Servlet 负责请求路由和会话操作,JSP 负责渲染,Session 负责保存购物车状态。为什么不用 Cookie?因为购物车条目可能多、结构复杂,Cookie 存不下也不安全;为什么不用数据库存购物车?因为简易项目要的是「打开就能跑」,Session 内存存储零配置。这个选型决定了它的边界:单机、单会话、重启即清空,但也正因为简单,你能把整条链路一眼看穿。

2.2 依赖与运行环境确认

pom.xml里通常只有两个关键依赖:Servlet API 和 JSP API,且 scope 为provided,因为 Tomcat 自带。常见做法是锁定javax.servlet-api3.1.0 或 4.0.1,JDK 用 8 或 11,Tomcat 用 8.5 或 9。这里有个血泪经验:如果你用 Tomcat 10,包名已经从javax.servlet变成jakarta.servlet,源码里所有 import 都会报红。所以第一步先确认 Tomcat 版本,别上来就改代码。

<dependencies> <!-- Servlet API,由容器提供,打包时不带入 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP API,同理 --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> </dependencies>

参数说明:scope=provided表示编译时需要、运行时由 Tomcat 提供,这样打出来的 war 包不会和容器自带的类冲突。如果你把这两个依赖的 scope 去掉,部署时很可能遇到ClassCastException或类加载冲突。改依赖之前先跑mvn dependency:tree看一眼实际解析结果,比盲改靠谱。

2.3 请求链路:一次「加入购物车」到底发生了什么

理解这个项目最快的方式,是跟一次请求走完。用户在index.jsp点「加入购物车」,浏览器发出GET /cart?action=add&id=3,CartServlet的doGet接住,从ProductDao按 id 查出商品,再从HttpSession里取出购物车 Map,判断该商品是否已存在,存在就数量加一,不存在就新建CartItem放进去,最后重定向回购物车页。整条链路没有任何框架介入,全靠request.getParameter和session.getAttribute。

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); HttpSession session = request.getSession(); // 购物车用 Map 存,key 是商品 id,value 是条目,查找 O(1) Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<>(); // 保持加入顺序,展示更自然 session.setAttribute("cart", cart); } if ("add".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); Product p = productDao.findById(id); CartItem item = cart.get(id); if (item == null) { cart.put(id, new CartItem(p, 1)); } else { item.setQuantity(item.getQuantity() + 1); } } response.sendRedirect(request.getContextPath() + "/cart.jsp"); }

逻辑说明:getSession()默认会创建会话,所以第一次加购就自动有了购物车容器;用LinkedHashMap而不是HashMap,是为了让购物车列表按加入顺序展示,避免每次刷新顺序乱跳这种玄学问题。参数上,action决定分支,id是商品主键,解析时没做空值保护——这是教学项目的常见简化,二次开发时第一件事就该补上try-catch和参数校验。重定向用sendRedirect而非转发,是为了避免刷新页面重复提交,这个细节很多新手会翻车。

3. 核心模块拆解:实体、DAO 与购物车操作

3.1 实体类与购物车数据结构设计

Product和CartItem是两个核心实体。Product存商品基本信息,CartItem存「商品 + 数量」,这样设计的好处是购物车条目和商品解耦,改数量不影响商品本身。常见做法是CartItem里持有一个Product引用加一个int quantity,并提供getSubtotal()计算小计。

public class CartItem { private Product product; private int quantity; public CartItem(Product product, int quantity) { this.product = product; this.quantity = quantity; } // 小计 = 单价 × 数量,展示和结算都用它 public double getSubtotal() { return product.getPrice() * quantity; } public Product getProduct() { return product; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity = quantity; } }

参数说明:quantity用int而非Integer,避免拆箱空指针;getSubtotal放在实体里而不是 JSP 里算,是为了让视图层只负责展示,逻辑留在 Java 侧。这里有个容易忽略的点:价格用double在真实项目里会精度丢失,教学项目无所谓,但你二次开发接支付时,务必换成BigDecimal,否则对账时会出现一分钱对不上的黑匣子问题。

3.2 ProductDao:商品数据从哪来

简易项目的商品数据一般不走数据库,而是硬编码在一个List里,ProductDao提供findAll()和findById()两个方法。这样省去了建库建表的步骤,clone 下来就能跑。如果你要改成数据库版,只需把这两个方法的实现换掉,上层 Servlet 一行不用动——这就是 DAO 层的价值。

public class ProductDao { // 内存商品表,模拟数据库;id 从 1 开始 private static final List<Product> PRODUCTS = Arrays.asList( new Product(1, "机械键盘", 299.0, "键盘.jpg"), new Product(2, "无线鼠标", 89.0, "鼠标.jpg"), new Product(3, "显示器支架", 159.0, "支架.jpg") ); public List<Product> findAll() { return PRODUCTS; } public Product findById(int id) { for (Product p : PRODUCTS) { if (p.getId() == id) return p; } return null; // 找不到返回 null,调用方需判空 } }

逻辑说明:Arrays.asList返回的是定长列表,只能读不能增删,作为只读商品源刚好合适。findById用线性遍历,商品少时无所谓,商品上千就该换Map索引。注意返回null的约定,Servlet 里拿到null还继续new CartItem(p, 1)就会空指针,这是新手最常见的翻车点之一,二次开发时建议改成抛业务异常或返回Optional。

3.3 修改数量与删除:Session 里的增删改

购物车页的「加一」「减一」「删除」三个操作,本质都是对 Session 里那个 Map 做修改。常见做法是用一个action参数区分:update带id和quantity,remove只带id。减到零时直接从 Map 移除,避免出现数量为 0 的僵尸条目。

if ("update".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); int qty = Integer.parseInt(request.getParameter("quantity")); CartItem item = cart.get(id); if (item != null) { if (qty <= 0) { cart.remove(id); // 数量归零即删除,保持数据干净 } else { item.setQuantity(qty); } } } else if ("remove".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); cart.remove(id); }

参数说明:quantity由前端表单提交,服务端必须重新校验,不能信任页面上的加减按钮——用户完全可以手改 URL。这里我一般会加一个上限,比如单件商品最多 99,防止有人传个999999把页面撑爆。删除操作用Map.remove而不是把数量置零,是为了让cart.jsp遍历时不用过滤无效条目,视图逻辑更干净。

3.4 JSP 视图:用 JSTL 遍历购物车

视图层用 JSTL 的c:forEach遍历 Session 里的 Map,配合c:if处理空购物车提示。注意 JSP 里不要写 Java 脚本片段,用 EL 表达式取值,这样页面和逻辑才分得开。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:choose> <c:when test="${empty sessionScope.cart}"> <p>购物车还是空的,去挑点东西吧。</p> </c:when> <c:otherwise> <c:forEach var="entry" items="${sessionScope.cart}"> <tr> <td>${entry.value.product.name}</td> <td>${entry.value.quantity}</td> <td>${entry.value.subtotal}</td> <td> <a href="cart?action=remove&id=${entry.key}">删除</a> </td> </tr> </c:forEach> </c:otherwise> </c:choose>

逻辑说明:sessionScope.cart直接取 Session 属性,entry.key是商品 id,entry.value是CartItem。EL 会自动调用 getter,所以entry.value.subtotal实际调的是getSubtotal()。这里要注意 JSTL 依赖:如果页面报taglib找不到,说明jstl的 jar 没进WEB-INF/lib,Maven 项目里补一个jstl:1.2依赖即可。别小看这个报错,它拦过太多第一次跑 JSP 项目的人。

4. 跑起来:从导入到访问的完整步骤

4.1 环境准备与项目导入

先把 JDK、Maven、Tomcat 三样装好,版本按第 2 章说的对齐。IDEA 里选File → Open打开项目根目录,等 Maven 索引完,检查Project Structure里 SDK 是不是 8 或 11,Facets里有没有识别出 Web 模块。如果没有,手动加一个 Web facet,把webapp设为 Web 资源目录,web.xml路径指到WEB-INF/web.xml。这一步没配好,后面启动会报「找不到上下文」或者 404,属于典型的配置翻车。

4.2 配置 Tomcat 与部署

在 IDEA 的Run → Edit Configurations里加一个 Tomcat Local,Deployment选项卡里加Artifact,选war exploded而不是war,这样改 JSP 不用重新打包。Application context设成/cart或/,记住这个路径,访问时要拼上。启动后控制台出现Server startup in xxx ms就算起来了,浏览器访问http://localhost:8080/cart/应该能看到商品列表。

# 如果你用命令行跑,先打包再丢进 Tomcat mvn clean package cp target/shopping-cart.war $TOMCAT_HOME/webapps/ $TOMCAT_HOME/bin/startup.sh

参数说明:war exploded是解压目录部署,热更新友好;war是打包部署,适合最终交付。命令行方式下,war 包名会成为上下文路径,所以访问地址是http://localhost:8080/shopping-cart/。启动失败先看logs/catalina.out,九成问题出在端口占用或 JDK 版本不匹配。

4.3 验证主流程是否走通

跑起来后别只看首页,按「浏览商品 → 加购两件 → 改数量 → 删一件 → 刷新页面」走一遍。重点验证两件事:刷新后购物车还在(说明 Session 生效),关掉浏览器重开购物车清空(说明没误用持久化)。如果加购后页面 404,检查sendRedirect里的getContextPath()拼出来的路径对不对;如果数量改了不生效,看表单提交的action和id参数名是否和 Servlet 里getParameter的一致。这套验证流程走完,你才算真正把这个项目跑明白了。

5. 避坑与排查:那些第一次跑必踩的坑

5.1 中文乱码:商品名变成问号

现象:页面上的商品名显示成???或乱码。原因:请求或响应没设字符编码,Tomcat 默认按 ISO-8859-1 解析。解决:在 Servlet 取参数前加request.setCharacterEncoding("UTF-8"),响应侧加response.setContentType("text/html;charset=UTF-8");更省事的做法是写一个CharacterEncodingFilter在web.xml里全局配。注意setCharacterEncoding必须在第一次getParameter之前调用,顺序错了不生效,这个坑我见过不止一次。

5.2 空指针:删完商品再点加购

现象:购物车里删光某商品后,再点加购报NullPointerException。原因:findById返回null时没判空就new CartItem。解决:在 Servlet 里对Product p做判空,为空直接重定向回列表页并提示;或者把findById改成找不到时抛自定义异常。根因是 DAO 的返回约定没被调用方遵守,二次开发时建议在接口注释里写清楚「可能返回 null」。

5.3 重复提交:刷新页面数量翻倍

现象:加购后按 F5 刷新,数量又加了一次。原因:加购用的是转发或直接渲染,浏览器刷新会重发上一次请求。解决:加购、删除这类写操作统一用sendRedirect重定向到展示页,也就是 PRG 模式。源码里如果某处用了forward,改成redirect即可。这个改动很小,但对体验影响很大,属于必须修的项。

5.4 依赖冲突:JSTL 标签不解析

现象:JSP 页面把<c:forEach>原样输出,或者报The absolute uri cannot be resolved。原因:jstljar 没打进WEB-INF/lib,或版本和 Servlet 不匹配。解决:Maven 项目补jstl:1.2依赖并确认 scope 是compile;非 Maven 项目手动把 jar 丢进WEB-INF/lib。Tomcat 10 环境下还要注意 JSTL 已迁到jakarta命名空间,老 jar 直接用不了。

5.5 会话丢失:重启 Tomcat 购物车清空

现象:重启容器后购物车没了。原因:Session 默认存在内存,容器重启即销毁。解决:这是设计使然,不是 bug。如果你需要重启保留,得配 Session 持久化或改用数据库存购物车。教学项目里我建议保持现状,但要在文档里写清楚这个边界,免得使用者误以为是数据丢失的故障。

6. 二次开发:把它改成能接数据库的版本

这份源码最大的价值不是「能跑」,而是「好改」。我一般会按三步走:先把ProductDao换成 JDBC 实现,再把购物车从 Session 迁到数据库表,最后补一层 Service。第一步改动最小、收益最直接,适合作为练手起点。

public class ProductDaoJdbc implements ProductDao { // 从连接池或 DriverManager 拿连接,这里用最朴素的写法示意 public Product findById(int id) { String sql = "SELECT id, name, price, image FROM product WHERE id = ?"; try (Connection conn = DbUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return new Product(rs.getInt("id"), rs.getString("name"), rs.getDouble("price"), rs.getString("image")); } } } catch (SQLException e) { throw new RuntimeException("查询商品失败, id=" + id, e); } return null; } }

改造时注意三点:一是用PreparedStatement防注入,别拼字符串;二是连接用完必须关,try-with-resources是最省心的写法;三是异常别吞,包一层带上下文的RuntimeException抛出去,排查时能直接看到是哪个 id 出的问题。购物车迁数据库时,表结构建议是cart_item(id, session_id, product_id, quantity),用session_id关联会话,这样多标签页也能共享同一份购物车。

验证改造是否成功,别只看页面,直接查库:加购后SELECT * FROM cart_item应该有记录,删光后应该为空。我习惯在改完 DAO 后先写一个main方法单独跑一遍增删查,确认 SQL 没问题再接 Servlet,比在容器里反复重启快得多。从那以后我每次动数据层,都强制先脱离 Web 容器验证一遍 SQL,再回到页面联调,省下的重启时间够喝好几杯咖啡。希望这份拆解能帮你把这份购物车源码真正跑起来、改下去。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询