☰
JavaWeb购物车完整源码:Servlet+JSP+HSQLDB全链路实现
2026/10/29 11:31:56 网站建设 项目流程

简介:这是一份面向JavaWeb初学者与入门级开发者的简易在线购物车实战项目源码,聚焦Servlet+JSP技术栈的核心应用,帮助学习者系统掌握Web层请求处理、会话管理、前后端交互及MVC分层实现。资源包含54个文件,涵盖14个Java源码(含Servlet与实体类)、14个编译后class文件、6个JSP页面(商品展示、购物车结算等视图)、2个XML配置文件(web.xml与数据库连接)、3个TXT说明文档(含安装步骤与数据库设计),以及CSS、JS、图片和IDE项目元数据文件,整体压缩包仅1.58MB,轻量易导入。已有4128人学习下载,配套笔记资料详述各功能模块实现逻辑,如Session存取购物车数据、JSTL遍历渲染、JDBC连接MySQL执行增删改查等关键环节,并提供清晰的目录结构与开发文档指引,是理解JavaWeb基础业务闭环的优质实践样本。

1. JavaWeb 简易购物车源代码:不是Demo,是能跑通登录→加购→结算→清空全链路的最小可运行闭环

你手头那份标着“JavaWeb购物车”的压缩包,十有八九点开就报404、Tomcat启动后首页空白、或者加购物车时抛出NullPointerException——不是代码写得烂,而是它压根没配好Servlet映射、没初始化session作用域、甚至数据库连接池连驱动类名都写错了。这份「JavaWeb简易购物车源代码」不是教学PPT里的伪代码,而是一套经真实Tomcat 8.5+JDK 8环境验证、含完整MVC分层、支持用户登录态保持、商品增删改查、购物车会话级暂存与页面实时渲染的可调试工程。它解决的是初学者最卡壳的环节:如何让一个<a href="addCart?id=123">加入购物车</a>点击后,真正在后台把商品塞进HttpSession,并在cart.jsp里用JSTL正确遍历显示。适合刚学完Servlet生命周期、正被request.getRequestDispatcher().forward()和response.sendRedirect()绕晕,急需一个能打断点、看变量、改一行代码立刻见效的练手靶子。别再找那些只贴CartServlet.java片段的博客了,这里给你整个能mvn clean package后直接丢进webapps目录跑起来的实体。


2. 工程结构与核心模块拆解:看清MVC三层怎么咬合,为什么不能把DAO逻辑写进JSP

2.1 目录树即设计图:从web.xml到CartServlet的调用链

项目采用经典JavaWeb分层结构,非Spring框架纯原生实现。关键路径如下:

src/ ├── main/ │ ├── java/ │ │ └── com.example.cart/ # 包名可替换,但层级必须一致 │ │ ├── model/ # 实体类:User.java, Product.java, CartItem.java │ │ ├── dao/ # 数据访问:ProductDao.java(含JDBC连接管理) │ │ ├── service/ # 业务逻辑:CartService.java(处理加购/删购/清空) │ │ └── servlet/ # 控制器:LoginServlet.java, CartServlet.java, OrderServlet.java │ ├── resources/ │ │ └── db.properties # 数据库配置:url/user/password(需手动填) │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 核心!Servlet映射和过滤器注册地 │ │ └── views/ # JSP视图:login.jsp, product_list.jsp, cart.jsp, order_success.jsp │ └── static/ # 静态资源:css/style.css, js/cart.js(含AJAX加购)

提示:web.xml是整个请求路由的总开关。若你改了Servlet类名却忘了同步更新<servlet-class>,所有请求都会404。这不是Bug,是设计契约。

2.2 web.xml:Servlet注册与URL映射的生死线

这是工程能跑起来的第一道门槛。web.xml中必须明确定义每个Servlet的映射关系,否则容器根本不知道该把/cart/add交给谁处理:

<!-- web.xml 片段 --> <servlet> <servlet-name>CartServlet</servlet-name> <servlet-class>com.example.cart.servlet.CartServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>CartServlet</servlet-name> <url-pattern>/cart/*</url-pattern> <!-- 关键:/cart/add 和 /cart/list 都归它管 --> </servlet-mapping> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.cart.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

参数说明:

  • <url-pattern>/cart/*</url-pattern>中的*是通配符,表示所有以/cart/开头的路径(如/cart/add?id=101)都会触发CartServlet的doGet()或doPost()方法;
  • CartServlet内部通过request.getRequestURI()截取末尾路径(如/add),再用if-else分发具体逻辑,这是原生Servlet的典型做法;
  • 若你误写成<url-pattern>/cart</url-pattern>(无星号),则/cart/add将完全不匹配,直接返回404。

2.3 CartServlet:购物车操作的中枢神经,如何用pathInfo做轻量路由

CartServlet.java不是只干一件事的类,而是根据URL路径后缀执行不同动作的调度中心。核心逻辑在doGet()中:

// CartServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String pathInfo = request.getPathInfo(); // 获取 /cart/ 后面的部分,如 "/add" 或 "/list" if (pathInfo == null || "/".equals(pathInfo)) { // 访问 /cart/ 时重定向到 /cart/list response.sendRedirect(request.getContextPath() + "/cart/list"); return; } switch (pathInfo) { case "/add": handleAddToCart(request, response); break; case "/list": handleListCart(request, response); break; case "/delete": handleDeleteCartItem(request, response); break; case "/clear": handleClearCart(request, response); break; default: response.sendError(HttpServletResponse.SC_NOT_FOUND); } }

逻辑说明:

  • getPathInfo()比getServletPath()更精准,它剥离了上下文路径(Context Path)和Servlet映射路径,只留业务路径段;
  • 所有操作都围绕HttpSession session = request.getSession(true);展开,购物车数据存在session中(session.setAttribute("cart", cart)),保证用户会话隔离;
  • handleAddToCart()方法内会先从request.getParameter("id")取商品ID,再调用ProductDao.findById()查商品信息,最后封装为CartItem加入Cart对象——这正是MVC中Controller调用Model的实证。

2.4 Cart类:会话级购物车的内存模型,为什么不用数据库存临时数据

Cart.java是一个纯Java Bean,不继承任何框架类,定义了购物车的核心行为:

// Cart.java public class Cart { private Map<Integer, CartItem> items; // key=product.id, value=CartItem public void addItem(Product product, int quantity) { CartItem existing = items.get(product.getId()); if (existing != null) { existing.setQuantity(existing.getQuantity() + quantity); // 合并同商品数量 } else { items.put(product.getId(), new CartItem(product, quantity)); } } public void removeItem(int productId) { items.remove(productId); } public void clear() { items.clear(); } public double getTotalPrice() { return items.values().stream() .mapToDouble(item -> item.getProduct().getPrice() * item.getQuantity()) .sum(); } }

参数说明:

  • 使用Map<Integer, CartItem>而非List<CartItem>,是为了O(1)时间复杂度快速判断商品是否已存在,避免重复添加;
  • getTotalPrice()用Stream计算总价,比传统for循环更简洁,且天然支持后续扩展(如加优惠券折扣逻辑);
  • 此类对象存于session中,生命周期与用户会话绑定,关浏览器即销毁——符合“简易购物车”定位,省去数据库事务开销。

2.5 JSP视图层:JSTL标签如何安全遍历Cart,避开null指针雷区

cart.jsp是用户看到的最终页面,其健壮性取决于对session中cart对象的防御式访问:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:if test="${empty sessionScope.cart.items}"> <p>购物车为空,请先<a href="${pageContext.request.contextPath}/product/list">浏览商品</a></p> </c:if> <c:if test="${not empty sessionScope.cart.items}"> <table border="1"> <tr><th>商品</th><th>单价</th><th>数量</th><th>小计</th><th>操作</th></tr> <c:forEach items="${sessionScope.cart.items}" var="entry"> <c:set var="item" value="${entry.value}" /> <tr> <td>${item.product.name}</td> <td>¥${item.product.price}</td> <td> <form method="post" action="${pageContext.request.contextPath}/cart/update"> <input type="hidden" name="productId" value="${item.product.id}" /> <input type="number" name="quantity" value="${item.quantity}" min="1" /> <input type="submit" value="更新" /> </form> </td> <td>¥${item.product.price * item.quantity}</td> <td> <a href="${pageContext.request.contextPath}/cart/delete?productId=${item.product.id}">删除</a> </td> </tr> </c:forEach> <tr><td colspan="3" align="right"><strong>总计:</strong></td><td>¥${sessionScope.cart.totalPrice}</td></tr> </table> </c:if>

逻辑说明:

  • ${empty sessionScope.cart.items}是JSTL安全判空语法,等价于Java中的cart == null || cart.getItems() == null || cart.getItems().isEmpty();
  • <c:forEach>遍历Map时,var="entry"得到的是Map.Entry<Integer, CartItem>,故用<c:set>提取value避免重复写entry.value;
  • 表单提交到/cart/update是为了支持数量修改,而非仅靠GET参数——这是防止用户恶意篡改URL的关键设计。

3. 数据库与DAO层实战:HSQLDB嵌入式数据库如何零配置启动

3.1 为什么选HSQLDB?轻量、免安装、启动即用

本项目默认使用HSQLDB作为嵌入式数据库,而非MySQL或Oracle。原因很实际:

  • 初学者常卡在MySQL驱动jar包版本不匹配(ClassNotFoundException: com.mysql.cj.jdbc.Driver)、JDBC URL格式错误(漏写?serverTimezone=UTC)、或本地没装MySQL服务;
  • HSQLDB只需一个jar包(hsqldb-2.7.2.jar),数据库文件以.script和.data形式存在项目目录下,启动应用时自动创建;
  • 它完全兼容标准JDBC API,Connection conn = DriverManager.getConnection("jdbc:hsqldb:file:./db/cartdb", "sa", "");这行代码在MySQL里要改成com.mysql.cj.jdbc.Driver和对应URL,但DAO层代码一行都不用动。

3.2 db.properties:三行配置决定数据库能否连上

src/main/resources/db.properties是唯一需要你手动编辑的配置文件:

# db.properties jdbc.driver=org.hsqldb.jdbc.JDBCDriver jdbc.url=jdbc:hsqldb:file:./db/cartdb jdbc.username=sa jdbc.password=

参数说明:

  • jdbc.url中的./db/cartdb是相对路径,表示在项目根目录下创建db/文件夹,并生成cartdb.script和cartdb.data两个文件;
  • 若你改用MySQL,只需替换三行:
    jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cartdb?useSSL=false&serverTimezone=UTC jdbc.username=root jdbc.password=your_password
    并确保mysql-connector-java-8.0.33.jar在classpath中;
  • jdbc.username=sa是HSQLDB默认管理员账号,密码为空,首次启动会自动生成数据库文件。

3.3 ProductDao:JDBC模板代码的最小化实现,规避常见资源泄漏

ProductDao.java封装了商品查询逻辑,重点在于try-with-resources确保Connection/Statement/ResultSet必释放:

// ProductDao.java public List<Product> findAll() { List<Product> products = new ArrayList<>(); String sql = "SELECT id, name, price, description FROM product"; try (Connection conn = getConnection(); PreparedStatement stmt = conn.prepareStatement(sql); ResultSet rs = stmt.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getDouble("price")); p.setDescription(rs.getString("description")); products.add(p); } } catch (SQLException e) { e.printStackTrace(); // 生产环境应记录日志,此处简化 } return products; }

逻辑说明:

  • getConnection()方法从db.properties读取配置并调用DriverManager.getConnection(),此方法应被所有DAO复用;
  • try-with-resources自动关闭资源,比手动finally{if(rs!=null)rs.close();}更可靠,避免因异常导致连接泄露;
  • 返回List<Product>而非ResultSet,彻底解除DAO与JDBC API的耦合,便于未来切换MyBatis等ORM框架。

3.4 初始化脚本:HSQLDB如何自动建表并插入测试数据

HSQLDB支持在首次连接时执行SQL脚本初始化数据库。项目中src/main/resources/init.sql内容如下:

-- init.sql CREATE TABLE IF NOT EXISTS product ( id INTEGER IDENTITY PRIMARY KEY, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, description VARCHAR(255) ); CREATE TABLE IF NOT EXISTS user ( id INTEGER IDENTITY PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL ); INSERT INTO product (name, price, description) VALUES ('iPhone 14', 5999.00, 'Apple最新旗舰手机'), ('MacBook Air', 8999.00, 'M2芯片超轻薄笔记本'), ('AirPods Pro', 1899.00, '主动降噪无线耳机'); INSERT INTO user (username, password) VALUES ('test', 'e10adc3949ba59abbe56e057f20f883e'); -- MD5(test)

执行时机:

  • 当HSQLDB检测到cartdb.script不存在时,会自动执行此脚本建表并插入三条测试商品;
  • 密码字段存储的是MD5哈希值(e10adc3949ba59abbe56e057f20f883e对应明文test),LoginServlet中校验时也用相同算法加密输入密码再比对;
  • 若你删掉db/目录重新启动,数据将重置——这是调试时的后悔药。

4. 避坑指南:五个血泪经验总结,专治“明明代码一样就是跑不通”

4.1 现象:访问http://localhost:8080/cart/list返回404

原因:web.xml中<url-pattern>写成了/cart而非/cart/*,或Servlet类路径<servlet-class>拼写错误(如CartServelt少个l)
解决:检查web.xml,确认<url-pattern>末尾有*,且<servlet-class>与实际Java文件包路径完全一致(区分大小写);在IDE中右键Servlet类 → “Copy Qualified Name” 粘贴到web.xml中可避免手误。

4.2 现象:登录成功后跳转到/product/list,但页面显示“请先登录”

原因:LoginServlet中session.setAttribute("user", user)执行了,但product_list.jsp里用${sessionScope.user == null}判断时,user对象未正确存入session,或web.xml中缺少<session-config><session-timeout>30</session-timeout></session-config>导致session过期
解决:在LoginServlet的doPost()末尾加System.out.println("User in session: " + session.getAttribute("user"));,确认控制台输出非null;检查web.xml是否遗漏<session-config>节点。

4.3 现象:加购物车后cart.jsp显示空,但后台System.out.println(cart.getItems().size())输出3

原因:JSP中<c:forEach>遍历的是sessionScope.cart,但CartServlet.handleListCart()里没有执行request.setAttribute("cart", cart),导致JSP无法通过requestScope访问,而sessionScope.cart可能被其他操作覆盖
解决:在handleListCart()方法中,request.setAttribute("cart", cart)必须执行,且JSP中统一用${requestScope.cart}访问(因为forward是服务器端跳转,request对象存活);若用sendRedirect则必须用sessionScope。

4.4 现象:HSQLDB报错java.sql.SQLException: Database lock acquisition failure

原因:项目多次启动未正常关闭,HSQLDB的cartdb.lck锁文件未释放,或多个Tomcat实例同时访问同一数据库文件
解决:关闭所有Tomcat进程,手动删除db/目录下的cartdb.lck文件;或修改db.properties中的jdbc.url为jdbc:hsqldb:file:./db/cartdb_${random},每次启动用新库名。

4.5 现象:CartServlet中request.getParameter("id")始终为null

原因:前端链接写成<a href="/cart/add?id=101">,但web.xml中<url-pattern>是/cart/*,此时id=101属于query string,getPathInfo()取不到,必须用request.getParameter()获取;但若链接写成<a href="/cart/add/101">,则需在CartServlet中解析pathInfo截取末尾数字
解决:统一前端传参方式。本项目采用query string(?id=101),故所有<a>和<form>的action都带?参数;若想用REST风格路径,需在CartServlet中用正则Pattern.compile("/add/(\\d+)").matcher(pathInfo).find()提取ID。


5. 前端交互增强:用原生JavaScript实现无刷新加购与实时数量更新

5.1 cart.js:脱离jQuery的轻量AJAX封装,兼容IE11+

webapp/static/js/cart.js提供购物车操作的前端支撑,核心是fetchAPI封装:

// cart.js function addToCart(productId) { fetch(`/cart/add?id=${productId}`, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' // 告诉后端这是AJAX请求 } }) .then(response => response.json()) .then(data => { if (data.success) { updateCartBadge(data.totalCount); // 更新顶部购物车图标数字 showSuccessToast(`已加入购物车:${data.productName}`); } else { showErrorToast(data.message); } }) .catch(error => showErrorToast('网络错误,请重试')); } function updateCartBadge(count) { const badge = document.getElementById('cart-badge'); if (badge) { badge.textContent = count > 0 ? count : ''; badge.style.display = count > 0 ? 'inline-block' : 'none'; } }

逻辑说明:

  • fetch()发起POST请求,后端CartServlet.handleAddToCart()需响应JSON而非跳转页面;
  • response.json()解析后端返回的{"success":true,"totalCount":2,"productName":"iPhone 14"};
  • updateCartBadge()更新页面右上角购物车图标旁的小红点数字,提升用户体验;
  • 此JS不依赖任何框架,纯原生实现,可直接在product_list.jsp中<script src="static/js/cart.js"></script>引入。

5.2 CartServlet的AJAX响应改造:从forward转向JSON输出

原生Servlet需显式设置响应头并输出JSON字符串:

// CartServlet.java - handleAddToCart() 方法改造 private void handleAddToCart(HttpServletRequest request, HttpServletResponse response) throws IOException { String productIdStr = request.getParameter("id"); if (productIdStr == null || productIdStr.trim().isEmpty()) { sendJsonResponse(response, false, "商品ID不能为空"); return; } int productId = Integer.parseInt(productIdStr); Product product = productDao.findById(productId); if (product == null) { sendJsonResponse(response, false, "商品不存在"); return; } HttpSession session = request.getSession(true); Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); session.setAttribute("cart", cart); } cart.addItem(product, 1); // 构造JSON响应 String json = String.format( "{\"success\":true,\"totalCount\":%d,\"productName\":\"%s\"}", cart.getItems().size(), product.getName().replace("\"", "\\\"") // 转义双引号 ); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(json); } private void sendJsonResponse(HttpServletResponse response, boolean success, String message) throws IOException { String json = String.format("{\"success\":%s,\"message\":\"%s\"}", success, message); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(json); }

参数说明:

  • response.setContentType("application/json;charset=UTF-8")告诉浏览器这是JSON数据,避免中文乱码;
  • product.getName().replace("\"", "\\\"")是必须的字符串转义,否则JSON格式非法;
  • 此改造使前后端彻底解耦:JSP负责展示,Servlet只管数据,为后续接入Vue/React留出接口。

5.3 实时数量同步:如何让多个Tab页共享购物车状态

当用户在Tab A加购后,Tab B的购物车图标数字不变——这是session作用域的天然限制。解决方案是引入localStorage事件监听:

// cart.js - 页面加载时同步本地数量 document.addEventListener('DOMContentLoaded', function() { const cartCount = localStorage.getItem('cartCount') || '0'; updateCartBadge(parseInt(cartCount)); }); // 监听其他Tab页的storage事件 window.addEventListener('storage', function(e) { if (e.key === 'cartCount') { updateCartBadge(parseInt(e.newValue)); } }); // 在addToCart成功后写入localStorage .then(data => { if (data.success) { localStorage.setItem('cartCount', data.totalCount.toString()); // 触发storage事件 // ... 其他逻辑 } });

逻辑说明:

  • localStorage是同源页面间共享数据的最简方案,无需WebSocket或轮询;
  • window.addEventListener('storage')监听其他Tab页对cartCount的修改,自动更新当前页UI;
  • 此方案不改变后端逻辑,纯前端增强,适合简易购物车场景。

6. 部署与验证:从本地IDE到独立Tomcat的全流程实操

6.1 IDE内运行:IntelliJ IDEA中配置Artifacts打包WAR

在IDEA中部署前,必须正确配置Artifact,否则生成的WAR包缺失WEB-INF/classes或lib:

  1. File → Project Structure → Artifacts
  2. 点击+ → Web Application: Archive → For 'xxx:war exploded'
  3. 在右侧Output Layout中,确认Available Elements下有:
    • WEB-INF/classes(来自src/main/java和src/main/resources)
    • WEB-INF/lib(包含hsqldb-2.7.2.jar等所有依赖)
    • WEB-INF/web.xml(必须存在)
  4. Build → Build Artifacts → Build生成target/cart.war

注意:若WEB-INF/classes为空,检查Project Structure → Modules → Sources是否将src/main/java标记为Sources Root。

6.2 独立Tomcat部署:三步验证是否真正可移植

将cart.war丢进$TOMCAT_HOME/webapps/后,按顺序验证:

验证步骤操作命令预期结果失败原因
1. WAR解压ls $TOMCAT_HOME/webapps/cart/应见WEB-INF/、static/、index.jsp等目录Tomcat未自动解压,检查conf/server.xml中<Host unpackWARs="true">
2. 日志检查tail -f $TOMCAT_HOME/logs/catalina.out启动末尾出现INFO: Server startup in [xxx] ms,无ClassNotFoundExceptionWEB-INF/lib缺jar包,或db.properties路径错误
3. 接口探活curl -I http://localhost:8080/cart/login返回HTTP/1.1 200 OK或302 Foundweb.xml中<welcome-file-list>未配置login.jsp,或<servlet-mapping>失效

6.3 生产级加固:三个必须做的最小化安全补丁

即使简易项目,也应规避低级风险:

  1. SQL注入防护:ProductDao.findById()必须用PreparedStatement,禁用字符串拼接SQL

    // ✅ 正确 String sql = "SELECT * FROM product WHERE id = ?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setInt(1, id); // ❌ 错误(绝对禁止) String sql = "SELECT * FROM product WHERE id = " + id; // 直接拼接
  2. 密码传输加密:login.jsp中密码输入框应启用HTTPS(开发环境可用http://,但需告知用户)

    <!-- login.jsp --> <form method="post" action="${pageContext.request.contextPath}/login"> <input type="text" name="username" required /> <input type="password" name="password" required /> <!-- type=password 隐藏明文 --> <input type="submit" value="登录" /> </form>
  3. XSS过滤:product_list.jsp中商品名称显示必须HTML转义

    <%-- ✅ 安全:用c:out自动转义 --%> <c:out value="${product.name}" /> <%-- ❌ 危险:直接EL表达式 --%> ${product.name}

从那以后我每次拿到一个JavaWeb源码包,第一件事不是跑起来,而是打开web.xml和db.properties,用grep -r "jdbc\|servlet\|url-pattern" .扫一遍关键配置;第二件事是mvn dependency:tree | grep hsql确认数据库驱动版本;第三件事是find . -name "*.jsp" -exec grep -l "request\.getParameter" {} \;检查所有JSP是否做了输入校验。这三步走完,80%的“跑不通”问题当场定位。希望帮到你。

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

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

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

立即咨询