☰
JSP零食商城源码实战:JavaBean+MySQL从建表到下单事务
2026/10/9 11:27:01 网站建设 项目流程

简介:这份资源是面向高校计算机相关专业学生与Java Web初学者的一套完整项目实践包,围绕网上零食销售系统的设计与开发展开,采用Java、JavaBean与JSP技术栈,配合MySQL数据库实现商品展示、购物车、订单管理等典型电商功能,适合作为课程设计、毕业设计或自学练手参考。压缩包为zip格式,整体约633.38MB,内含项目全套源码与完整文档,源码经过测试校正后可稳定运行,文档部分则覆盖系统设计思路与实现说明,便于对照理解各模块的职责划分与调用关系。目前已有656人学习下载,说明该案例在同类选题中具有一定参考价值。读者可从中获得一套可直接运行的Web项目实例、数据库表结构设计参考以及JSP页面与JavaBean交互的实践范例,有助于快速搭建开发环境、梳理分层结构并积累调试经验,为后续独立完成类似系统打下基础。

1. 从一份 JSP 零食商城源码说起:JavaBean + MySQL 这套老架构为什么还值得动手

如果你手里正躺着一个网上零食销售系统.zip,解压后看到WEB-INF、jsp、src三层目录,别急着嫌它“老”。这套基于 Java + JavaBean + JSP + MySQL 的网上零食销售系统,恰恰是理解 JavaWeb 请求链路最省心的样本:浏览器发一个.jsp请求,容器编译成 Servlet,JavaBean 承载商品、订单、用户三类数据,JDBC 把 MySQL 里的零食库存读出来渲染成页面。它没有 Spring 的注解魔法,也没有 MyBatis 的动态 SQL,所有调用关系都摊在明面上,出问题能一行行跟。

适合谁?一是刚学完 Java 基础、想找一个完整案例把 JSP 内置对象、Session、JDBC 串起来的学生;二是需要快速交付一个课程设计或小型电商 Demo 的开发者;三是想拿它当“反面教材”,对比 Spring Boot + MyBatis 到底解决了什么痛点的人。这篇笔记不讲空泛的“系统概述”,而是按真实落地顺序,把环境、建表、JavaBean 设计、JSP 页面、下单事务和排错一条条拆开,让你能照着复现,也能看清这套架构的边界在哪。

2. 环境与数据库先落地:JDK、Tomcat、MySQL 的版本搭配和建表脚本

2.1 版本选型:为什么这套 JSP 项目别硬上 JDK 17

这套源码的典型特征是<%@ page language="java" %>加 JDBC 直连,没有模块化描述符,也没有jakarta.*命名空间。如果你用 JDK 17 配 Tomcat 10,第一个翻车点就是javax.servlet找不到——Tomcat 10 把包名迁到了jakarta.servlet,老 JSP 里的import="javax.servlet.*"全部报红。血泪经验是:JDK 8 + Tomcat 8.5/9.0 + MySQL 5.7/8.0是这套组合最稳的搭配。

组件推荐版本说明
JDK1.8兼容javax.servlet,编译无警告
Tomcat8.5 或 9.09.0 仍保留javax.servlet,别用 10
MySQL5.7.44 或 8.0.x8.0 需注意驱动包和时区
JDBC 驱动mysql-connector-java 5.1.x / 8.0.x与 MySQL 版本对应
IDEEclipse / IDEA需配置 Web 模块和 Artifact

MySQL 安装这块,Windows 上常见做法是下载 ZIP 版解压后手动初始化,比 installer 更可控。初始化命令和改密流程如下:

# 解压到 D:\tool\mysql-8.0.46-winx64 后,以管理员身份进入 bin 目录 mysqld --initialize-insecure --console # 注册服务 mysqld --install mysql --defaults-file="D:\tool\mysql-8.0.46-winx64\my.ini" # 启动 net start mysql # 首次无密码登录后改密 mysql -u root ALTER USER 'root'@'localhost' IDENTIFIED BY 'your_password';

--initialize-insecure生成空密码 root,方便首次进入;生产环境请改用--initialize并保存随机密码。my.ini里至少要写basedir、datadir、port=3306和character-set-server=utf8mb4,否则中文零食名会变问号。

2.2 建表:零食商城最少需要哪几张表

