JavaWeb购物车源码全解析:从Session到MySQL的完整实现

发布时间:2026/10/7 15:24:59
JavaWeb购物车源码全解析:从Session到MySQL的完整实现 简介这套基于Servlet与JSP的JavaWeb简易购物车源代码面向刚接触JavaWeb的初学者和需要课程设计参考的开发者。项目覆盖商品展示、加入购物车、修改数量、删除商品、结算下单等典型流程集中体现Servlet请求处理、JSP页面展示、Session会话保持、JDBC数据库交互以及MVC分层设计思想。包内共54个文件其中包含14个Java源文件、14个对应的class文件、6个JSP页面还附带数据库脚本、XML配置、properties配置和项目安装说明txt压缩包整体仅1.58MB结构清晰便于按模块阅读。目前已有4125人学习下载热度较高。资源配有详尽的笔记资料和项目安装说明分别对商品表、用户表、购物车表等数据库设计和Tomcat部署、IDE导入步骤做了梳理可帮助读者快速跑通项目并深入理解从添加商品到结算的每个功能背后的实现逻辑。对课程设计、期末复习或JavaWeb入门实战都很有实用价值。1. javaWeb简易购物车源代码一个Servlet 讲透的经典有状态场景期末课设拿到“javaWeb购物车源代码”这种题目时新手最容易卡住的地方不是写代码而是想不通一个道理HTTP 请求本来是无状态的浏览器打开商品页加到购物车关掉页面再打开东西还在——这个“记得住”的能力从哪来购物车这个看似简单的模块恰好是 Servlet/JSP 阶段少有的完整有状态业务场景。把这一条链路打通JavaWeb 的 Session、Cookie、JDBC、三层结构、请求转发与重定向你就全串起来了。这篇文章适合三类人刚学完 Servlet 想找个完整案例练手的同学正在做课设需要可讲解代码的人以及拿到一套旧购物车源代码却不知道怎么改的人。我会把数据模型、建表 SQL、Servlet 核心逻辑到避坑点全部拆开保证你照着能跑通答辩时也讲得清。2. 购物车四种存法怎么选Session、Cookie、MySQL 与 Redis 的取舍2.1 从 HTTP 无状态说起购物车为什么必须“找地方存”要理解购物车为什么难先看一个最简单的加购动作。用户点击“加入购物车”浏览器向服务器发一个 HTTP 请求服务器处理完返回响应这个连接就被丢掉了。下一次用户再点“买一件”服务器根本不记得他是谁。购物车的要求恰恰相反它需要跨请求记住一组数据——用户加过哪些商品、每个商品几件。所以购物车的本质是“给无状态的 HTTP 协议配一个临时记忆”所有的实现方案都是在回答同一个问题这份记忆放在哪里。选错了存储位置代码写起来会很痛苦。最常见的翻车方式是把购物车数据存在某个全局变量里比如一个 static HashMap结果发现所有用户共享同一份购物车A 加进去的商品 B 也能看见。这种问题不是代码逻辑错了而是从一开始就没搞清“记忆的边界”。下面这几种存法是 JavaWeb 简易购物车的主流选项各有各的边界。2.2 四种方案对比存哪里决定了购物车的生命周期常见做法是四种Cookie、Session、MySQL、Redis。它们的区别可以用一张参数表讲清楚。存储方案位置共享范围生命周期容量是否需要登录适合场景Cookie浏览器单台浏览器默认关闭浏览器即失效可设过期时间单条 4KB 左右不需要游客购物车、记住上次加购记录Session服务器内存单个会话默认 30 分钟无操作失效取决于服务器内存不需要简易购物车最经典的方案MySQL数据库表所有设备持久保存手动删除几乎无限需要登录后关联用户已登录用户的真实购物车Redis缓存服务器所有设备可持久化可设过期取决于内存配置通常需要登录高并发场景的购物车先看 Cookie。把购物车内容序列化成字符串写在 Cookie 里好处是不用服务器维护状态缺点是容量太小存不了几件商品而且购物车数据结构复杂的话序列化和反序列化都很别扭。我自己早期做课设时试过把 CartItem 列表拼成字符串存 Cookie商品超过五六件就超 4KB直接被浏览器丢弃属于明显的坑。再看 Session 方案。服务器给每个浏览器分配一个唯一的 JSESSIONID购物车数据以对象形式放在 session 里。这是简易购物车源代码里最常见、也最适合新手理解的方案不用建表不用处理登录态代码量最小。但它的代价是数据在服务器内存里Tomcat 一重启就没了。如果你做的是课设这完全够用如果将来要上线必须迁移。MySQL 方案是“能讲出东西”的方案。把购物车落到表里用户登录后每次加购就是一条 INSERT 或 UPDATE 语句购物车数据不再依赖服务器内存。代价是每个操作都多一次数据库查询而且需要处理“用户未登录时加购了登录后怎么合并”这种问题。对课设答辩来说这个方案的技术含量明显比 Session 高。Redis 方案是工业级做法。用 Redis 的 Hash 结构key 是 cart:用户IDfield 是商品IDvalue 是数量天然适合购物车。但你的课程项目里引入 Redis 意味着多装一个服务对新手不太友好我建议先跑通 Session 版再往这个方向迁移。2.3 简易购物车为什么优先选 Session四个理由如果你是照着这个题目做课设我建议先用 Session 方案跑通全流程理由有四条。第一代码量最少不需要额外建表不需要写 DAO一个 ArrayList 就能装购物车。第二调试直观IDEA 的 Debug 面板里直接能看到 session 里的对象。第三账号体系不是必需品未登录状态也能加购符合“简易”二字的定位。第四后续扩展方向明确你将来把 session 里的 List 变成数据库表Servlet 层代码不需要大改。用 IDEA 运行 JavaWeb 项目时很多人第一次发现“咦我重启 Tomcat 后购物车不见了”——这就是 session 在服务器内存里的直接体现。理解这一点比背十个概念都有用。3. 先建表再编码购物车数据库设计与 JDBC 三层结构3.1 三张表的设计用户、商品、购物车条目哪怕你选的是 Session 方案我仍然建议你把商品表和用户表建出来。原因很简单Session 里放的是“商品的对象”这些对象从哪里来从数据库查出来。建表这一步是所有后续代码的地基。第一张表是用户表。注意 password 字段不要存明文至少做个 MD5 或 SHA-256 哈希这是答辩时老师大概率会问的点CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT 密码存哈希值, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第二张是商品表。这里有两个字段需要重点说明。price 用DECIMAL(10,2)而不是 DOUBLE是因为浮点数在 Java 和 MySQL 之间传递时会出现 0.10.2≠0.3 的精度问题钱相关的字段必须用定点数。stock 用 INT 表示库存后面做扣减时要配合条件 UPDATE 防止超卖CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, name VARCHAR(100) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 单价注意用定点数, stock INT NOT NULL DEFAULT 0 COMMENT 库存, image_url VARCHAR(255) DEFAULT NULL COMMENT 商品图可存 iconfont 图标的 URL, description VARCHAR(500) DEFAULT NULL COMMENT 商品描述 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;第三张表是购物车表。如果你的方案要把购物车落库这张表是核心。注意user_id和product_id要建立外键或者至少加索引否则按用户查购物车时会全表扫描CREATE TABLE cart_item ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT 关联 user.id, product_id INT NOT NULL COMMENT 关联 product.id, quantity INT NOT NULL DEFAULT 1 COMMENT 数量, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_user_product这个唯一键是设计里的点睛之笔它保证了同一个用户加同一件商品数据库里只有一行记录。你的 DAO 层代码就可以先尝试 INSERT冲突时改成 UPDATE quantity不需要先查一遍再判断。3.2 JDBC 连接参数URL 里那些容易被忽略的玄学参数建完表就该写 JDBC 工具类。这里最容易翻车的是连接 URL。很多新手的代码在本地能跑一换机器就报错多半是 URL 参数没写对private static final String URL jdbc:mysql://localhost:3306/shop?useSSLfalsecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 你的密码;三个参数逐个说。characterEncodingutf8mb4控制的是 JDBC 驱动和 MySQL 之间传输的字符集漏了它你在 Servlet 里写入中文商品名数据库里查出来是乱码。serverTimezoneAsia/Shanghai是 MySQL 8.x 驱动强制要求的不写会报时区错误。useSSLfalse是本地开发时关掉加密连接省掉一堆证书告警。MySQL 8.x 的驱动类名是com.mysql.cj.jdbc.Driver老版本的com.mysql.jdbc.Driver在 8.x 下已经废弃了这是另一个容易踩的坑。3.3 DAO 三层结构一份能直接抄的 ProductDao 骨架购物车项目用到的数据库操作其实只有四个按 ID 查商品、按用户 ID 查购物车、往购物车加物品、更新数量。这里给出 ProductDao 的完整写法用的是最朴素的 JDBC 方式不引框架方便课设时讲原理public class ProductDao { public Product findById(int id) { String sql SELECT id, name, price, stock 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()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setPrice(rs.getBigDecimal(price)); p.setStock(rs.getInt(stock)); return p; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }这个类里有个细节值得讲使用PreparedStatement而不是Statement。前者的意义不只是防止 SQL 注入更重要的是用?占位符配合setInt/setStringJava 类型和数据库类型之间的转换由驱动完成不会因为拼字符串把数字拼坏。另外try-with-resources语法保证了 Connection、PreparedStatement、ResultSet 用完后自动关闭许多老代码里忘记关连接导致连接数打满这在课设机器上不容易暴露但在共用数据库时是灾难。顺带提醒BigDecimal对应数据库里的 DECIMAL 字段从 ResultSet 里取价格不要用getFloat。4. 把“加入购物车”到“提交结算”跑通核心代码落地4.1 数据模型CartItem 和 Product 怎么设计最顺手购物车的最小单元不是一个商品而是“商品数量”。所以必须建一个 CartItem 类把这两个信息包起来。这里有个设计细节为什么不直接在 CartItem 里存一个 productId 整数而是存整个 Product 对象原因是 JSP 页面上要显示商品名、单价、小计如果只存 ID每次渲染页面都得回查数据库。对于一个简易项目直接把 Product 对象放在 CartItem 里页面渲染时拿属性直接用代码最少public class CartItem { private Product product; // 商品信息包含 id/name/price/stock private int quantity; // 购买数量 public CartItem(Product product, int quantity) { this.product product; this.quantity quantity; } public Product getProduct() { return product; } public void setProduct(Product product) { this.product product; } public int getQuantity() { return quantity; } public void setQuantity(int quantity) { this.quantity quantity; } // 小计单价乘数量给 JSP 页面直接调用 public BigDecimal getSubtotal() { return product.getPrice().multiply(new BigDecimal(quantity)); } }getSubtotal()这个方法很关键。JSP 页面里用${item.subtotal}就能直接显示小计不需要在 Servlet 里循环计算。把计算逻辑放在模型类里Java 代码里不用写一堆金额累加的循环——这是新手经常搞复杂的地方。购物车本身在 Session 里用ArrayListCartItem表示它自带顺序容量动态扩展遍历修改都方便没有比它更合适的容器。4.2 加入购物车为什么先查商品再判断重复加入购物车的 Servlet 是整个项目最核心的一段逻辑。它的流程可以概括为三步接收参数、查商品、判断购物车里有没有同款。注意判断重复的依据是商品 ID 而不是商品名因为同名商品可能是不同规格。用商品 ID 判断时建议直接用比较两边取出的基本类型 int避免 Integer 包装类用 equals 的麻烦WebServlet(/addToCart) public class AddToCartServlet extends HttpServlet { private ProductDao productDao new ProductDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 第一步接收前端传过来的商品 ID 和数量 int productId Integer.parseInt(req.getParameter(productId)); int quantity 1; String qtyParam req.getParameter(quantity); if (qtyParam ! null !qtyParam.isEmpty()) { quantity Integer.parseInt(qtyParam); } // 第二步根据 ID 查出完整的商品信息价格以数据库为准 Product product productDao.findById(productId); if (product null) { resp.sendRedirect(req.getContextPath() /error.jsp); return; } // 第三步从 Session 里取购物车没有就创建一个 HttpSession session req.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayList(); session.setAttribute(cart, cart); } // 第四步遍历购物车找到同款商品就累加数量找不到就新增条目 boolean found false; for (CartItem item : cart) { if (item.getProduct().getId() productId) { item.setQuantity(item.getQuantity() quantity); found true; break; } } if (!found) { cart.add(new CartItem(product, quantity)); } // 第五步重定向到购物车页面避免刷新时重复提交 resp.sendRedirect(req.getContextPath() /cart.jsp); } }这段代码里最需要向答辩老师解释的是第五步。为什么用sendRedirect而不是forward转发到 cart.jsp因为重定向会改变浏览器的请求地址用户刷新购物车页面时浏览器重新请求的是/cart.jsp而不是重新执行这个 Servlet——这就避免了刷新一次数量叠加一次的经典 bug。只用 forward 而不重定向的话一刷新页面就多一件商品属于购物车项目里最著名的翻车场景之一。req.getSession()的默认参数是 true表示如果没有 session 就新建一个。另一个重载req.getSession(false)是“拿不到就返回 null”常用于需要强制登录的接口做拦截。这里用无参版本是因为简易购物车允许游客加购。4.3 购物车页面JSTL 遍历与数量修改的一次到位写法购物车页面用 JSP JSTL 标签库渲染。这里必须注意JSTL 不是 Tomcat 自带的需要把 jstl-1.2.jar 放到 WEB-INF/lib 目录下否则页面报 “The absolute uri cannot be resolved”。在 IDEA 里跑 JavaWeb 项目时这一步漏了会在运行时报错但编译期不报属于典型的配置类坑% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title购物车/title /head body h2我的购物车/h2 c:choose c:when test${empty cart} p购物车还是空的去逛逛吧。/p /c:when c:otherwise table border1 tr th商品名/thth单价/thth数量/thth小计/thth操作/th /tr c:forEach items${cart} varitem tr td${item.product.name}/td td${item.product.price}/td td a hrefupdateCart?productId${item.product.id}quantity${item.quantity-1}-/a ${item.quantity} a hrefupdateCart?productId${item.product.id}quantity${item.quantity1}/a /td td${item.subtotal}/td tda hrefremoveFromCart?productId${item.product.id}删除/a/td /tr /c:forEach /table a hrefcheckout.jsp去结算/a /c:otherwise /c:choose /body /html数量加减用链接传参的方式实现是最朴素的解法点一下就跳转到 UpdateCart Servlet把当前的 productId 和新数量传过去。对应的 Servlet 里只需要一行关键逻辑遍历购物车匹配商品 ID 后调用setQuantity。如果传入的数量小于等于 0就把这条记录从 List 里 remove 掉相当于删除。一个接口同时承担修改和删除两种职责这是简易项目里避免类爆炸的常用手法。页面里${item.subtotal}能直接输出小计全靠前面 CartItem 里定义了getSubtotal()方法。这套写法里JSP 表达式语言只负责取值真正的计算落在 Java 模型的 getter 中分工清楚。如果你做的是数据库版购物车Servlet 里的逻辑会多一步加购时 INSERT 或 UPDATE 数据库之后再把最新购物车重新查出来 setAttribute 到 session 里页面上仍然用统一的 JSTL 渲染方式——这说明 JSP 展示层和存储方案是解耦的。5. 购物车项目的 4 个高频翻车点从中文乱码到库存超卖5.1 数据库中文变问号页面中文一片乱码现象商品表中插入中文商品名控制台查出来显示正常但页面上显示为一串问号或者乱码。原因数据从 Servlet/JSP 到 MySQL 链路中某一环的字符集不是 UTF-8。最常见的是 JDBC 连接 URL 漏了characterEncodingutf8mb4或者 MySQL 建表时没指定 CHARSET。解决把连接 URL 补上字符集参数建表语句统一用DEFAULT CHARSETutf8mb4JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 %。另一个低频坑是 Tomcat 的 GET 请求参数默认按 ISO-8859-1 解码中文参数传过来也是乱码需要在 Tomcat 的 server.xml 的 Connector 上加URIEncodingUTF-8。我的经验是先查 URL 参数再查连接串最后查 Tomcat 配置按这个顺序排查最省时间。5.2 同一件商品被存成了两条购物车记录现象用户把同一件商品加入购物车两次页面出现两行数量分别是 1 和 1而不是一行数量 2。原因加购逻辑里判断是否“同款”时用了item.getProduct().getName().equals(name)或者用item.getProduct().getId().equals(productId)但 ID 类型不匹配。解决统一用item.getProduct().getId() productId判断前提是两边都是基本类型 int。如果你写的 Product 类里 id 用了Integer包装类型比较的就会是引用地址这个问题排查起来很绕——建议 Product 和 CartItem 里的关键字段全部用基本类型 int避免包装类带来的坑。5.3 加了商品但页面没反应重定向路径不对现象点击“加入购物车”后浏览器地址栏变了但页面 404或者页面没变化。原因resp.sendRedirect(req.getContextPath() /cart.jsp)里的getContextPath()获取的是项目部署路径如果你在 IDEA 里给这个 Web 项目设定的 Application context 是/shopping-cart那么完整路径是/shopping-cart/cart.jsp。少了getContextPath()的前缀会直接跳到localhost:8080/cart.jsp路径不对自然 404。解决重定向和转发里的所有路径都加上req.getContextPath()前缀。这也是为什么我前面代码里每处 sendRedirect 都写这个不是冗余是血泪经验。5.4 库存越卖越多下单数量不对现象商品库存 10 件两个用户同时下单各自买了 8 件数据库扣减后库存变成 -6而实际库存应为 0。原因代码里先SELECT stock查出库存判断库存充足后执行UPDATE product SET stock stock - ?这两步之间存在间隙并发时第二个请求读到旧库存。解决把“判断库存”和“扣减库存”合并成一条 SQLUPDATE product SET stock stock - ? WHERE id ? AND stock ?。这条语句里stock ?就是乐观锁的逻辑——如果库存不足影响行数为 0程序里判断int rows ps.executeUpdate()rows 等于 0 说明扣减失败提示用户库存不足。注意不管库存够不够都不用先查一遍一条 UPDATE 全搞定。5.5 Session 里的购物车突然全空了现象购物车用得好好的Tomcat 重启后所有用户的购物车都消失了或者用户长时间不操作后来看购物车是空的。原因Session 默认存储在服务器内存Tomcat 重启内存被清空。会话超时时间默认 30 分钟超过这个时间没有请求session 自动失效。解决课设阶段给 Tomcat 的 web.xml 里配置session-configsession-timeout120/session-timeout/session-config把超时调到 120 分钟。要真正解决重启丢数据的问题必须把购物车落库登录用户每次加购直接写 MySQL。如果嫌每次操作都查库太慢可以把 Redis 作为中间层Session 只是缓存这是生产环境的常见做法但课设做不做取决于你的时间。6. 从简易购物车到能部署的项目还剩这几步如果照着上面的代码跑通了你的购物车已经是一个“能答辩”的作品。但离“能上线”还差三层功夫我建议按顺序补。第一层是库存扣减的原子性。前面避坑部分提到用条件 UPDATE 防止超卖但真正下单时要保证“扣库存”和“生成订单”要么同时成功要么同时失败。这就要引入事务在 Service 层开启conn.setAutoCommit(false)扣库存和插入订单表这两条 SQL 都执行成功后conn.commit()任何一条抛异常就conn.rollback()。这是答辩时最能拉开差距的加分项因为你用上了数据库的核心能力。第二层是购物车数据结构的升级。把 Session 里的ArrayListCartItem换成 Redis 的 Hash 结构key 是cart:userIdfield 是productIdvalue 是quantity。加购操作变成一条HINCRBY命令查购物车变成HGETALL。接口层不变Servlet 里的代码改动很小但整个项目的架构级别提升了一大截。这一步做完后你会发现 Session 方案和数据库方案、缓存方案之间差的不是“原理”而是“数据放哪”这一个决策。第三层是前端细节打磨。购物车条目一般配有商品图标可以接 iconfont 的矢量图标库把 add-cart 图标和数字角标联动起来——商品加入购物车后图标右上角的小红点自增。这一步不改变后端逻辑但让页面看起来“像样”。记住一个教训按钮的点击频率要防抖否则用户快速点三下加购会发出三个请求数量变成 3这是体验问题不是功能缺陷。最后说一个我的习惯每次改完购物车相关代码一定要跑一次“清空购物车 → 重新加购 → 改数量 → 删除 → 再加购”的完整流程再开两个浏览器窗口同时下单同一个商品测库存。这两个操作能挡掉绝大多数购物车项目的隐性故障。希望帮到你。本文还有配套的精品资源点击获取