水果超市JavaWeb项目实战:从Servlet+JSP到购物车与订单事务
发布时间:2026/9/17 1:49:26 锦皓数字建站

简介水果超市Javaweb是一份基于Servlet和JSP技术的Java Web课程设计/实践项目面向初学Java Web或需要完成类似作业的学习者。项目围绕水果商品信息管理实现了商品的添加、删除、修改、查询以及用户注册登录等基础功能清晰呈现了Tomcat运行环境下HTTP请求处理、Servlet业务逻辑与数据库交互的完整链路。压缩包共85个文件以Java源码、JSP页面和编译后的class文件为主同时包含web.xml等配置文件、样式脚本、图片素材整体仅2.1MB目录保留IDEA工程结构与编译输出便于导入环境直接阅读。目前已有1288人学习浏览。透过该实例可以理解src、web、WEB-INF等模块的分工掌握商品管理各页面、注册登录和密码修改的实现方式以及使用Navicat管理MySQL数据表的方法适合作为入门实践与课程设计参考。1. 水果超市 JavaWeb 项目到底在练什么把水果超市四个字换成任意商品类目你会发现这一类 JavaWeb 完整案例在课设、毕设和简历项目里出现频率高得惊人。原因不是业务有趣而是它用最小的领域覆盖了 JavaWeb 里最容易被忽略的三个硬问题用户状态放在哪、下单时库存怎么扣、请求从 URL 到数据库的路径是否清晰。水果超市的规格足够简单——商品、分类、购物车、订单每一块都能被肉眼验证对错但每一块都能往深处挖。这篇文章按照做这类项目最通用的从业方案把技术选型、工程骨架、建表 SQL、购物车与下单、部署排错完整过一遍。适合刚学完 Servlet/JSP、正发愁第一个 JavaWeb 程序怎么落地的读者也适合拿着旧课设想改成简历项目的熟手对照查漏。先立一个判断这类项目的分水岭从来不在页面样式而在 Session 状态管理和订单事务的一致性。2. 技术选型与工程骨架JavaWeb 配置先从 ServletJSP 起步2.1 为什么先 ServletJSP不直接上 Spring Boot现在的新手往往被 Spring Boot 的零配置惯坏建一个水果超市项目最先遇到的问题不是功能而是不知道 HTTP 请求从 Tomcat 端口进来之后到底经过了谁。Spring Boot 把 DispatcherServlet、内嵌 Tomcat、依赖管理全部打包开发者看到的只有 Controller 注解Servlet 容器被彻底黑盒化。ServletJSP 的价值在于把请求生命周期摊开request 进入容器按 url-pattern 匹配到 ServletServlet 调 ServiceService 调 DAODAO 访问 MySQL最后把数据塞进 request 域forward 到 JSP 渲染。这条链路每一步都有显式代码出了 404 或 500 能顺着链路直接定位。选择建议分两种。如果只是校内课设、目标是跑通功能ServletJSP JDBC 足够代码量可控答辩时也能把每个类的作用讲清楚。如果简历上打算写基于 SSM 的水果超市那就干脆一步到位上 MyBatis Spring但要把 Spring 容器、Mapper 映射、声明式事务全部补齐工作量至少翻一倍。我一般建议中间路线视图和控制层用 ServletJSP 保持直观数据访问层直接用 MyBatis这样既能看到 HTTP 全流程又让简历上的技术标签更有分量。2.2 怎么创建 JavaWeb 工程Maven 骨架与 web.xml 必配项不管用 IDEA 还是 Eclipse第一步都建议建 Maven 工程而不是手工建 Web 工程。Maven 能把依赖版本、打包、插件统一管住避免换台电脑就跑不起来的经典尴尬。下面是最小可用的pom.xml按 Java 8 Tomcat 8/9 的环境配置project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdfruit-market/artifactId version1.0.0/version packagingwar/packaging properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target project.build.sourceEncodingUTF-8/project.build.sourceEncoding /properties dependencies !-- Servlet API编译期需要运行时由 Tomcat 提供 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTLJSP 里做循环和格式化 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- HikariCP 连接池 -- dependency groupIdcom.zaxxer/groupId artifactIdHikariCP/artifactId version4.0.3/version /dependency /dependencies build finalNamefruit-market/finalName /build /project几个参数必须说清楚packaging必须是warJavaWeb 应用最终要打成 war 包丢进 Tomcat 的 webapps 目录servlet-api的 scope 是provided因为 Tomcat 自带了这套 API打成 war 时不需要重复打包否则会和容器冲突JSTL 没有 providedJSP 页面运行时要靠它解析c:forEach标签。目录结构按三层架构拆这是 JavaWeb 项目完整案例里约定俗成的分包方式目录职责controllerServlet 类接收请求、调服务、转发页面service业务逻辑事务边界在这里控制daoJDBC 或 MyBatis 的数据访问entity与表结构对应的实体类util连接池、日期工具类webapp/jspJSP 视图放在 WEB-INF 下更安全对应的web.xml是最简配置只声明欢迎页和 Session 超时。如果用注解方式写 Servlet这里甚至可以不注册任何 Servletweb-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version4.0 welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list session-config session-timeout30/session-timeout /session-config /web-appsession-timeout单位是分钟默认 30。水果超市这类带购物车的项目这个值直接决定用户把商品丢进购物车后多久失效后面第 5 章会专门讲验证方法。2.3 第一个 JavaWeb 程序商品列表 Servlet 的请求路径从怎么创建 JavaWeb到跑通第一个程序中间只隔一个 Servlet。下面这个FruitListServlet是水果超市的入口演示了注解映射、参数读取、调用 DAO、请求转发这四件事WebServlet(/fruit/list) public class FruitListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 处理请求参数页码缺省为第 1 页 String pageStr req.getParameter(page); int page 1; if (pageStr ! null !pageStr.trim().isEmpty()) { page Integer.parseInt(pageStr); } // 2. 调用数据访问层pageSize 固定为 8 FruitDao dao new FruitDao(); PageBeanFruit pageBean dao.page(page, 8); // 3. 把结果放进 request 域forward 到 JSP req.setAttribute(pageBean, pageBean); req.getRequestDispatcher(/jsp/fruit_list.jsp).forward(req, resp); } }WebServlet(/fruit/list)是 Servlet 3.0 起的注解式映射等价于在 web.xml 里写servlet-mappingURL 里的/fruit/list不能包含webapp前缀只写应用内的虚拟路径。forward是服务端跳转浏览器地址栏不变request 域里的属性在 JSP 里能直接用 EL 取出如果用了sendRedirectrequest 域会丢必须靠 Session 或 URL 参数传递数据这是初学最容易混淆的地方。3. 数据库设计与数据访问层水果超市的核心表结构3.1 建表 SQL商品、分类、用户、订单四张主表水果超市的数据量不会大但表结构必须按业务边界拆干净。四张主表分别是分类、商品、用户、订单订单明细作为第五张表。下面是可以直接执行的建表 SQL我按课设通用标准写字段注释已经标好CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20) NOT NULL COMMENT 分类名如热带水果 ) COMMENT 商品分类表; CREATE TABLE fruit ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 所属分类逻辑外键, name VARCHAR(50) NOT NULL COMMENT 水果名称, price DECIMAL(10,2) NOT NULL COMMENT 单价单位元, stock INT NOT NULL DEFAULT 0 COMMENT 库存, image VARCHAR(255) COMMENT 图片相对路径, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) COMMENT 水果商品表; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 建议存 MD5/BCrypt 哈希 ) COMMENT 用户表; CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已发货 3已完成 4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) COMMENT 订单主表; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, fruit_id INT NOT NULL, price DECIMAL(10,2) NOT NULL COMMENT 下单时的单价快照, quantity INT NOT NULL COMMENT 购买数量 ) COMMENT 订单明细表;两个细节值得注意。第一价格必须用DECIMAL(10,2)不能用FLOAT或DOUBLE二进制浮点数在累计金额时会产生 0.1 这种误差涉及钱的一律定点数。第二order是 MySQL 的保留字直接建表会报语法错误常见做法是表名写成t_order这是个能让人排查半小时的坑。各表关系用一句话概括fruit.category_id指向categoryt_order.user_id指向userorder_item通过order_id挂在订单下。这里我刻意没有加物理外键约束只保留逻辑外键和索引原因是课设里经常要手工改数据、清表重置外键会拖累删除顺序而且后续做分库分表时物理外键基本都要去掉。3.2 DAO 层用 JDBC 还是 MyBatis常见做法是课设用 JDBC HikariCP简历项目用 MyBatis。JDBC 能逼着你写出PreparedStatement、ResultSet的完整代码理解连接生命周期MyBatis 则把 SQL 拆到 XML 里适合业务变多以后的维护。下面用 JDBC 实现分页查询这是水果超市列表页的核心方法public PageBeanFruit page(int pageNo, int pageSize) { String countSql SELECT COUNT(*) FROM fruit WHERE status 1; String listSql SELECT id, category_id, name, price, stock, image FROM fruit WHERE status 1 ORDER BY id DESC LIMIT ?, ?; try (Connection conn dataSource.getConnection(); PreparedStatement ps conn.prepareStatement(listSql)) { // LIMIT 的 offset 从 0 开始第 1 页对应 0 ps.setInt(1, (pageNo - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs ps.executeQuery()) { // 逐行封装成 Fruit 对象加入 List } } catch (SQLException e) { throw new RuntimeException(查询商品分页失败, e); } }LIMIT ?, ?的两个参数第一个是偏移量(pageNo-1) * pageSize第二个是每页条数。try-with-resources会自动关闭Connection、PreparedStatement、ResultSet顺序是先关 ResultSet 再关 Statement 最后关 Connection这比手写 finally 块更不容易泄漏。连接必须从dataSource.getConnection()拿如果直接用DriverManager.getConnection()每次请求都新建 TCP 连接并发一上来 Tomcat 线程池立刻被打满。3.3 商品列表页渲染与分页条的 JSTL 写法DAO 返回PageBeanFruit后Servlet 把它放入 request 域JSP 页面用 JSTL 循环输出。注意 JSP 页面顶部必须有taglib指令否则c:forEach不会被解析% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classfruit-grid c:forEach varf items${pageBean.list} div classcard img src${f.image} alt${f.name} / p classname${f.name}/p p classprice${f.price}/p p classstock库存${f.stock}/p a hrefc:url value/cart/add c:param namefruitId value${f.id}/ c:param nameqty value1/ /c:url加入购物车/a /div /c:forEach /divc:url标签会自动在 URL 前拼接应用上下文路径避免部署后 context path 变化导致链接 404这是 JavaWeb 笔记里常被忽略的细节。EL 表达式${f.price}对应实体类的getPrice()方法JSTL 会自动调用。如果商品被下架status为 0查询条件已经把它过滤掉前端不用再判断。4. 购物车与下单Session 状态与事务一致性怎么落地4.1 购物车的三种存储方式各自边界在哪购物车是水果超市里最能体现 JavaWeb 状态管理能力的模块。三种存储方式各有适用场景存储方式数据位置生命周期适用场景Session服务器内存随会话失效未登录用户也能加购课设首选Cookie浏览器本地可设长期有效记住购物车但容量有限、可被篡改数据库表MySQL永久多设备同步、需分析购物车转化率课设阶段用 Session 最合理。HttpSession由容器维护HttpSessionListener会在超时或手动invalidate()时触发失效清理开发者只需要把 Map 结构放进 Session。生产环境的购物车基本都落库因为 Session 存服务器内存集群部署时 Session 不一致问题会立刻暴露。我不建议在课设里用 Cookie 存购物车Cookie值不能超过 4KB水果种类一多就溢出而且水果 id 和数量明文暴露在浏览器端改包就能低价下单。4.2 加购 Servlet用 Map 结构管理购物车购物车的数据结构选MapInteger, Integerkey 是水果 idvalue 是数量。加购逻辑的核心是同商品数量累加Java 8 的merge方法可以一行完成WebServlet(/cart/add) public class CartAddServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { HttpSession session req.getSession(); int fruitId Integer.parseInt(req.getParameter(fruitId)); int qty Integer.parseInt(req.getParameter(qty)); // Session 中获取购物车没有则新建 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } // 同一种水果数量累加第一次加入时默认值 0 cart.merge(fruitId, qty, Integer::sum); resp.sendRedirect(req.getContextPath() /fruit/list); } }merge的三个参数key 是水果 idvalue 是本次加购数量第三个参数Integer::sum是冲突时合并函数——如果 key 已存在把旧值和现值相加。这段代码有一个必须处理的隐患Integer.parseInt遇到非数字参数会抛NumberFormatException直接导致 500。正常做法是包一层 try-catch非法参数返回 400 而不是让 Tomcat 打印异常页。4.3 下单扣库存用事务把订单和库存绑在一起购物车阶段不碰数据库到了提交订单就必须保证订单写入成功库存一定扣减任何一步失败全部回滚。水果超市最常见的并发问题是超卖两个用户同时买最后 1 斤苹果各自读到库存 1都执行扣减库存变成 -1。解决思路是两层保险SELECT ... FOR UPDATE锁行UPDATE ... WHERE stock ?做条件兜底。public int createOrder(int userId, MapInteger, Integer cart) { Connection conn null; try { conn dataSource.getConnection(); // 开启事务后续所有操作同进同退 conn.setAutoCommit(false); // 1. 锁定购物车涉及的所有商品行 // 事务内对同一批 id 加排他锁其他事务必须等待 String lockSql SELECT stock FROM fruit WHERE id ? FOR UPDATE; // 2. 条件扣减库存不足时影响行数为 0 String deductSql UPDATE fruit SET stock stock - ? WHERE id ? AND stock ?; // 3. 插入 t_order 主表获取自增主键 // 4. 插入 order_item 明细表记录单价快照 // 5. 清空 Session 购物车 conn.commit(); } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { } } throw new RuntimeException(下单失败库存不足或商品已下架, e); } finally { try { if (conn ! null) conn.setAutoCommit(true); } catch (SQLException e) { } try { if (conn ! null) conn.close(); } catch (SQLException e) { } } }setAutoCommit(false)之后的每一条 SQL 都不立即生效直到commit()。SELECT ... FOR UPDATE是悲观锁事务 A 锁住这批商品行时事务 B 对同一批行的读写会阻塞直到 A 提交。UPDATE ... WHERE stock ?是关键兜底即使锁失效或代码漏锁只要当前库存小于购买数量影响行数就是 0代码里检查影响行数即可判定失败并回滚。最后在finally里把连接归还连接池前务必把setAutoCommit(true)重置否则连接池复用时会带着false的提交状态。4.4 订单状态与超时未支付的边界订单状态用TINYINT字段维护就够了0 到 4 依次是待支付、已支付、已发货、已完成、已取消。课设阶段不需要做完整的支付对接但至少要在待支付订单上记录create_time并提供一个定时任务把超过 30 分钟未支付的订单状态改成已取消、库存加回。注意库存回补必须走同一个 Service 方法不能直接写UPDATE fruit SET stock stock 1否则再次踩到并发问题。这个小功能做完订单闭环的完整性就有了在答辩里是很加分的点。5. 部署到 Tomcat 与三个必须调的 JavaWeb 配置5.1 war 包部署与 context path 的关系本地验证完功能最后一步是把项目打成 war 包丢进 Tomcat。命令行执行mvn clean package在target目录下会生成fruit-market.war把它复制到 Tomcat 的webapps目录启动bin/startup.sh。Tomcat 会自动解压并把fruit-market作为 context path访问地址是http://localhost:8080/fruit-market/fruit/list。这里有个高频踩坑点本地用 IDEA 的 Tomcat 集成运行时如果不手动改 Application context默认也是/fruit-market但 IDE 里配置的 context 和命令行部署的可能不一致导致 JSP 里的c:url拼出的路径 404。最笨也最可靠的方法是部署后先访问http://localhost:8080/fruit-market/确认首页能开再逐级点链接。5.2 中文乱码的三个位置水果超市全是中文商品名乱码问题几乎是必现的。需要同时检查三个位置缺一个都会在某一层出问题位置配置症状Tomcat 接收请求参数server.xml的 Connector 加URIEncodingUTF-8GET 参数中文变问号Servlet 读取请求体req.setCharacterEncoding(UTF-8)POST 表单中文乱码数据库连接JDBC URL 加characterEncodingUTF-8入库后中文变??GET 请求的参数在请求行里Tomcat 默认按 ISO-8859-1 解码必须在 Connector 上指定URIEncodingUTF-8。POST 请求体则由 Servlet 容器按请求头 charset 解码setCharacterEncoding要在读取第一个参数之前调用。数据库连接串建议写全三个参数useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/ShanghaiMySQL 8 驱动强制要求时区参数漏掉会直接报连接异常。5.3 上线前验证并发扣库存与 Session 失效的检查方法手点页面只能验证流程通不通验证不了并发正确性。最轻量的做法是用 JMeter 开 100 个线程同时对一个库存为 50 的商品下单 100 次下单完成后查库存表结果必须恰好是 0不能出现负数且成功订单数加失败订单数等于 100。如果出现负数说明stock ?兜底失效检查事务是否真的开启、commit是否在全部 SQL 之后执行。Session 失效的验证同样重要把 web.xml 里的session-timeout临时改成 1 分钟加购商品后等 2 分钟再提交订单正确行为是跳转到登录页并提示会话过期而不是 500 或空指针——因为 Session 里的 cart 已经是 null提交订单的代码必须对 null 购物车做防御。验证完记得把超时改回 30 分钟。这一步做完水果超市 JavaWeb 项目才算是能拿得出手的完整案例而不是能点按钮的页面集合。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。