☰
JavaWeb购物车源码解析:Servlet+JSP+JDBC与Session状态管理
2026/10/16 1:09:35 网站建设 项目流程

简介:这是一份基于JavaWeb的简易购物车系统源代码,面向Java初学者与Web开发入门者,用来学习Servlet、JSP、Session、JDBC及MVC分层等核心技术。压缩包共54个文件,包含14个Java源文件、14个class文件、6个JSP页面,以及数据库脚本、XML/Properties配置、项目说明与开发笔记,总大小仅1.58MB。目前已有4125人浏览学习,适合入门级开发者对照实践。随包提供项目安装说明和笔记资料,从商品展示、加入购物车、数量更新到订单结算均有对应实现,系统讲述HTTP请求与会话跟踪、JDBC数据库操作、JSTL/EL标签用法,并设计了购物车相关数据表,给出Tomcat部署和IDE导入指引。同时涉及异常处理与SQL注入防护等安全细节,可帮助读者在真实项目中理解JavaWeb开发全流程,提升编码与排错能力。

1. 一个JavaWeb购物车源码,值得下载的“最小但完整”的练手闭环

很多学JavaWeb的人都有这种经历:SSM、SpringBoot的教程看了一堆,真让自己独立写一个带业务状态的页面,反而无从下手。这份javaWeb简易购物车源代码恰好补上这个断层——它不依赖任何框架,用Servlet + JSP + JDBC这三件套,把“浏览商品→加入购物车→调整数量→清空→结算”这条完整链路做通了。适合刚学完Servlet和JSP、想看看一个完整项目长什么样的人;也适合准备做毕设或课设、需要一个能改能跑的底子的同学。它不花哨,但五脏俱全:Session管理、JDBC操作、MVC分层、JSTL渲染,这些面试常被追问的点,在这个代码里都能找到落点。

2. 整体架构:从包名到一次请求的完整闭环

2.1 项目类型与技术选型

这份源码是一个传统JavaWeb项目,打包结构就是标准的Web工程:src目录放Java代码,WebContent目录放JSP页面和配置文件。技术组合是Servlet 3.0 + JSP + JSTL + MySQL + JDBC,没有引入Spring、MyBatis这些重量级框架,也没有Maven管理依赖——就是最原始的“拷进Eclipse或IDEA,配好Tomcat就能跑”的形态。

为什么这个组合反而值得看?因为购物车本质上是一个状态管理问题,Servlet负责接收请求和控制跳转,JSP负责展示,Session负责暂存购物车数据,三层职责清晰,没有任何框架遮蔽你理解“一个Web请求到底走了哪几行代码”。如果你已经会用SpringBoot,回来看这套代码,能更直观地理解框架帮你封装掉的到底是什么。

2.2 源码目录结构

拿到源码后第一件事不是找启动类,而是先看包结构。这份代码的包划分是这样的:

src/ ├── com/xxx/shop/dao/ // 数据访问层,负责查数据库 │ ├── ProductDao.java │ ├── UserDao.java │ └── impl/ // 部分版本的实现类 ├── com/xxx/shop/entity/ // 实体类,对应数据库表 │ ├── Product.java │ ├── User.java │ └── CartItem.java ├── com/xxx/shop/service/ // 业务层,处理购物车计算逻辑 │ └── CartService.java ├── com/xxx/shop/servlet/ // 控制器层,接收请求并转发 │ ├── ProductListServlet.java │ ├── LoginServlet.java │ ├── LogoutServlet.java │ └── CartServlet.java └── com/xxx/shop/util/ // 工具类,比如DBUtil数据库连接 └── DBUtil.java WebContent/ ├── index.jsp // 商品列表入口 ├── login.jsp // 登录页面 ├── cart.jsp // 购物车展示页面 ├── error.jsp // 统一错误页 ├── css/ // 样式文件 ├── js/ // 前端脚本 └── WEB-INF/ ├── web.xml // Servlet映射与配置 └── lib/ // 依赖jar包,如jstl.jar、mysql-connector.jar

从依赖方向看,Servlet层调用Service层,Service层调用Dao层,Dao层最后通过JDBC操作数据库,单向依赖,没有循环引用。这比把所有代码堆在一个Servlet里要干净得多,也是这份源码最有价值的地方——它示范了一个JavaWeb项目该有的分层习惯,即便规模小,节奏是对的。