网上零食销售系统的核心实体就四个:用户、商品(零食)、订单、订单明细。分类表可选,但加上它页面导航更好做。下面是一份可直接执行的建表脚本,字段命名和常见 JSP 源码保持一致,方便你对号入座。

CREATE DATABASE snack_shop DEFAULT CHARACTER SET utf8mb4; USE snack_shop; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, phone VARCHAR(20), address VARCHAR(200), reg_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ); CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image VARCHAR(200), category_id INT, intro VARCHAR(500), FOREIGN KEY (category_id) REFERENCES t_category(id) ); CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, -- 0待付款 1已付款 2已发货 3已完成 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id) ); CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (product_id) REFERENCES t_product(id) );

price用DECIMAL(10,2)而不是FLOAT,是因为金额做加减时浮点会丢精度,订单总额对不上账就是这里埋的雷。status用TINYINT存状态码,比字符串省空间也便于WHERE status=0查询。外键约束在 Demo 里能帮你早发现脏数据,但真实高并发下单场景常会去掉外键改由应用层保证,这点后面避坑章会展开。

3. JavaBean 与 DAO 分层:把零食数据从 MySQL 搬到 JSP 页面

3.1 实体 JavaBean 的写法:无参构造和 getter/setter 一个都不能少

JavaBean 不是随便一个 Java 类,它要满足三个硬条件:public 无参构造、属性私有、提供 getter/setter。JSP 里用<jsp:useBean>和<jsp:setProperty>时,容器靠反射调用无参构造和 setter,缺一个就抛InstantiationException或属性注入失败。以零食商品为例:

package com.snack.entity; public class Product { private int id; private String name; private double price; private int stock; private String image; private int categoryId; private String intro; public Product() {} // 无参构造,反射实例化必需 public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public double getPrice() { return price; } public void setPrice(double price) { this.price = price; } public int getStock() { return stock; } public void setStock(int stock) { this.stock = stock; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public int getCategoryId() { return categoryId; } public void setCategoryId(int categoryId) { this.categoryId = categoryId; } public String getIntro() { return intro; } public void setIntro(String intro) { this.intro = intro; } }

注意price在实体里用double方便 JSP 表达式直接输出,但入库时 DAO 层要转成BigDecimal或让 SQL 的DECIMAL兜底。如果你在 JSP 里写${product.price},EL 表达式会自动调getPrice(),这就是 JavaBean 规范带来的便利。

3.2 DAO 层 JDBC 工具类:连接、查询、关闭的标准姿势

JSP 页面里直接写 JDBC 是新手最容易犯的错,代码重复且连接泄漏。正确做法是抽一个BaseDao或DBUtil,把DriverManager.getConnection和close封装起来。下面是一个最小可用的工具类:

package com.snack.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/snack_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PWD = "your_password"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); // 8.0 驱动 } catch (ClassNotFoundException e) { throw new RuntimeException("驱动加载失败", e); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(Connection c, Statement s, ResultSet r) { try { if (r != null) r.close(); } catch (SQLException ignored) {} try { if (s != null) s.close(); } catch (SQLException ignored) {} try { if (c != null) c.close(); } catch (SQLException ignored) {} } }

serverTimezone=Asia/Shanghai是 MySQL 8.0 的必填项,不写会报The server time zone value is unrecognized。Class.forName在 8.0 驱动里其实可以省略,但显式加载能让你在驱动缺失时第一时间看到异常,而不是等到getConnection才报错。查询零食列表的 DAO 方法:

public List<Product> findAll() { List<Product> list = new ArrayList<>(); String sql = "SELECT id,name,price,stock,image,category_id,intro FROM t_product ORDER BY id DESC"; try (Connection c = DBUtil.getConn(); PreparedStatement ps = c.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.getDouble("price")); p.setStock(rs.getInt("stock")); p.setImage(rs.getString("image")); p.setCategoryId(rs.getInt("category_id")); p.setIntro(rs.getString("intro")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }

用try-with-resources自动关闭连接,比手动finally更不容易漏。ORDER BY id DESC让最新上架的零食排前面,符合商城习惯。这里用PreparedStatement而非Statement,既防 SQL 注入,又能在参数多时保持可读。

3.3 JSP 页面渲染:用 JSTL 循环输出零食列表

拿到List<Product>后,在 JSP 里用 JSTL 的<c:forEach>渲染,比脚本片段<% %>干净得多。前提是引入 JSTL 的 jar 包并在页面顶部声明标签库:

<%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <body> <h2>零食列表</h2> <table border="1"> <tr><th>名称</th><th>价格</th><th>库存</th><th>操作</th></tr> <c:forEach items="${productList}" var="p"> <tr> <td>${p.name}</td> <td>${p.price}</td> <td>${p.stock}</td> <td><a href="cartAdd.jsp?id=${p.id}">加入购物车</a></td> </tr> </c:forEach> </table> </body> </html>

${productList}是 request 或 session 域里的属性名,Servlet 里用request.setAttribute("productList", list)放进去。<c:forEach>的var="p"就是循环变量,EL 里${p.name}自动调 getter。如果你看到页面输出空白,先检查标签库 URI 是否写对、JSTL jar 是否放进WEB-INF/lib,这两个是最高频的“页面不报错但没数据”原因。

4. 下单与库存扣减:JSP 表单提交到事务提交的完整链路

4.1 购物车用 Session 还是数据库

网上零食销售系统的购物车有两种常见实现:Session 内存购物车和数据库购物车表。Session 方案简单,适合 Demo,用户关浏览器就清空;数据库方案能持久化,但要多一张t_cart表并处理并发。我一般建议课程设计用 Session,理由是代码量少、演示直观。Session 里存一个Map<Integer, Integer>,key 是商品 id,value 是数量:

// 加入购物车 Map<Integer,Integer> cart = (Map<Integer,Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } int id = Integer.parseInt(request.getParameter("id")); cart.put(id, cart.getOrDefault(id, 0) + 1); response.sendRedirect("cart.jsp");

getOrDefault让首次加入和重复加入走同一行逻辑,避免NullPointerException。sendRedirect而不是forward,是为了防止用户刷新页面重复提交。

4.2 下单事务:库存扣减和订单写入必须同生共死

下单是这套系统里唯一必须用事务的地方。流程是:遍历购物车 → 校验库存 → 扣库存 → 写t_order→ 写t_order_item。任何一步失败都要回滚,否则会出现“订单生成了但库存没扣”或“库存扣了订单没写”的脏数据。JDBC 手动事务的标准写法:

Connection c = null; try { c = DBUtil.getConn(); c.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 插入订单主表,拿到自增主键 String orderSql = "INSERT INTO t_order(user_id,total,status) VALUES(?,?,0)"; PreparedStatement psOrder = c.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); psOrder.setInt(1, userId); psOrder.setBigDecimal(2, total); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); keys.next(); int orderId = keys.getInt(1); // 2. 逐条扣库存并写明细 String stockSql = "UPDATE t_product SET stock=stock-? WHERE id=? AND stock>=?"; String itemSql = "INSERT INTO t_order_item(order_id,product_id,price,quantity) VALUES(?,?,?,?)"; for (Map.Entry<Integer,Integer> e : cart.entrySet()) { PreparedStatement psStock = c.prepareStatement(stockSql); psStock.setInt(1, e.getValue()); psStock.setInt(2, e.getKey()); psStock.setInt(3, e.getValue()); if (psStock.executeUpdate() == 0) { throw new SQLException("库存不足,商品id=" + e.getKey()); } // ... 写 itemSql 略 } c.commit(); // 全部成功才提交 } catch (Exception ex) { if (c != null) try { c.rollback(); } catch (SQLException ignored) {} ex.printStackTrace(); } finally { if (c != null) try { c.setAutoCommit(true); c.close(); } catch (SQLException ignored) {} }

关键点在UPDATE ... WHERE id=? AND stock>=?:把库存校验和扣减合并成一条原子 SQL,靠executeUpdate()返回 0 判断库存不足。如果先SELECT再UPDATE,两个并发请求可能都查到库存够,然后一起扣成负数,这就是超卖的经典翻车现场。RETURN_GENERATED_KEYS拿自增订单 id,避免再查一次数据库。

4.3 订单状态流转与页面回显

订单状态用status字段控制:0 待付款、1 已付款、2 已发货、3 已完成。用户下单后跳到订单列表页,用不同颜色或文字区分状态。JSP 里可以用<c:choose>做条件渲染:

<c:choose> <c:when test="${o.status == 0}"><span style="color:red">待付款</span></c:when> <c:when test="${o.status == 1}"><span style="color:green">已付款</span></c:when> <c:when test="${o.status == 2}">已发货</c:when> <c:otherwise>已完成</c:otherwise> </c:choose>

