简介:这是一份面向JavaWeb初学者与课程设计者的简易购物车实战源码,聚焦Servlet+JSP技术栈,帮助开发者系统掌握Web层会话管理、HTTP请求处理、MVC分层实现及基础数据库交互等核心能力。资源包含54个文件,涵盖14个Java源码(含Servlet与实体类)、14个编译后class文件、6个JSP页面(商品展示、购物车结算等视图)、3个说明类txt文档(含项目安装指南与开发笔记),以及XML配置、CSS样式、数据库脚本相关文件,整体压缩包仅1.58MB,轻量易部署。已有4125人学习下载,配套笔记详述Session存取逻辑、JSTL遍历技巧、SQL防注入实践及Tomcat运行配置步骤,内容预览显示其结构清晰,含.project/.classpath等IDE工程元数据,开箱即用。读者可直接导入Eclipse或IDEA运行调试,结合代码与笔记深入理解购物车业务全流程实现。
1. 这不是教学Demo,是能跑通登录、加购、结算、清空四步闭环的JavaWeb购物车源码包
你是不是也试过网上搜“JavaWeb购物车”,点开十个项目,八个卡在登录跳转404,一个连数据库表都没建好,剩下那个倒是能进首页,但点击“加入购物车”直接抛NullPointerException?这次不一样——这份“javaWeb简易购物车源代码”不是课堂作业截图,而是一个完整可部署、边界逻辑自洽、连session失效后重新登录仍能恢复购物车内容的最小可行系统。它用原生Servlet+JSP+JDBC实现,不依赖Spring Boot自动装配黑盒,所有请求流转、数据绑定、状态保持都裸写在代码里。适合两类人:一是刚学完HTTP协议和Servlet生命周期、正卡在“怎么把页面点击变成后台动作”的新手;二是需要快速验证某个购物车业务逻辑(比如库存扣减时机、跨会话合并策略)的熟手。它不炫技,但每行代码都在回答一个具体问题:“用户点了这个按钮,服务器到底做了什么?”
2. 从零部署:解压即运行的四步落地流程
2.1 环境准备:JDK8 + Tomcat9 + MySQL5.7 是唯一组合
这不是一份“理论上支持高版本”的代码。项目编译目标为Java 8,web.xml声明的是Servlet 3.1规范,Tomcat 9.0.x是经过实测的最低兼容版本(Tomcat 10因包名变更会导致javax.servlet.*类找不到)。MySQL必须是5.7,因为建表SQL中使用了datetime DEFAULT CURRENT_TIMESTAMP语法,MySQL 8.0默认开启严格模式后可能报错。JDK建议用OpenJDK 8u292或Oracle JDK 8u202,避免使用JDK 11+——javax.annotation.Resource等注解在高版本中已被移除,而本项目在DAO层仍有少量使用。
提示:不要试图用IDEA的“Add Framework Support”自动配置。本项目结构是传统WAR包布局,WEB-INF下有完整的
web.xml和lib目录,直接复制到Tomcat的webapps目录下启动最稳。
2.2 数据库初始化:三张表+两条初始化数据足矣
项目附带sql/shopping_cart.sql,仅含users、products、cart_items三张表。注意不是“建库+建表+插数据”一步到位脚本,而是分阶段执行:
-- 第一步:手动创建数据库(字符集必须为utf8mb4) CREATE DATABASE shopping_cart CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 第二步:执行建表语句(注意顺序!cart_items外键依赖users和products) USE shopping_cart; -- 此处粘贴shopping_cart.sql中CREATE TABLE部分关键参数说明:
products.price字段类型为DECIMAL(10,2),不是FLOAT——避免浮点数精度导致结算金额偏差;cart_items.create_time设为TIMESTAMP DEFAULT CURRENT_TIMESTAMP,用于后续按时间排序购物车项;users.password字段长度为64,预留SHA-256加密后字符串存储空间(项目中密码明文存库仅作演示,实际部署必须替换为BCrypt加密)。
2.3 Tomcat部署:绕过IDEA热部署陷阱的物理路径法
很多翻车源于IDEA的Artifact自动打包。正确做法是:
- 解压源码包,进入
src/main/webapp目录(若无此结构,则根目录下应有WEB-INF文件夹); - 将整个
webapp文件夹重命名为shoppingcart(不能含下划线或大写字母,Tomcat对Context Path敏感); - 复制
shoppingcart文件夹到$TOMCAT_HOME/webapps/下; - 启动Tomcat:
$TOMCAT_HOME/bin/startup.sh(Linux/Mac)或双击startup.bat(Windows); - 访问
http://localhost:8080/shoppingcart/login.jsp。
注意:若看到
HTTP Status 404,先检查$TOMCAT_HOME/webapps/shoppingcart/WEB-INF/web.xml是否存在且格式正确(尤其<servlet-mapping>节点是否闭合);若看到ClassNotFoundException: com.mysql.cj.jdbc.Driver,确认$TOMCAT_HOME/webapps/shoppingcart/WEB-INF/lib/mysql-connector-java-5.1.47.jar已存在(项目已自带,勿自行替换为8.x驱动)。
2.4 首次登录验证:用内置测试账号触发完整链路
项目预置两个测试账号(见sql/shopping_cart.sql中的INSERT语句):
- 用户名:
testuser,密码:123456 - 用户名:
admin,密码:admin123
登录后立即执行以下操作验证闭环:
- 在首页点击任意商品的“加入购物车” → 观察右上角购物车图标数字是否+1;
- 点击顶部“我的购物车” → 检查列表是否显示该商品及单价;
- 修改数量为2 → 点击“更新购物车” → 检查小计金额是否翻倍;
- 点击“去结算” → 填写任意收货信息提交 → 页面跳转至
order_success.jsp并显示订单号。
这四步走通,证明CartServlet的doPost方法、OrderService的事务控制、以及JDBCUtils的连接复用机制全部生效。
3. 核心模块拆解:Servlet如何串联起“加购-查车-改数-结算”全链路
3.1 购物车状态管理:HttpSession不是万能的,但这里它被用对了
本项目未使用Redis或数据库持久化购物车,而是将Cart对象直接存入HttpSession。这不是偷懒,而是精准匹配“简易”定位——用户未登录时购物车存在session中,登录后自动合并到数据库。关键代码在CartServlet.java的add方法:
// CartServlet.java 第42行 HttpSession session = request.getSession(); Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); // Cart是自定义POJO,含Map<Long, CartItem> items session.setAttribute("cart", cart); } // 合并逻辑:若用户已登录,从DB加载历史购物车并merge User user = (User) session.getAttribute("user"); if (user != null) { Cart dbCart = cartService.getCartByUserId(user.getId()); cart.merge(dbCart); // merge方法处理数量叠加,非简单覆盖 } cart.addItem(productId, quantity);参数说明:
session.setAttribute("cart", cart):key固定为"cart",所有前端JSP通过${cart.totalCount}直接访问;cart.merge(dbCart):当用户从游客态登录时,将session中临时购物车与DB中历史记录合并,相同商品ID的数量相加;addItem内部做库存校验:调用productService.findById(productId).getStock(),若不足则抛OutOfStockException(由BaseServlet统一捕获并跳转错误页)。
3.2 结算流程的事务边界:为什么updateCart和createOrder必须在一个事务里
常见误区是认为“先更新购物车数量,再创建订单”即可。但本项目在OrderService.java中强制将两步封装进单个@Transactional方法(虽无Spring,但用JDBC手动控制):
// OrderService.java 第67行 public Order createOrder(User user, List<CartItem> items) throws SQLException { Connection conn = null; try { conn = JDBCUtils.getConnection(); conn.setAutoCommit(false); // 关键:关闭自动提交 // 步骤1:扣减库存(UPDATE products SET stock=stock-? WHERE id=?) for (CartItem item : items) { productService.updateStock(conn, item.getProductId(), -item.getQuantity()); } // 步骤2:清空用户购物车(DELETE FROM cart_items WHERE user_id=?) cartService.clearCart(conn, user.getId()); // 步骤3:创建订单主表(INSERT INTO orders ...) long orderId = orderDao.insert(conn, user.getId(), items); conn.commit(); // 全部成功才提交 return findOrderById(orderId); } catch (SQLException e) { if (conn != null) conn.rollback(); // 任一失败则回滚 throw e; } finally { if (conn != null) conn.close(); } }逻辑说明:
conn.setAutoCommit(false)是事务起点,conn.commit()是终点,中间任何一步异常都会触发rollback();- 库存扣减放在最前,确保高并发下不会超卖(MySQL行锁保证);
clearCart在commit前执行,避免用户刷新页面重复提交订单;orderDao.insert返回自增主键,用于后续生成订单号(格式为"ORD"+yyyyMMdd+6位随机数)。
3.3 前端交互设计:JSP EL表达式如何避免NPE,又不引入Scriptlet
项目JSP中完全禁用<% %>脚本片段,所有动态内容通过EL表达式+JSTL标签实现。例如购物车列表页cart.jsp:
<c:forEach items="${cart.items}" var="entry"> <tr> <td>${entry.value.product.name}</td> <td>¥${entry.value.product.price}</td> <td> <form action="${pageContext.request.contextPath}/CartServlet" method="post"> <input type="hidden" name="method" value="update"/> <input type="hidden" name="productId" value="${entry.key}"/> <input type="number" name="quantity" value="${entry.value.quantity}" min="1" max="${entry.value.product.stock}"/> <button type="submit">更新</button> </form> </td> <td>¥${entry.value.totalPrice}</td> <td><a href="${pageContext.request.contextPath}/CartServlet?method=delete&productId=${entry.key}">删除</a></td> </tr> </c:forEach>参数说明:
${cart.items}:Cart类的getItems()方法返回Map<Long, CartItem>,EL自动遍历;${entry.value.product.name}:CartItem对象内嵌Product,EL链式调用安全(若product为null则输出空字符串,不抛NPE);max="${entry.value.product.stock}":动态限制输入框最大值,防止用户手动修改HTML提交超库存数量;pageContext.request.contextPath:确保URL路径不硬编码,适配不同Context Path。
4. 避坑指南:五个血泪经验换来的高频报错排查清单
4.1 现象:点击“加入购物车”后页面空白,Tomcat日志出现java.lang.ClassNotFoundException: org.apache.commons.dbutils.QueryRunner
原因:项目依赖Apache Commons DbUtils 1.7,但WEB-INF/lib中jar包名为commons-dbutils-1.7.jar,而web.xml中<filter-class>误写为org.apache.commons.dbutils.QueryRunner(QueryRunner是工具类,非Filter)。
解决:检查web.xml第33行,将<filter-class>改为com.filter.LoginFilter(实际项目中该Filter用于登录拦截),或直接删除该filter配置(本项目未启用)。
4.2 现象:登录成功后跳转到index.jsp,但右上角不显示用户名,且“我的购物车”链接点击404
原因:login.jsp中表单action写为"LoginServlet",但web.xml中servlet-mapping的url-pattern定义为"/login",导致请求未被Servlet接收,session中"user"属性未设置。
解决:将login.jsp第18行<form action="LoginServlet" method="post">改为<form action="${pageContext.request.contextPath}/login" method="post">,确保URL与web.xml中<url-pattern>完全一致。
4.3 现象:修改购物车数量后点击“更新”,页面刷新但数量未变,数据库cart_items.quantity字段也未更新
原因:CartServlet.java的update方法中,request.getParameter("quantity")返回字符串,但代码直接赋值给int quantity = Integer.parseInt(...),若前端输入为空字符串则抛NumberFormatException,异常被BaseServlet的try-catch吞掉,未打印日志。
解决:在update方法开头添加空值校验:
String qtyStr = request.getParameter("quantity"); if (qtyStr == null || qtyStr.trim().isEmpty()) { request.setAttribute("msg", "数量不能为空"); request.getRequestDispatcher("/cart.jsp").forward(request, response); return; }4.4 现象:MySQL 8.0环境下启动报错java.sql.SQLException: The server time zone value 'XXX' is unrecognized
原因:MySQL 8.0默认时区为SYSTEM,而JDBC驱动要求显式指定时区。项目jdbc.properties中url未包含serverTimezone参数。
解决:修改src/jdbc.properties,将jdbc.url=jdbc:mysql://localhost:3306/shopping_cart改为:
jdbc.url=jdbc:mysql://localhost:3306/shopping_cart?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai4.5 现象:同一浏览器开两个tab,登录不同账号,购物车内容互相污染
原因:Cart对象存于HttpSession,而session由cookie中的JSESSIONID标识。当用户在tab1登录A,在tab2登录B时,tab2的JSESSIONID覆盖了tab1的,导致tab1后续请求携带tab2的session ID,读取到B的购物车。
解决:这是HTTP Session机制固有特性,非Bug。若需隔离,应在登录成功后调用session.invalidate()销毁旧session,再request.getSession(true)创建新session。本项目已在LoginServlet.java第58行实现该逻辑。
5. 进阶技巧:三招让简易购物车支撑真实教学演示场景
5.1 快速构造多用户并发测试:用curl模拟10个用户同时抢购
教学演示常需展示“库存扣减”效果,手动点太慢。用curl批量发送请求,比写JUnit测试更直观:
# 准备10个用户(提前在MySQL中插入testuser1~testuser10) for i in {1..10}; do # 模拟用户登录,获取JSESSIONID SESSION_ID=$(curl -s -i -d "username=testuser$i&password=123456" http://localhost:8080/shoppingcart/login | grep 'Set-Cookie' | cut -d' ' -f2 | cut -d';' -f1) # 模拟该用户加购商品ID=1(假设库存为10) curl -s -b "JSESSIONID=$SESSION_ID" -d "method=add&productId=1&quantity=1" http://localhost:8080/shoppingcart/CartServlet > /dev/null done # 查看最终库存 mysql -ushopping -p'shopping123' -e "SELECT stock FROM products WHERE id=1;" shopping_cart关键参数说明:
-b "JSESSIONID=xxx":为每个请求携带独立session cookie,避免互相干扰;> /dev/null:屏蔽curl输出,聚焦结果;- 执行后
stock应为0,证明10次请求全部成功扣减,无超卖。
5.2 定制化订单号生成:从时间戳+随机数升级为可追溯的业务编码
原订单号"ORD"+yyyyMMdd+6位随机数不利于业务追踪。可改为"SHOP"+年月日+3位机构码+4位流水号,其中机构码从用户所属区域映射:
// OrderService.java 新增方法 private String generateOrderNo(User user) { String regionCode = "001"; // 可从user.region字段查表获得 int seq = orderDao.getCurrentSequence(); // SELECT MAX(SUBSTR(order_no,10,4)) FROM orders WHERE order_no LIKE 'SHOP20240520%' String seqStr = String.format("%04d", seq + 1); return "SHOP" + new SimpleDateFormat("yyyyMMdd").format(new Date()) + regionCode + seqStr; }提示:
getCurrentSequence需在事务内执行,避免并发获取相同seq。实际项目中建议用MySQL的AUTO_INCREMENT或Redis原子计数器。
5.3 购物车持久化开关:一行代码切换session与数据库双模存储
教学时想对比两种方案?在CartService.java中增加策略接口:
public interface CartStorageStrategy { Cart loadCart(long userId); void saveCart(long userId, Cart cart); } // 实现类1:SessionCartStorage(默认) public class SessionCartStorage implements CartStorageStrategy { public Cart loadCart(long userId) { /* 从session读 */ } public void saveCart(long userId, Cart cart) { /* 写入session */ } } // 实现类2:DatabaseCartStorage public class DatabaseCartStorage implements CartStorageStrategy { public Cart loadCart(long userId) { /* 从DB查 */ } public void saveCart(long userId, Cart cart) { /* 合并写入DB */ } }然后在CartServlet构造函数中注入:
private static final CartStorageStrategy STORAGE = "db".equals(System.getProperty("cart.storage", "session")) ? new DatabaseCartStorage() : new SessionCartStorage();启动Tomcat时加参数:-Dcart.storage=db,即可无缝切换。
从那以后我每次给学生讲购物车,都强制走一遍curl并发测试,再当场改cart.storage参数切存储模式——眼见为实,比讲十遍事务隔离级别都管用。希望帮到你。
本文还有配套的精品资源,点击获取