2.3 一次“加入购物车”请求的完整流转

假设用户点击了“加入购物车”按钮,这个请求从前端到后端的路径是这样的:

浏览器发起 POST 请求 → web.xml 中映射的 CartServlet → CartServlet 从 request 中取出商品 id 和数量 → 从 session 中取出购物车对象(若不存在则新建) → 调用 CartService 做加入、合并数量、计算小计 → 把更新后的购物车放回 session → 重定向到 cart.jsp 或返回商品列表页

这里有一个关键的细节:购物车数据不落数据库,只放Session。这意味着只要Session还在(默认30分钟),购物车就在;一旦关闭浏览器或Session过期,购物车就丢了。这是简易购物车最主流的做法,也是后面改造时第一个需要考虑的点。

3. 核心模块拆解:购物车从“被点击”到“展示出来”的完整代码链路

3.1 实体类设计:CartItem到底存了什么

购物车不是一张数据库表,而是一个内存中的对象集合。每个条目对应CartItem,它封装了商品信息和购买数量。这份源码里CartItem的代码长这样:

package com.xxx.shop.entity; import java.math.BigDecimal; public class CartItem { private int productId; // 商品编号 private String productName; // 商品名称,冗余存储,方便JSP展示 private BigDecimal price; // 单价,使用BigDecimal避免浮点误差 private int quantity; // 购买数量 private BigDecimal subtotal; // 小计 = price * quantity // 构造方法与getter/setter省略 public BigDecimal getSubtotal() { return price.multiply(new BigDecimal(quantity)); } }

注意这里的小计没有用字段存储,而是通过方法实时计算,这样改数量后无需额外调用刷新小计的逻辑。

这里有个新手容易忽略的问题:价格为什么用BigDecimal而不是double?因为double在涉及小数点运算时会出现精度丢失,比如0.1加0.2可能得到0.30000000000000004,钱算错哪怕几分钱都很麻烦。BigDecimal虽然写起来啰嗦,但做乘法、加法都安全。

3.2 购物车本体:Cart类的会话存储逻辑

购物车本身用Cart类表示,内部维护一个List 。核心是add方法,它处理“同一商品重复加入”的合并逻辑:

package com.xxx.shop.entity; import java.util.ArrayList; import java.util.List; public class Cart { private List<CartItem> items = new ArrayList<>(); // 添加商品到购物车 public void add(CartItem newItem) { boolean found = false; for (CartItem item : items) { if (item.getProductId() == newItem.getProductId()) { // 已存在同一商品,只加数量,不新增条目 item.setQuantity(item.getQuantity() + newItem.getQuantity()); found = true; break; } } if (!found) { items.add(newItem); } } public void remove(int productId) { items.removeIf(item -> item.getProductId() == productId); } // 清空购物车 public void clear() { items.clear(); } // 计算购物车总金额 public double getTotalPrice() { double total = 0; for (CartItem item : items) { total += item.getSubtotal().doubleValue(); } return total; } public List<CartItem> getItems() { return items; } }

这段代码解决了一个实际使用痛点:用户把同一个商品加了两次,如果每次都生成新条目,购物车列表会膨胀且难看。合并逻辑很简单——遍历已有条目,找到同商品的就把数量累加,找不到再追加。这个模式在做电商、点餐这类系统时会反复用到,值得记下来。

3.3 CartServlet:从请求参数到Session对象

CartServlet是本项目里逻辑最多的控制器,它处理三种动作:add(加入)、remove(删除)、clear(清空)。通过请求参数action区分。

package com.xxx.shop.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.xxx.shop.entity.Cart; import com.xxx.shop.entity.CartItem; import com.xxx.shop.dao.ProductDao; import com.xxx.shop.entity.Product; @WebServlet("/cart") public class CartServlet extends HttpServlet { private ProductDao productDao = new ProductDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); HttpSession session = request.getSession(); Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); session.setAttribute("cart", cart); } if ("add".equals(action)) { int productId = Integer.parseInt(request.getParameter("productId")); int quantity = Integer.parseInt(request.getParameter("quantity")); // 从数据库查出商品信息,补充到购物车条目 Product product = productDao.findById(productId); CartItem item = new CartItem(); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setQuantity(quantity); cart.add(item); response.sendRedirect("cart.jsp"); } else if ("remove".equals(action)) { int productId = Integer.parseInt(request.getParameter("productId")); cart.remove(productId); response.sendRedirect("cart.jsp"); } else if ("clear".equals(action)) { cart.clear(); response.sendRedirect("cart.jsp"); } } }

逻辑说明:add动作做了三件事——从会话里拿购物车(没有就建一个)、从数据库查出商品完整信息、组装成CartItem加入购物车,最后重定向回购物车页面。这里有个隐含前提:商品表和数据库连接必须当时可用,否则查不到商品,购物车条目就是坏的。

参数说明:productId是Request参数,对应商品列表页面隐藏域里的商品编号;quantity是用户选择的购买数量,在商品列表页用数字输入框传过来。这里没做参数校验,如果直接请求quantity=abc会抛NumberFormatException,这是后续可以优化的点,也说明这个项目定位在学习场景。

3.4 商品列表与购物车页面的JSTL渲染

购物车页面cart.jsp是典型的前后端分离不彻底时代的写法,用JSTL标签遍历集合替代Java脚本片段:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head><title>购物车</title></head> <body> <h2>我的购物车</h2> <c:choose> <c:when test="${empty sessionScope.cart.items}"> <p>购物车还是空的,去<a href="index.jsp">商品列表</a>看看吧</p> </c:when> <c:otherwise> <table border="1" cellpadding="8"> <tr> <th>商品名</th><th>单价</th><th>数量</th><th>小计</th><th>操作</th> </tr> <c:forEach items="${sessionScope.cart.items}" var="item"> <tr> <td>${item.productName}</td> <td><fmt:formatNumber value="${item.price}" type="currency"/></td> <td>${item.quantity}</td> <td><fmt:formatNumber value="${item.subtotal}" type="currency"/></td> <td> <form action="cart" method="post" style="display:inline"> <input type="hidden" name="action" value="remove"> <input type="hidden" name="productId" value="${item.productId}"> <input type="submit" value="删除"> </form> </td> </tr> </c:forEach> </table> <p>总计:<fmt:formatNumber value="${sessionScope.cart.totalPrice}" type="currency"/></p> <form action="cart" method="post"> <input type="hidden" name="action" value="clear"> <input type="submit" value="清空购物车"> </form> </c:otherwise> </c:choose> </body> </html>

这里有个值得说的细节:删除按钮用了一个内联form提交POST请求,而不是普通链接的GET请求。因为删除是修改操作,按REST规范应该用POST或DELETE,用GET虽然能跑,但万一搜索引擎或浏览器预加载误触发了链接,就会把商品意外删掉。这个项目用的是小表单POST提交,算是安全习惯的雏形。

另一个细节是fmt:formatNumber标签做金额格式化,避免出现“486.8999999999999”这样的尴尬显示。初学者在JSP里直接输出double经常遇到这个问题,用JSTL的格式化标签可以一次性解决。

4. 数据库设计与会话策略:两张表支撑起一个完整业务

4.1 表结构设计:product表与user表

这份源码的数据库脚本藏在项目里,通常是shop.sql或者doc目录下。核心只有两张表:商品表和用户表。表结构大致是这个样子:

CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, description VARCHAR(500), stock INT DEFAULT 100, image_url VARCHAR(255) ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(50) NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

设计说明:product表把价格设为DECIMAL(10,2),这是数据库里存钱的标准做法——整数位最多8位、小数位2位,足够覆盖常见商品价格。之前说过Java代码里用BigDecimal,数据库用DECIMAL,这两件事是配套的,从源头避免浮点误差。

user表只存用户名和明文密码。这个设计在现实中不能用,但作为教学项目可以理解。如果你想拿到项目后自己练手改造,把password字段改成存MD5或BCrypt哈希是第一个能动手的点。

4.2 Session保存购物车:为什么不存数据库

购物车数据只存在HttpSession对象里,这是本项目的核心设计决策。Session的服务端超时时间默认是30分钟,用户在这期间关了浏览器再打开,Session就没了,购物车也跟着清零。

这个方案的好处:

  • 简化数据库设计,不需要专门建购物车表和购物车明细表
  • 不需要处理“游客未登录就加购”的账目归属问题
  • 读写都在内存,速度极快,不产生数据库IO

代价也很明显:用户换了浏览器或清了Cookie,购物车就消失,无法做到多设备同步。这种以Session为购物车容器的模式,在早期电商网站很普遍,今天的大型电商都已经改成“登录状态下的购物车落库”方案了。但学的时候要明白:购物车本质是一个“临时购物袋”,Session是它的最小实现。

4.3 商品列表页的JDBC查询代码

商品列表页的数据来自ProductDao,代码风格是传统JDBC写法:

package com.xxx.shop.dao; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.util.ArrayList; import java.util.List; import com.xxx.shop.entity.Product; import com.xxx.shop.util.DBUtil; public class ProductDao { public List<Product> findAll() { List<Product> list = new ArrayList<>(); String sql = "SELECT id, name, price, description, stock FROM product"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setDescription(rs.getString("description")); p.setStock(rs.getInt("stock")); list.add(p); } } catch (Exception e) { e.printStackTrace(); } return list; } }

