简介这份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 Session2.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.1JDK 用 8 或 11Tomcat 用 8.5 或 9。这里有个血泪经验如果你用 Tomcat 10包名已经从javax.servlet变成jakarta.servlet源码里所有 import 都会报红。所以第一步先确认 Tomcat 版本别上来就改代码。dependencies !-- Servlet API由容器提供打包时不带入 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同理 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency /dependencies参数说明scopeprovided表示编译时需要、运行时由 Tomcat 提供这样打出来的 war 包不会和容器自带的类冲突。如果你把这两个依赖的 scope 去掉部署时很可能遇到ClassCastException或类加载冲突。改依赖之前先跑mvn dependency:tree看一眼实际解析结果比盲改靠谱。2.3 请求链路一次「加入购物车」到底发生了什么理解这个项目最快的方式是跟一次请求走完。用户在index.jsp点「加入购物车」浏览器发出GET /cart?actionaddid3CartServlet的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 是商品 idvalue 是条目查找 O(1) MapInteger, CartItem cart (MapInteger, 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 ListProduct PRODUCTS Arrays.asList( new Product(1, 机械键盘, 299.0, 键盘.jpg), new Product(2, 无线鼠标, 89.0, 鼠标.jpg), new Product(3, 显示器支架, 159.0, 支架.jpg) ); public ListProduct 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和quantityremove只带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 prefixc urihttp://java.sun.com/jsp/jstl/core % c:choose c:when test${empty sessionScope.cart} p购物车还是空的去挑点东西吧。/p /c:when c:otherwise c:forEach varentry items${sessionScope.cart} tr td${entry.value.product.name}/td td${entry.value.quantity}/td td${entry.value.subtotal}/td td a hrefcart?actionremoveid${entry.key}删除/a /td /tr /c:forEach /c:otherwise /c:choose逻辑说明sessionScope.cart直接取 Session 属性entry.key是商品 identry.value是CartItem。EL 会自动调用 getter所以entry.value.subtotal实际调的是getSubtotal()。这里要注意 JSTL 依赖如果页面报taglib找不到说明jstl的 jar 没进WEB-INF/libMaven 项目里补一个jstl:1.2依赖即可。别小看这个报错它拦过太多第一次跑 JSP 项目的人。4. 跑起来从导入到访问的完整步骤4.1 环境准备与项目导入先把 JDK、Maven、Tomcat 三样装好版本按第 2 章说的对齐。IDEA 里选File → Open打开项目根目录等 Maven 索引完检查Project Structure里 SDK 是不是 8 或 11Facets里有没有识别出 Web 模块。如果没有手动加一个 Web facet把webapp设为 Web 资源目录web.xml路径指到WEB-INF/web.xml。这一步没配好后面启动会报「找不到上下文」或者 404属于典型的配置翻车。4.2 配置 Tomcat 与部署在 IDEA 的Run → Edit Configurations里加一个 Tomcat LocalDeployment选项卡里加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;charsetUTF-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再回到页面联调省下的重启时间够喝好几杯咖啡。希望这份拆解能帮你把这份购物车源码真正跑起来、改下去。本文还有配套的精品资源点击获取