JSP零食商城源码实战:从环境搭建到连接池优化的完整指南 简介这份资源是面向Java Web初学者与课程设计者的网上零食销售系统完整项目包基于Java、JavaBean与JSP技术栈开发数据库采用MySQL适合作为毕业设计、课程大作业或Java Web入门练手项目。压缩包内包含项目全套源码与配套完整文档源码经过测试校正可保证百分百成功运行方便读者直接部署学习。资源包整体约633.38MB内容涵盖前端页面、后端业务逻辑、数据库脚本及说明文档等结构清晰便于按模块查阅与二次开发。目前已有655人学习下载说明其在同类项目中具有一定参考价值。通过研读源码与文档读者可以掌握JSP页面与JavaBean的协作方式、MySQL数据库表结构设计、商品展示与订单处理等核心业务逻辑并借鉴其分层组织思路快速搭建自己的Java Web项目减少从零摸索的时间成本。1. 从一份 JSP 零食商城源码说起这套 JavaBean MySQL 架构到底能跑多远很多人第一次接触「网上零食销售系统」这类题目是在课程设计或者毕设选题阶段。打开搜索引擎输入 java、jsp、mysql 这几个词跳出来的结果铺天盖地但真正能跑起来的没几个。我拿到手的这类项目典型结构是JSP 负责页面渲染JavaBean 封装商品、订单、用户这些实体Servlet 或者 JSP 页面里直接写业务逻辑MySQL 存数据。听起来简单但它几乎覆盖了 JavaWeb 开发的所有基础知识点——JDBC 连接、会话管理、请求转发与重定向、表单校验、分页查询、事务处理。这套架构放在今天看确实不算先进前后端不分离JSP 里嵌 Java 代码页面和逻辑搅在一起。但它的价值在于你能在一台普通笔记本上用 IDEA 加 MySQL 就把整个系统跑通不需要 Docker、不需要微服务、不需要前端框架。对于想理解「一个 Web 请求从浏览器到数据库再回来」完整链路的开发者来说这种传统 JSP 项目的教学意义远大于它的工程意义。这篇文章不讲空泛的架构演进只讲一件事拿到这份源码之后怎么让它在你机器上跑起来怎么改怎么避开那些让人抓狂的坑。2. 环境搭建与项目导入从零把 JSP 零食商城跑起来2.1 JDK、Tomcat、MySQL 的版本选择与安装顺序先明确一个原则这套技术栈的版本兼容性比你想的敏感。JDK 用 8 或者 11 都行但别上 17JSP 里有些老写法在模块化之后会报错。Tomcat 选 8.5 或者 9.0Tomcat 10 把 javax.servlet 改成了 jakarta.servlet这份源码大概率没做适配导进去就是一片红。MySQL 用 5.7 或者 8.0 都可以但 8.0 的 JDBC 驱动类名和连接 URL 参数跟 5.x 有区别后面会细说。安装顺序建议是JDK → MySQL → Tomcat → IDEA。JDK 装完配好 JAVA_HOME 和 PATH命令行敲java -version能出结果就行。MySQL 安装时记住 root 密码字符集选 utf8mb4排序规则选 utf8mb4_general_ci。Tomcat 解压到没有中文和空格的路径下比如D:\dev\tomcat9。IDEA 社区版够用但如果你要用 Tomcat 集成调试建议用 Ultimate 版社区版配置外部 Tomcat 会麻烦一些。提示安装路径里不要出现中文、空格、特殊符号。我见过太多因为路径里有空格导致 Tomcat 启动报 ClassNotFound 的案例排查半天以为是代码问题。2.2 用 IDEA 导入项目并配置 Tomcat 运行环境假设你已经把 zip 解压到了一个目录结构大概是src、WebContent或者web、lib、sql这几个文件夹。打开 IDEA选择 Open指向项目根目录。如果是 Eclipse 风格的项目IDEA 会提示你导入模块选 Eclipse 然后一路 Next。接下来配置 Tomcat。打开 Run → Edit Configurations点左上角加号选 Tomcat Server → Local。在 Server 标签页里Application server 指向你解压的 Tomcat 目录。切到 Deployment 标签页点加号选 Artifact把项目的 war exploded 加进去Application context 设成/snack或者/。# 检查 Tomcat 是否独立启动正常 cd D:\dev\tomcat9\bin startup.bat # 浏览器访问 http://localhost:8080 看到 Tomcat 欢迎页即正常 # 关掉再在 IDEA 里配避免端口冲突 shutdown.bat这段命令的意思是先验证 Tomcat 本身没问题再去 IDEA 里配。很多人跳过这步结果 IDEA 启动报端口占用或者 404分不清是 Tomcat 的问题还是项目的问题。参数上注意Tomcat 默认端口 8080如果被占了改conf/server.xml里的 Connector port。2.3 导入 SQL 脚本与修改数据库连接配置项目里一般有个sql文件夹里面是.sql文件。用 Navicat 或者 MySQL Workbench 连上本地 MySQL新建一个数据库叫snack_db名字看脚本里的 CREATE DATABASE 语句字符集 utf8mb4然后运行 SQL 脚本导入表结构和初始数据。-- 先建库字符集必须和脚本一致 CREATE DATABASE snack_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE snack_db; -- 再执行项目里的建表语句和数据插入语句 SOURCE D:/project/sql/snack_db.sql;导入完成后找到项目里数据库连接的工具类通常叫DBUtil.java或者BaseDao.java里面会有类似jdbc:mysql://localhost:3306/snack_db?useSSLfalseserverTimezoneAsia/Shanghai的 URL。把用户名密码改成你本地的。如果是 MySQL 8.0驱动类名是com.mysql.cj.jdbc.DriverURL 里必须加serverTimezone否则报时区错误。MySQL 5.7 用com.mysql.jdbc.Driver就行。注意lib文件夹里的mysql-connector-java-x.x.x.jar版本要和你的 MySQL 版本匹配。5.1.x 的驱动连 MySQL 8.0 会报Unknown system variable query_cache_size换成 8.0.x 的驱动即可。3. JavaBean 与 JSP 的分层逻辑哪些代码该放哪哪些是历史包袱3.1 实体 Bean、DAO、Service 的职责边界打开源码你会看到com.snack.entity下面有User.java、Product.java、Order.java这些类属性跟数据库字段一一对应有 getter/setter这就是实体 Bean。再往下看com.snack.dao里面是UserDao.java、ProductDao.java方法名通常是findAll()、findById()、insert()、update()、delete()里面写 JDBC 代码。再往上com.snack.service或者直接没有 service 层业务逻辑写在 Servlet 或者 JSP 里。这套分层在当年是标准做法但现在看有几个问题。第一DAO 里每次方法调用都DriverManager.getConnection()没有连接池并发一上来就崩。第二实体 Bean 和数据库表强耦合表结构一改Bean 和 DAO 全要动。第三事务控制基本没有下单扣库存和生成订单是两条独立 SQL中间出错就数据不一致。但话说回来你要做的是让它跑起来不是重构它。我的建议是先按原样跑通理解每一层在干什么然后再考虑要不要引入连接池或者把业务逻辑抽到 Service 层。如果你只是想交个课程设计原样跑通就够了。如果你想拿它练手可以从加一个 Druid 连接池开始。3.2 JSP 页面里嵌 Java 代码的典型写法与替代方案随便打开一个product_list.jsp你大概率会看到这种代码% page importcom.snack.dao.ProductDao % % page importcom.snack.entity.Product % % page importjava.util.List % % ProductDao dao new ProductDao(); ListProduct list dao.findAll(); for (Product p : list) { % div classproduct h3% p.getName() %/h3 p价格% p.getPrice() %/p /div % } %这段代码能跑但问题很明显页面里直接 new DAO数据库连接在 JSP 渲染时建立页面加载速度取决于数据库查询速度。而且% %脚本片段多了之后页面根本没法维护。常见的改进做法是把查询逻辑挪到 Servlet 里Servlet 通过request.setAttribute(productList, list)把数据传给 JSPJSP 里只用 JSTL 的c:forEach遍历。这样 JSP 里就没有 Java 代码了职责清晰。但改造成本在于你需要为每个页面写一个对应的 Servlet并且在web.xml或者注解里配好路由。// ProductListServlet.java 的核心逻辑 WebServlet(/product/list) public class ProductListServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ProductDao dao new ProductDao(); ListProduct list dao.findAll(); req.setAttribute(productList, list); req.getRequestDispatcher(/product_list.jsp).forward(req, resp); } }参数说明WebServlet(/product/list)定义了访问路径forward是服务器内部跳转浏览器地址栏不变。这样做的好处是 JSP 只负责展示逻辑在 Servlet 里调试的时候断点打在 Servlet 里就行。3.3 用 JSTL 和 EL 表达式替换脚本片段的实操步骤如果你决定改造第一步是在pom.xml或者lib里加 JSTL 的 jar 包jstl.jar和standard.jar或者jakarta.servlet.jsp.jstl对应 Tomcat 10。然后在 JSP 页面顶部加 taglib 声明% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %接着把原来的脚本片段改成c:forEach items${productList} varp div classproduct h3${p.name}/h3 p价格${p.price}/p /div /c:forEach${productList}就是 Servlet 里 setAttribute 传过来的那个 Listvarp是循环变量${p.name}会自动调用getName()。这套写法比脚本片段干净得多而且 JSTL 自带条件判断c:if和c:choose能覆盖大部分页面逻辑。提示JSTL 的 uri 在 Tomcat 9 和 Tomcat 10 里不一样。Tomcat 9 用http://java.sun.com/jsp/jstl/coreTomcat 10 用jakarta.tags.core。导错了就是一片 500 错误。4. 数据库设计与核心功能实现商品、订单、用户三张表的联动4.1 商品表、订单表、订单明细表的字段设计与索引这套系统的数据库一般至少有三张核心表user、product、orders或者order但 order 是 SQL 关键字建议加 s。订单明细表order_item记录每笔订单买了哪些商品、数量、单价。CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL DEFAULT 0.00, stock INT NOT NULL DEFAULT 0, category_id INT, image VARCHAR(255), description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_name (name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, order_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待付款 1已付款 2已发货 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), INDEX idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL, INDEX idx_order (order_id), INDEX idx_product (product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得注意。price用DECIMAL(10,2)而不是FLOAT浮点数算钱会丢精度。order_no加唯一索引防止重复提交生成重复订单。status用 TINYINT 存状态码比 VARCHAR 省空间。索引方面product表的category_id和name加索引因为列表页经常按分类筛选或者按名称搜索。4.2 下单流程的 JDBC 事务控制从扣库存到生成订单下单是最容易出数据不一致的地方。用户点「提交订单」后台要做三件事扣商品库存、插入订单记录、插入订单明细。这三步必须在一个事务里任何一步失败都要回滚。public boolean createOrder(int userId, ListCartItem items) { Connection conn null; PreparedStatement ps1 null, ps2 null, ps3 null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 扣库存同时检查库存是否充足 String sql1 UPDATE product SET stock stock - ? WHERE id ? AND stock ?; ps1 conn.prepareStatement(sql1); for (CartItem item : items) { ps1.setInt(1, item.getQuantity()); ps1.setInt(2, item.getProductId()); ps1.setInt(3, item.getQuantity()); if (ps1.executeUpdate() 0) { conn.rollback(); return false; // 库存不足回滚 } } // 2. 插入订单主表 String orderNo generateOrderNo(); String sql2 INSERT INTO orders(user_id, order_no, total_amount, status) VALUES(?,?,?,0); ps2 conn.prepareStatement(sql2, Statement.RETURN_GENERATED_KEYS); ps2.setInt(1, userId); ps2.setString(2, orderNo); ps2.setBigDecimal(3, calculateTotal(items)); ps2.executeUpdate(); ResultSet rs ps2.getGeneratedKeys(); int orderId 0; if (rs.next()) orderId rs.getInt(1); // 3. 插入订单明细 String sql3 INSERT INTO order_item(order_id, product_id, quantity, price) VALUES(?,?,?,?); ps3 conn.prepareStatement(sql3); for (CartItem item : items) { ps3.setInt(1, orderId); ps3.setInt(2, item.getProductId()); ps3.setInt(3, item.getQuantity()); ps3.setBigDecimal(4, item.getPrice()); ps3.addBatch(); } ps3.executeBatch(); conn.commit(); return true; } catch (SQLException e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { // 关闭资源注意顺序ResultSet → Statement → Connection DBUtil.close(conn, ps3, null); DBUtil.close(null, ps2, null); DBUtil.close(null, ps1, null); } }关键点conn.setAutoCommit(false)开启事务conn.commit()提交conn.rollback()回滚。扣库存的 SQL 里AND stock ?是防止超卖的关键如果更新影响行数为 0说明库存不够直接回滚。Statement.RETURN_GENERATED_KEYS用来拿自增主键插入订单后需要这个 ID 去关联明细。注意DBUtil.close()的关闭顺序不能乱先关 ResultSet 再关 Statement 再关 Connection。如果用了连接池Connection 的 close 是归还而不是真正关闭但顺序依然要对。4.3 分页查询在 JSP 列表页的落地写法商品列表页数据多了必须分页。MySQL 用LIMIT offset, size实现。假设每页 10 条当前第 page 页offset 就是(page-1)*10。public ListProduct findByPage(int page, int size) { ListProduct list new ArrayList(); String sql SELECT * FROM product ORDER BY id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * size); ps.setInt(2, size); 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.setStock(rs.getInt(stock)); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; } public int countAll() { String sql SELECT COUNT(*) FROM product; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { if (rs.next()) return rs.getInt(1); } catch (SQLException e) { e.printStackTrace(); } return 0; }JSP 页面里接收page参数调用这两个方法然后渲染分页条。分页条的逻辑是总页数 Math.ceil(count / size)当前页高亮上一页/下一页根据边界禁用。% int page 1; String pageStr request.getParameter(page); if (pageStr ! null !pageStr.isEmpty()) page Integer.parseInt(pageStr); int size 10; ProductDao dao new ProductDao(); ListProduct list dao.findByPage(page, size); int total dao.countAll(); int totalPages (int) Math.ceil((double) total / size); request.setAttribute(list, list); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); %参数说明page从 URL 参数取默认 1。size是每页条数可以做成配置项。totalPages用于渲染分页按钮。注意Integer.parseInt要做异常处理用户手动改 URL 传个非数字进来会抛 NumberFormatException。5. 避坑与排查这套 JSP 零食系统最容易翻车的五个地方5.1 中文乱码从数据库到 JSP 页面的全链路排查现象商品名称在数据库里看是正常的但页面上显示问号或者乱码。原因乱码可能出现在三个环节——数据库字符集、JDBC 连接字符集、JSP 页面编码。任何一环不是 utf8中文就会出问题。解决数据库建库时指定utf8mb4JDBC URL 加useUnicodetruecharacterEncodingutf8JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %Servlet 里在doGet和doPost第一行加request.setCharacterEncoding(UTF-8)和response.setContentType(text/html;charsetUTF-8)。四个地方都对了乱码才会消失。5.2 ClassNotFoundException: com.mysql.jdbc.Driver 的三种触发场景现象启动项目访问页面控制台报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。原因第一种jar 包没放到WEB-INF/lib下或者放了但 IDEA 没识别第二种MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver代码里写的是旧类名第三种Tomcat 的lib目录和项目的lib目录都有驱动版本冲突。解决确认WEB-INF/lib下有mysql-connector-java的 jarMySQL 8.0 把驱动类名改成com.mysql.cj.jdbc.Driver删掉 Tomcatlib下多余的驱动只保留项目里的一份。在 IDEA 的 Project Structure → Artifacts 里检查 lib 是否被正确打包。5.3 Tomcat 启动报 404路径、端口与 war 包部署的常见误区现象Tomcat 启动没报错但浏览器访问http://localhost:8080/snack/显示 404。原因Application context 配错了或者项目没有正确部署成 war exploded或者web.xml里 welcome-file 没配。解决在 IDEA 的 Run Configuration → Deployment 里确认 Application context 是/snack检查web.xml里welcome-file-list是否包含index.jsp或者login.jsp如果是手动把 war 包丢到 Tomcatwebapps下确认 war 包解压后的目录名和访问路径一致。端口被占的话改server.xml里的 Connector port。5.4 数据库连接池未关闭导致的 Too many connections现象系统跑一段时间后访问任何页面都报Too many connectionsMySQL 拒绝新连接。原因DAO 里的 Connection 没有在 finally 里关闭每次请求都新建连接连接数很快达到 MySQL 的max_connections上限默认 151。解决每个 DAO 方法里Connection、PreparedStatement、ResultSet 都要在 finally 里关闭。更好的做法是引入 Druid 或者 HikariCP 连接池把连接的创建和回收交给池管理。如果暂时不想改代码先把 MySQL 的max_connections调大但这只是缓兵之计。// 正确的关闭姿势 Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); rs ps.executeQuery(); // 处理结果 } catch (SQLException e) { e.printStackTrace(); } finally { try { if (rs ! null) rs.close(); } catch (SQLException e) {} try { if (ps ! null) ps.close(); } catch (SQLException e) {} try { if (conn ! null) conn.close(); } catch (SQLException e) {} }5.5 JSP 页面修改后不生效Tomcat 缓存与 IDEA 热部署配置现象改了 JSP 文件刷新浏览器还是旧内容。原因Tomcat 默认会缓存编译后的 JSP ServletIDEA 的热部署有时候没触发重新编译。解决在 Tomcat 的conf/server.xml里找到 Host 标签加上unpackWARstrue autoDeploytrue在 IDEA 的 Run Configuration → Server 里On Update action 选Update classes and resourcesOn frame deactivation 选Update classes and resources。如果还不行直接点 IDEA 的 Build → Rebuild Project然后重启 Tomcat。最粗暴的办法是删掉 Tomcatwork目录下的缓存文件。6. 从能跑到好用给这套 JSP 零食系统加一层连接池与统一编码把系统跑起来只是第一步。如果你想让它在小规模使用下稳定一点我建议做两件事加连接池、加编码过滤器。这两个改动不大但效果立竿见影。连接池用 Druid 或者 HikariCP 都行。以 Druid 为例在lib下加druid-1.2.x.jar然后写一个DruidUtil替换原来的DBUtilpublic class DruidUtil { private static DataSource dataSource; static { try { Properties props new Properties(); props.load(DruidUtil.class.getClassLoader() .getResourceAsStream(druid.properties)); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) {} try { if (stmt ! null) stmt.close(); } catch (SQLException e) {} try { if (conn ! null) conn.close(); } catch (SQLException e) {} } }druid.properties放在src目录下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/snack_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot passwordyour_password initialSize5 maxActive20 maxWait3000参数说明initialSize是启动时创建的连接数maxActive是最大连接数maxWait是获取连接的超时时间毫秒。这三个值根据你的实际并发调整个人项目 5 和 20 足够了。编码过滤器解决的是全站中文问题。写一个CharacterEncodingFilter实现javax.servlet.Filter在doFilter里设置请求和响应的编码然后在web.xml里配成对所有路径生效filter filter-nameencodingFilter/filter-name filter-classcom.snack.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping这样就不用在每个 Servlet 里重复写setCharacterEncoding了。过滤器是 JavaWeb 里非常实用的一个组件除了编码还能做登录校验、日志记录、权限控制。最后说一个验证方法改完之后用ab或者jmeter压一下商品列表页看连接池的活跃连接数和等待队列。如果maxActive经常被打满说明要么连接泄漏了要么确实需要调大。我自己的习惯是每次改完数据库相关的代码都会在本地跑一轮下单流程从加购物车到生成订单到查看订单详情走通一遍再提交。这套 JSP 零食系统虽然老但把连接池和过滤器加上之后作为一个练手项目或者课程设计已经足够体面了。希望帮到你。本文还有配套的精品资源点击获取