这里用了try-with-resources语法,Connection、PreparedStatement、ResultSet都会在try块结束后自动关闭,避免了手动关连接把代码写得又臭又长还容易漏。DBUtil是静态工具类,用Class.forName加载驱动后,通过DriverManager.getConnection获取连接,连接的URL、用户名、密码都写死在常量里,这也是可以改成配置文件的点。

所有代码都在try块里捕获异常并printStackTrace,不向上抛。这种写法在练习代码里能看,但正式项目里至少应该把异常包装成业务异常再抛给上层处理,否则前端永远拿不到出错原因。

5. 运行部署与避坑指南:从源码到能点出购物车页面

5.1 本地运行完整步骤

很多人下载这个源码后卡在环境配置上,我把跑通的步骤完整列出来:

  1. 准备环境:JDK 8 + Tomcat 8/9 + MySQL 5.7/8.0 + Eclipse或IDEA。
  2. 在MySQL中执行shop.sql脚本,创建数据库和两张表。注意修改编码为utf8,避免商品名称乱码。
  3. 找到DBUtil.java或db.properties,把数据库账号密码改成你自己的。
  4. 在Eclipse中:File -> Import -> General -> Existing Projects into Workspace,选中整个项目目录。
  5. 在IDEA中:项目结构里把WebContent目录标记为Web资源目录,并添加Tomcat运行配置。
  6. 把mysql-connector.jar、jstl.jar、standard.jar放进WEB-INF/lib目录。
  7. 以Tomcat方式启动项目,浏览器访问http://localhost:8080/项目名/index.jsp。

