简介一份基于JSP和Servlet的JavaWeb购物商城项目面向初学JavaWeb的学生或需要快速搭建电商原型的开发者覆盖用户注册登录、商品分类浏览、购物车商品数量修改、订单提交与状态跟踪以及管理员后台等典型功能。资源压缩包共270个文件包含Java源码、编译后的class文件、JSP页面、XML配置文件、CSS样式与大量GIF演示截图整体仅7.08MB目录结构清晰方便按功能模块查阅。目前已有1287人学习下载项目中涉及商品、用户、订单等核心实体以及购物车与订单处理的业务控制逻辑并附带数据库脚本和项目配置文件可导入MySQL后在Tomcat中直接运行调试。对读者而言既能对照源码理解Servlet与JSP的分工协作又能借助演示截图直观还原页面交互其中还包含常见的数据访问与配置示例可用于课程设计、毕业设计或日常练手是一份轻量且具有实战价值的JavaWeb学习资料。1. 这是个什么项目Java Web 购物商城完整版我刚拿到这套 Java Web 购物商城源码时第一反应是“又是个放了几年没人动的课设”。但打开之后发现它把我最需要的部分都做完了注册登录、商品列表分页、购物车增减、订单生成和后台数据落库一条电商闭环完整走通。技术栈是经典的 Servlet JSP JDBC MySQL没引入 Spring 这类重型框架源码铺在你面前不用翻框架文档就能看懂完整请求链路。对刚学完 Java 基础、想拿真实项目练手的人它是最合适的入门样本对需要做课程设计、或者要给老项目做二次开发的人来说它结构清楚、跑起来快、改起来也不劝退。这篇文章从目录结构拆到核心代码再给你部署步骤和踩坑记录目标是让这份源码真正能运行在你本机并且你能自信地改它。2. 架构与目录Servlet JSP 三层结构和一次请求的完整链路传统 Java Web 购物商城不引入 Spring Boot而是用 Servlet 接收请求、JSP 渲染页面、JDBC 操作数据库这种“教科书三层”在今天依然有它的价值请求从页面到 Servlet 再到 DAO每个环节都能单独查看、单独调试不存在框架替你包办导致你找不到数据流向的情况。这套项目的选型克制而完整适合作为理解 Java Web 底层机制的训练场。2.1 技术栈选择教科书三层在真实项目里怎么落地这个项目的技术栈可以列成一张表每一项都能找到对应文件没有多余的依赖组件选型在项目里承担的角色语言Java 8兼容 Tomcat 8.5语法收敛适合教学与维护Web 容器Tomcat 8.5运行 Servlet/JSPwar 包直接放进 webapps 即可控制层Servlet 3.1接收 login、product、cart、order 等请求视图层JSP 2.3 JSTL 1.2渲染商品列表、购物车页面、订单确认页面数据层JDBC mysql-connector-java用 DAO 封装增删改查SQL 全走 PreparedStatement数据库MySQL 5.7用户、商品、购物车项、订单四类核心数据构建工具Maven 3.6管理依赖、编译打包成 war前端Bootstrap 3 jQuery页面布局、表单校验和基础交互这套选型最直观的优点是可观测性强。用户在页面上点击“加入购物车”请求交给了 TomcatTomcat 根据WebServlet注解找到对应 ServletServlet 从 request 里取参数、调 service 或 dao最后再跳回 JSP 渲染。整个链路没有 AOP、没有 IOC出现问题你只需要顺着调用栈往上翻基本能找到源头。对比 Spring Boot 购物商城项目少了自动配置和注解扫描带来的便利但也少了黑匣子。比如接收参数时Spring 用RequestParam自动做类型转换而这里你要手动request.getParameter(id)再自己Integer.parseInt确实容易翻车可正因为如此你才会真正记住“参数是字符串、要自己转型”这件事。2.2 目录结构从 webapp 到 dao 的分层布局项目按 Maven 标准结构组织包名按职责划分javaweb-shop/ ├── pom.xml ├── src/main/ │ ├── java/com/shop/ │ │ ├── controller/ # Servlet 控制器 │ │ │ ├── UserServlet.java │ │ │ ├── ProductServlet.java │ │ │ ├── CartServlet.java │ │ │ └── OrderServlet.java │ │ ├── service/ # 业务逻辑层 │ │ │ ├── UserService.java │ │ │ └── OrderService.java │ │ ├── dao/ # 数据访问层 │ │ │ ├── UserDao.java │ │ │ ├── ProductDao.java │ │ │ └── OrderDao.java │ │ ├── entity/ # 实体类 │ │ │ ├── User.java │ │ │ ├── Product.java │ │ │ ├── CartItem.java │ │ │ └── Order.java │ │ ├── util/ │ │ │ ├── DBUtil.java # 数据库连接工具类 │ │ │ └── MD5Util.java # 密码加密工具 │ │ └── filter/ │ │ └── LoginFilter.java # 登录状态拦截器 │ ├── resources/ │ │ └── db.properties # 数据库连接配置 │ └── webapp/ │ ├── index.jsp │ ├── WEB-INF/ │ │ ├── web.xml # Servlet映射、过滤器配置 │ │ └── jsp/ │ │ ├── login.jsp │ │ ├── register.jsp │ │ ├── product_list.jsp │ │ ├── cart.jsp │ │ └── order_confirm.jsp │ └── static/ │ ├── css/ # Bootstrap 样式文件 │ ├── js/ # jQuery 与页面脚本 │ └── images/ # 商品占位图 └── sql/ └── shop.sql # 建库建表与初始数据几个目录特别值得注意。controller 包里的 Servlet 不直接写 SQL而是调用 service 层service 再调 dao这是典型的层间依赖方向如果你的版本没有 service 包Servlet 直接调 DAO 也能跑但后续要加“下单时扣库存”这类涉及多张表的逻辑没有 service 层会写得很乱。dao 包全用 PreparedStatement 参数化查询这是安全底线项目里所有WHERE name ?都是占位符不拼接字符串。web.xml里登录过滤器的配置也值得一看filter filter-nameLoginFilter/filter-name filter-classcom.shop.filter.LoginFilter/filter-class /filter filter-mapping filter-nameLoginFilter/filter-name url-pattern/order/*/url-pattern url-pattern/cart/*/url-pattern /filter-mapping这里/order/*和/cart/*是拦截路径购物车与订单请求必须先经过登录校验。如果你把/product/*也加进去未登录用户连商品列表都看不到实际项目中商家通常希望游客也能浏览所以一般不拦商品页。2.3 一次商品搜索请求的完整调用链拿“搜索商品”这个最常见的操作走一遍全流程就能理解目录分层的作用。用户在 index.jsp 顶部的搜索框输入“手机”表单 action 指向${pageContext.request.contextPath}/product/search浏览器发起 GET 请求Tomcat 根据注解WebServlet(/product/*)把请求交给ProductServlet.doGet。doGet 拿到keyword参数后调ProductDao.search(keyword)DAO 执行SELECT * FROM product WHERE name LIKE ?占位符被ps.setString(1, % keyword %)填充。结果集封装成ListProduct返回Servlet 把它放进request.setAttribute(productList, list)再 forward 到product_list.jsp。JSP 里用 JSTL 的c:forEach循环渲染商品卡片每个商品下的“加入购物车”按钮带着 productId 等着下一次请求。这个链路里有两个细节能决定成败。第一转发和重定向的区别转发是服务器内部完成跳转request 里存放的productList数据能正常读到如果用sendRedirect浏览器发第二次请求原来的 request 对象已经销毁属性全部丢失。第二访问路径里的pageContext.request.contextPath动态拼出项目根路径页面里所有 form 的 action、静态资源的 src 都靠它不能在 JSP 里写死项目名否则换个 war 包名称就全部 404。这套设计贯穿整个项目controller 只做参数接收和页面跳转service 只做业务判断dao 只做 SQL 执行。职责边界维持好以后定位一个功能问题的时间基本在三十秒内。3. 核心模块拆解登录、商品分页、购物车和订单落库的实现这一章挑项目里最值得抄的四块代码讲登录会话、商品分页、购物车存取、订单生成。每段代码都来自实际可用版本不是伪代码可以直接对照自己的源码看。3.1 注册登录MD5 加密与 Session 校验登录处理集中在UserServlet里前端表单把用户名和密码 POST 过来Servlet 负责比对数据库WebServlet(/user/login) public class UserServlet extends HttpServlet { private UserDao userDao new UserDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password req.getParameter(password); // 密码统一走 MD5 加密后再比对不能拿明文直接查库 String md5Pwd MD5Util.encode(password); User user userDao.findByUsernameAndPassword(username, md5Pwd); if (user ! null) { // 登录成功把 user 对象放进 session供购物车和订单模块获取 req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /product/list); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/WEB-INF/jsp/login.jsp).forward(req, resp); } } }逻辑分四步先设置请求编码因为 Tomcat 8 之后 POST 的默认编码是 ISO-8859-1不带这行中文用户名会乱码再取出参数拼上 MD5 加密结果去查库判断 user 是否为空最后根据结果决定重定向到商品页还是转发回登录页提示错误。这里有两个容易被忽略的坑。第一登录成功用的sendRedirect而不是 forward因为登录成功后 url 应该变成商品列表地址用户刷新页面不会出现“表单重复提交”的弹窗这是重定向在 POST 场景下的经典用法。第二密码存储用的是 MD5 加盐前的版本还是纯 MD5要看MD5Util的具体实现纯 MD5 在今天的彩虹表攻击下已经不够安全生产环境建议换成BCrypt但课程设计和本地跑通场景下 MD5 已经够用。注册接口的写法类似多了一个“检查用户名是否已存在”的 pre-check返回true就提示“用户名已被注册”否则插入一条新记录并跳转登录页。3.2 商品列表分页LIMIT 参数与页面渲染商品列表是商城访问量最大的页面项目里用 MySQL 的LIMIT实现分页DAO 层接收 page 和 pageSize 两个参数public ListProduct findPage(int page, int pageSize) throws SQLException { String sql SELECT * FROM product ORDER BY id LIMIT ?, ?; ListProduct list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { // LIMIT 的两个参数第一个是偏移量第二个是每页条数 ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); try (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)); p.setImage(rs.getString(image)); list.add(p); } } } return list; }分页公式(page - 1) * pageSize是核心第一页 offset 是 0第二页如果是 10 条每页offset 就是 10第三页是 20依此类推。PreparedStatement 的两个占位符分别绑定偏移量和 pageSize防止用户传入负数页码时造成全表扫描。配套的ProductServlet还需要一个总条数查询代码通常是SELECT COUNT(*) FROM product再把总页数totalPage (totalCount pageSize - 1) / pageSize放进 request。前端 JSP 页面渲染分页条时需要把当前页码拼进查询字符串c:if test${currentPage 1} a href${pageContext.request.contextPath}/product/list?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} / ${totalPage} 页/span c:if test${currentPage totalPage} a href${pageContext.request.contextPath}/product/list?page${currentPage 1}下一页/a /c:if这段 JSP 的要点是页面不能写死 URL必须动态拼 contextPath否则部署路径一变翻页链接就会指向不存在目录。很多新人在这里把 request 参数名写成和 Java 代码里不一致比如 Java 代码叫pageNumJSP 里取${page}结果页面永远停在第一页排查时先对一遍参数名再查 SQL。3.3 购物车与订单Session 存储与一次扣库存购物车在这个项目里没有建数据库表而是放在 Session 中核心结构是ListCartItemCartItem包含商品 id、商品名、单价、数量和总价。添加商品的CartServlet核心代码如下WebServlet(/cart/add) public class CartServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); int productId Integer.parseInt(req.getParameter(productId)); int quantity Integer.parseInt(req.getParameter(quantity)); HttpSession session req.getSession(); // 从 session 取购物车如果还没有就新创建一个 ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayList(); session.setAttribute(cart, cart); } // 遍历购物车相同商品只累加数量不重复加行 boolean exists false; for (CartItem item : cart) { if (item.getProductId() productId) { item.setQuantity(item.getQuantity() quantity); item.setTotalPrice(item.getPrice().multiply( BigDecimal.valueOf(item.getQuantity()))); exists true; break; } } if (!exists) { // 首次加入用 id 查出商品信息来构建 CartItem Product p new ProductDao().findById(productId); cart.add(new CartItem(p.getId(), p.getName(), p.getPrice(), quantity)); } // 跳回购物车页面不能 forward否则刷新会重复添加 resp.sendRedirect(req.getContextPath() /cart.jsp); } }这段逻辑是购物车模块最容易出问题的部分。第一件要注意的事是 Session 里存的对象购物车存在 Session 里意味着同一个浏览器、同一个 sessionId 才会共享换浏览器就看不到购物车这是设计约束不是 bug。第二件是 BigDecimal 做金额运算时不能直接multiply(int)要点是把 int 数量转成BigDecimal.valueOf(quantity)再乘否则会出现结果被截断的精度问题。第三件是添加完成后用重定向而不是转发转发会导致刷新页面时重新执行一次 POST购物车里的数量被重复累加。订单模块稍微复杂一点OrderServlet里执行的是多表联动先从 Session 中取出购物车列表计算总金额插入 order 表获得订单 id再遍历 cart 里的每一项插入 order_item 表同时更新 product 表的库存// 下单核心步骤写订单主表 - 写订单明细 - 扣库存 - 清空购物车 boolean success false; Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务任何一个步骤失败都整体回滚 Order order new Order(); order.setUserId(((User) req.getSession().getAttribute(loginUser)).getId()); order.setTotalAmount(totalAmount); order.setStatus(1); int orderId orderDao.insert(conn, order); // 1. 插入订单主表 for (CartItem item : cart) { orderItemDao.insert(conn, orderId, item); // 2. 插入明细 productDao.deductStock(conn, item.getProductId(), item.getQuantity()); // 3. 扣库存 } cartDao.clear(req.getSession()); // 4. 清空购物车 conn.commit(); success true; } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步异常就回滚 } log.error(下单失败, e); } finally { if (conn ! null) { conn.close(); } }这段代码是最接近生产环境的实现。setAutoCommit(false)开启手动事务后必须保证 insert 订单、insert 明细、update 库存三步要么全部成功、要么全部失败否则会出现“订单生成了但库存没扣”或者“库存扣了但订单没写进去”的脏数据。如果你拿到的项目版本里没有这层事务控制下单时就会埋雷这是个值得自己动手补强的点。4. 部署与运行Tomcat 8.5 MySQL 5.7 的本地启动全流程源码拿到手第一步是让它在本地跑起来。整个部署流程分成环境准备、建库导入、打包部署三步每一步需要的工具和版本尽量按下面的表对齐不要用 JDK 11 跑 Tomcat 8.5 的老项目ClassLoader 问题会很折磨人。4.1 本地环境版本对齐是省事的关键组件推荐版本说明JDK8u202Tomcat 8.5 在 JDK 8 下最稳定Maven3.6.3负责依赖解析与打包MySQL5.7与源码 SQL 语法兼容性最好Tomcat8.5.x直接解压版即可不要用安装版IDEIntelliJ IDEA直接识别 Maven 项目运行最省事安装顺序建议是 JDK 先行、Maven 其次、MySQL 再次、Tomcat 最后。装完后在命令行验证java -version mvn -v mysql --version三条命令都正常输出版本号后再继续否则后面所有报错都要往环境变量上排查。Tomcat 8.5 的启动可以通过bin/startup.batWindows或bin/startup.shLinux/macOS本地开发更推荐直接用 IDEA 里配置 Tomcat Server 的方式启动好处是控制台能直接看到日志和异常堆栈。4.2 数据库初始化建库建表与初始数据项目根目录的sql/shop.sql是数据库初始化脚本包含建库、建表、插入测试商品和默认管理员账号。命令行导入方式mysql -uroot -p sql/shop.sql导入后检查一下库和表是否正常生成SHOW DATABASES; USE shop; SHOW TABLES; SELECT id, name, price FROM product LIMIT 5;预期应该看到 user、product、order、order_item 四张表商品表里有几条测试数据。如果看到的是空库检查shop.sql开头是否带了CREATE DATABASE IF NOT EXISTS shop没有的话手动执行一遍建库语句再导入。接下来修改src/main/resources/db.properties里的连接配置jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/shop?useUnicodetruecharacterEncodingutf-8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你的数据库密码注意useUnicodetruecharacterEncodingutf-8这两个参数不能去掉MySQL 5.7 默认字符集可能是 latin1不指定连接编码会导致数据库里存的中文读出后乱码。serverTimezoneAsia/Shanghai主要给 MySQL 8.0 用5.7 可不加但留着也无害。4.3 打包部署Maven 构建之后放入 Tomcat数据库就绪后开始打包。在项目根目录执行mvn clean package第一次运行会下载依赖耗时几分钟。构建成功后target目录下出现javaweb-shop.war把它复制到 Tomcat 的webapps/目录下cp target/javaweb-shop.war %TOMCAT_HOME%/webapps/启动 Tomcat%TOMCAT_HOME%/bin/startup.bat浏览器访问http://localhost:8080/javaweb-shop/看到首页说明启动成功。测试完整个闭环可以按这个顺序走注册一个账号 → 去商品列表页翻两页 → 把商品加入购物车 → 进购物车改数量 → 下订单 → 到订单列表确认记录存在。这套流程能跑通说明数据库、Session、事务都没问题。比起手动复制 war更推荐在 IDEA 里直接配置 TomcatProject Structure 里添加 Artifact 为javaweb-shop:war explodedRun Configuration 里选 Tomcat ServerDeployment 页签里把 Artifact 加进去Application context 填/javaweb-shop。这样每次修改代码后IDEA 自动把 class 文件同步到 Tomcat 的工作目录不需要手动重启容器开发效率会高很多。5. 避坑与排查五个容易翻车的常见问题这个项目代码量不大但我在亲自跑通和帮人看问题的过程中遇见过重复率极高的五个坑。每条都按现象、原因、解决三步说明你可以把这一章当成排查手册用。5.1 数据库连接失败MySQL 8 使用旧驱动报错现象项目启动后所有涉及数据库的页面都抛 500Tomcat 日志里出现Communications link failure或The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因源码环境是 MySQL 5.7配的是 mysql-connector-java 5.1.x如果你本机装的是 MySQL 8.0旧驱动对新增的认证插件caching_sha2_password和时区配置不兼容连接建立阶段就失败。解决把 pom.xml 中 mysql-connector-java 的版本改为 8.0.x同时在jdbc.url里补上useSSLfalseserverTimezoneAsia/Shanghai。数据库账号的认证方式也要改成 mysql_native_password命令是ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;。改完重启 Tomcat 再看日志这一步能解决九成连接报错。5.2 JSP 页面中文乱码编码设置不一致现象浏览器打开商品列表中文商品名变成一排问号或乱码从数据库查出来的一段字符串在页面显示正常但注册的用户名在数据库里存的是乱码。原因三层编码不一致导致。第一层是 JSP 页面本身的pageEncoding第二层是请求参数的编码第三层是 JDBC 连接与 MySQL 表的字符集。任何一层是 ISO-8859-1 或 latin1中文就保不住。解决统一三处。JSP 文件头部确认% page contentTypetext/html;charsetUTF-8 languagejava %每个 Servlet 的doPost开头执行req.setCharacterEncoding(UTF-8)数据库建表语句确认DEFAULT CHARSETutf8连接 URL 带上characterEncodingutf-8。改完后清空之前的测试数据重新跑乱码问题不会再回弹。5.3 静态资源 404CSS 和图片加载不出来现象页面能打开但整个商城没有皮肤图片全部裂图浏览器 F12 控制台一堆 404报错路径类似http://localhost:8080/css/bootstrap.min.css。原因JSP 页面里把静态资源路径写死了用了/css/bootstrap.min.css而不是项目上下文路径/css/bootstrap.min.css。Tomcat 部署后的实际上下文是javaweb-shop但浏览器直接访问根路径/css/...Tomcat 在 ROOT 下找这个目录自然找不到。解决所有静态资源的引用统一改用${pageContext.request.contextPath}前缀比如link relstylesheet href${pageContext.request.contextPath}/static/css/bootstrap.min.css script src${pageContext.request.contextPath}/static/js/jquery-3.5.1.min.js/script这项改动也适用于 form 的 action 和超链接的 href不要在 JSP 里出现任何以斜杠开头、又没有 contextPath 的路径。5.4 购物车内容在重启后消失Session 的固有限制现象项目跑着的时候购物车正常但重启 Tomcat 或按 Ctrl Shift F10 重新部署后之前加进购物车的商品全没了用户必须重新登录、重新加一遍。原因购物车对象存在 HttpSession 里Session 是 JVM 内存中的数据结构Tomcat 重启时内存释放Session 全部销毁。如果购物车数据没落到 MySQL 或 Redis 这种持久化存储里它就只活在一段进程周期内。解决这是设计取舍不算 bug。课程设计场景下存 Session 完全够用但如果要改善体验有两个方向。第一个最轻量——把购物车序列化成 JSON 存到浏览器 localStorage每次加载页面时和 Session 里的合并优点是无需后端改动第二个接近生产——新建 cart_item 表购物车改成数据库存储字段包括 user_id、product_id、quantity登录后从库里读回。走数据库方案要注意连同“未登录用户也能加购物车”的需求一起考虑通常做法是游客购物车存 Cookie登录后迁移到库。5.5 Tomcat 热部署几次后内存溢出现象IDEA 里连续改了四五次代码Tomcat 自动重载完 class 后控制台报java.lang.OutOfMemoryError: PermGen space或者Metaspace然后整个容器假死。原因Tomcat 热部署时会创建新的 ClassLoader 加载新 class但旧类可能被某些静态引用占住无法被回收。项目里如果用到了日志框架、连接池等持有静态引用的库几次热部署后内存就满了。解决最有效的处理是开发期把 JVM 的元空间上限调大——IDEA 的 Tomcat Run Configuration 里在 VM options 填入-XX:MaxMetaspaceSize512m。更稳妥的做法是每次改动比较大时直接重启 Tomcat不要强行依赖热部署。生产环境更是坚决不用热部署改完代码就正常停服、发新版战争包、再启动。6. 二次开发起点在购物车旁新增一个「收藏」功能拿到能跑起来的源码后最好的练手方式是在既有模块旁加一个新功能。以“商品收藏”为例它不需要改动订单逻辑又能完整覆盖建表、DAO、Servlet、JSP、页面入口五个环节。新功能要走的路和商品浏览一致新增favorite表字段包括 id、user_id、product_id、create_time两个业务查询是“判断用户是否已收藏某商品”和“查用户收藏列表”。DAO 层写FavoriteDaoServlet 层写FavoriteServlet处理两个动作/favorite/add和/favorite/remove先生成 FavoriteServletWebServlet(/favorite/add) public class FavoriteServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { User user (User) req.getSession().getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /user/login); return; } int productId Integer.parseInt(req.getParameter(productId)); new FavoriteDao().insert(user.getId(), productId); // 收藏完成后回到来源页避免用户丢失浏览位置 String referer req.getHeader(Referer); resp.sendRedirect(referer ! null ? referer : req.getContextPath() /product/list); } }收藏按钮放到商品列表和商品详情页里样式参考“加入购物车”按钮action 指向/favorite/add?productId${p.id}。页面入口再加一个“我的收藏”链接跳转FavoriteListServlet查列表用 JSTL 渲染成和商品列表一样的卡片布局。这个功能唯一要注意的是登录态校验未登录用户点击收藏应该跳回登录页而不是直接报错参考 LoginFilter 的做法在当前 Servlet 里做一次判断即可。验证这次改造是否成功走一个带业务视角的流程注册一个新账号 → 打开商品列表 → 收藏两个商品 → 退出登录 → 重新登录 → 点“我的收藏”看到那两个商品还在 → 到数据库里SELECT * FROM favorite确认数据落库。如果收藏与真实用户绑定、重启后不丢失这个功能就和购物车形成了正确对比——一个存内存一个存库。这次改动基本上把整个项目的扩展套路跑了一遍加表、加实体、加 DAO、加 Servlet、改 JSP、加入口。以后你再想加“订单评论”“收货地址”“商品分类”这些功能结构都是一样的。除了功能本身更要体会的是数据流对自己代码的约束。从那以后我每次加新功能都强制先画一遍数据流草稿字段从哪来、状态存哪里、权限卡在哪层三个问题想清楚再动手。这个项目正是练习这套习惯的最好载体希望帮到你。本文还有配套的精品资源点击获取