状态码和文案的映射建议抽成一个工具方法或常量类,别散落在多个 JSP 里。改需求时你会感谢自己只改了一处。

5. 这套 JSP 零食商城的避坑清单:从乱码到连接池的 5 个真实故障

5.1 中文零食名变问号:三层编码要统一

现象:数据库里存进去的“薯片”变成“???”或乱码。原因通常是 MySQL 服务端字符集、连接 URL 字符集、JSP 页面pageEncoding三者不一致。解决:建库用utf8mb4,URL 加useUnicode=true&characterEncoding=utf8,JSP 顶部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,表单提交用 POST 并在 Servlet 里request.setCharacterEncoding("UTF-8")。四处对齐后乱码基本消失。

5.2 刷新页面重复下单

现象:用户下单后按 F5,订单列表多出一条重复订单。原因是下单请求用 forward 转发,浏览器地址栏还停在处理 URL,刷新就重发。解决:下单成功后用response.sendRedirect("orderList.jsp")重定向,让浏览器地址栏跳到结果页,刷新只会重载结果页而不会重发下单请求。这是 PRG 模式(Post-Redirect-Get)的最小实践。

5.3 连接用完不关,Tomcat 跑一会儿就卡死

现象:系统运行几分钟后所有请求变慢,日志出现Too many connections。原因是 JSP 或 DAO 里getConnection后没在finally关闭,连接池(或 MySQL 的max_connections)被耗尽。解决:统一用try-with-resources,或把关闭逻辑收进DBUtil.close。更彻底的做法是引入连接池,但纯 JSP 项目里手写连接池容易出错,Demo 阶段用try-with-resources就够。

5.4 图片上传后页面不显示

现象:零食图片上传成功,但列表页<img src="${p.image}">是裂图。原因通常是上传路径存的是服务器绝对路径,而 JSP 里按相对路径解析。解决:上传时把文件存到webapp/upload/下,数据库只存upload/xxx.jpg这样的相对路径,页面用${pageContext.request.contextPath}/upload/xxx.jpg拼接。别把文件存到WEB-INF里,那里浏览器访问不到。

5.5 金额用 double 计算,订单总额差几分钱

现象:购物车总价和订单表total对不上,差 0.01。原因是double累加有精度误差。解决:计算金额时用BigDecimal,new BigDecimal("3.5").multiply(new BigDecimal(qty)),入库时setBigDecimal。实体类里展示可以用double,但所有参与运算的金额都走BigDecimal,这是财务相关代码的铁律。

6. 从 JSP 到 Spring Boot:这套源码怎么当跳板用

如果你已经把这套网上零食销售系统跑通,下一步最值得做的不是继续加功能,而是拿它和 Spring Boot + MyBatis 版本做对照,看清每一层被什么替代了。JSP 里的<c:forEach>对应 Thymeleaf 的th:each;手写DBUtil对应 HikariCP 连接池加application.yml配置;手动setAutoCommit(false)对应@Transactional注解;web.xml里的 Servlet 映射对应@Controller和@RequestMapping。把这张对照表列出来,你对 JavaWeb 演进的理解会比背面试题扎实得多。

JSP 方案Spring Boot 方案解决的问题
DBUtil 手写连接HikariCP + 配置连接复用、泄漏防护
手动事务@Transactional声明式、少写 try-catch
JSTL 标签Thymeleaf模板与逻辑分离更彻底
web.xml 映射注解映射零配置、易重构
Session 购物车Redis / 数据库分布式下会话共享

验证迁移是否成功,有个具体技巧:把原 JSP 项目的建表脚本原样导入新库,用 Spring Boot 写一个/product/list接口,返回 JSON,和 JSP 页面渲染的数据逐条比对。数量、价格、库存三项一致,说明数据层迁移没问题;再压测下单接口,看@Transactional回滚是否生效——故意传一个库存不足的商品 id,观察订单表是否多出脏数据。

我自己的习惯是:每接手一个老 JSP 项目,先不急着改代码,而是把它的表结构和请求链路画在一张纸上,标出哪些是业务逻辑、哪些是框架该干的活。画完再决定是重构还是重写,比上来就动手省一半时间。这套零食商城源码的价值不在它本身多完善,而在于它把 JavaWeb 的每一颗螺丝都露在外面,让你拧过一遍之后,再看任何框架都知道它替你拧了哪几颗。希望帮到你。

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

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

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

立即咨询