如果第4步导入后项目没有自动变成Web项目,检查项目根目录下有没有.settings目录和.classpath文件,老项目迁移到IDEA时这个问题很常见。

5.2 避坑指南:5个高频问题排查记录

以下几条都是我在给同学调试这个项目时实际遇到过的现象,按“现象 → 原因 → 解决”的顺序整理:

1. 商品列表能打开,但点“加入购物车”后404

现象:按钮提交后跳转到404页面,URL是……/cart。

原因:web.xml里没有正确声明servlet映射,或者IDE没有把web.xml里的servlet-class自动识别。

解决:打开WEB-INF/web.xml,确认有一个指向CartServlet的servlet-name和url-pattern配置,url-pattern值必须是/cart;如果是用@WebServlet注解方式,确认类上注解值写的是/cart而不是其它路径。

2. 加了商品但购物车页面还是空的

现象:加了两次商品,购物车页面有商品列表,但sessionScope.cart.items为空。

原因:CartServlet改变了Session里的Cart对象引用,但购物车页面读取的是另一个SessionID对应的Session。这种跨页面Session丢失通常是因为项目部署名不一致,导致两次请求的JSESSIONID变了。

解决:在Tomcat的conf/context.xml里检查session-cookie是否设置了path;另外确保访问的URL路径始终使用同一上下文根,比如都是/shop/index.jsp,不要一会儿访问shop、一会儿访问Shop。

3. 商品价格显示成4.899999999999

现象:单价12.50元加入两个,小计显示24.999999999。

原因:JSP页面直接用EL表达式输出double类型,没经过fmt:formatNumber格式化。

解决:把页面里所有金额输出统一改为<fmt:formatNumber value="${item.price}" pattern="0.00"/>,同时把实体类中的价格类型改成BigDecimal,从源头消掉精度问题。改完这步,运行就没再翻车过。

4. 启动时报ClassNotFoundException: com.mysql.jdbc.Driver

现象:Tomcat一启动就报错,找不到MySQL驱动。

原因:mysql-connector.jar没有放进WEB-INF/lib目录里,或者放在了Java Build Path里但没部署到Tomcat。

解决:把jar包同时添加到项目构建路径和WEB-INF/lib下,注意tomcat部署时web应用只认WEB-INF/lib下的jar。

5. 数据库连接报Communications link failure

现象:启动项目后商品列表页面一直转圈,最后报Communications link failure。

原因:数据库没启动、端口不对(MySQL 8默认3306但可能被改过)或账号密码不匹配。这基本属于环境类玄学问题,看着像代码错,实际是连接信息不对。

解决:打开MySQL命令行工具,先用账号密码手动登录一次,确认能连上。然后检查DBUtil中的URL,确认jdbc:mysql://localhost:3306/数据库名这一段没有多余空格,URL里如果需要加useSSL=false和characterEncoding=utf8,就补上。

5.3 部署到Tomcat的webapps:脱离IDE运行

想不想脱离Eclipse直接看效果?可以把整个项目打成WAR包,放到Tomcat的webapps目录下,启动Tomcat后会自动解压部署。具体做法:

# 在Eclipse中右键项目 -> Export -> WAR file # 或者进入项目根目录,执行 jar -cvf shop.war . cp shop.war /path/to/tomcat/webapps/ cd /path/to/tomcat/bin ./startup.sh

部署完成后Tomcat会自动解包shop.war,访问http://localhost:8080/shop/index.jsp即进入项目。这整个过程就是把Eclipse封装的东西打回原形,手动理解“部署”二字是什么意思。看懂这步以后,Tomcat对你来说就不再是黑匣子了。

6. 二次开发:从简易购物车到最小可用订单系统

这个项目的改造空间非常大,我这里提供一个我认为性价比最高的改造路径:把“购物车仅仅存在Session里”升级成“提交订单时把购物车快照落库”。这是从demo迈向真实系统的第一步,改动量不大,但业务完整性提升明显。

具体要做的事是新增三张表:订单主表、订单明细表、订单状态表。核心落单逻辑在CartServlet里新增一个“checkout”的action分支:

// 购物车结算时调用 if ("checkout".equals(action)) { Cart cart = (Cart) session.getAttribute("cart"); User user = (User) session.getAttribute("user"); int orderId = orderDao.createOrder(user.getId(), cart.getTotalPrice()); for (CartItem item : cart.getItems()) { orderItemDao.createOrderItem( orderId, item.getProductId(), item.getQuantity(), item.getPrice() ); } cart.clear(); response.sendRedirect("orderList.jsp"); }

这段逻辑说明:先创建订单主记录,拿到自增的orderId,再遍历购物车条目逐条写入明细,最后清空Session里的购物车——因为购物车已经被“固化”成订单了。注意这里还不涉及事务,如果中途出现异常会导致主表和明细对不上。想练事务控制的话,可以研究下在JDBC里怎么用setAutoCommit(false)、commit、rollback把这几步包成一个原子操作,这是很经典的面试手写题场景。

另外一个值得动手的地方是把“商品数量在购物车里可以直接修改”。现在源码里加购只能通过商品页的输入框定量,购物车页面没有改数量的入口。你可以给cart.jsp里的每条记录加一个数量输入框,表单里带上action=update和新的quantity值,在CartServlet里增加一个update分支,遍历时找到对应productId并把quantity改为新值,同时刷新小计。

从那以后,我每次接手类似练手项目,都会先用“跑通→看分层→找精度的坑→尝试模块替换”四步走一遍,这比直接打开源码从头通读更高效。希望这份分析能帮你把购物车的每一行代码都吃透,用最短的时间做到能改、能讲、能扩展——希望帮到你。

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

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

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

立即咨询