简介:这份基于 JSP+Servlet+MySQL 的蛋糕甜品店购物网站源码,面向正在完成毕业设计或课程项目的 Java 学习者,覆盖从商品浏览、购物车到订单提交与后台管理的完整电商流程。资源包内含源码、使用文档和数据库脚本,并整理出管理员与用户两类角色的功能模块,便于直接运行、二次开发或作为答辩演示素材。资源共 221 个文件,以 Java/class 源码、JSP 页面、SQL 数据库脚本为主,另含 jar 依赖、前端 css/js 与图片素材,压缩包约 7.23MB,结构简洁,适合本地部署学习;Tomcat 8/9、JDK 1.8 环境下即可运行调试。已有 187 人浏览学习,属于难度适中、评分较高的实战项目。除完整可运行代码外,资料还附有使用说明和数据库初始化文件,管理员与用户账号密码均已给出,可帮助快速跑通前后台流程、理解 Servlet+DAO 分层写法与订单、客户、类目管理等常见业务实现。
1. 蛋糕甜品店购物网站:一份能跑通的 JSP+Servlet+MySQL 全栈样例
一个基于 Java + MySQL 的蛋糕甜品店购物网站源码,Web 层用 JSP 和 Servlet,业务层自己写 DAO/Service,这种组合在课程设计和毕业设计里出现频率非常高。这份资源里管理员可以登录后台、管理订单、客户和类目,用户可以在前台注册、登录、浏览分类、加购物车、提交订单并模拟支付,功能闭环完整。实际拆解下来,技术难度中等偏基础,但覆盖面很全,比单纯一个 CRUD 案例更适合用来理解 JavaWeb 的请求流转。适合刚学完 JSP/Servlet、想找一套完整项目复现的人,也适合需要快速交付演示项目的开发者。它最大的价值不是代码本身多高明,而是让你能在真实项目上下文里把请求、Session、JDBC 这些概念串起来。
2. JSP+Servlet 三层架构拆解:从 class 文件反推 DAO、Service 与 Servlet 的职责边界
拿到压缩包后第一件事不是解压双击,而是看里面有哪些 .class 文件。我在不少资源包里见过这种情况:源码带了编译后的 class,但没带整个项目的目录说明。你可以根据类名直接反推出这个项目分了几层、每层管什么。像 GoodsDao、OrderDao、UserDao 这类名字,摆明是数据访问层;GoodsService、OrderService 是业务逻辑层;AdminGoodsEditServlet、AdminGoodsAddServlet 是控制层里负责后台商品操作的 Servlet;Goods、Order、User 这些不带前缀的类,则是数据库表对应的实体。
2.1 一份 class 文件清单背后的分层逻辑
把刚才的类名整理成表格:
| 层次 | 代表类 | 职责 |
|---|---|---|
| 实体层 | Goods.class、Order.class、User.class | 对应商品表、订单表、用户表,只有字段和 getter/setter |
| 数据访问层 | GoodsDao.class、OrderDao.class、UserDao.class | JDBC 查询、插入、更新,直接拼 SQL |
| 业务层 | GoodsService.class、OrderService.class | 订单金额计算、库存校验、事务边界 |
| 控制层 | AdminGoodsAddServlet.class、AdminGoodsEditServlet.class | 接收 HTTP 参数,调用 Service,控制页面跳转 |
| 表现层 | JSP 页面 | 展示数据、收集表单,不在里面写 Java 业务代码 |
这个分层的核心思想是单向依赖:Servlet 依赖 Service,Service 依赖 DAO,DAO 依赖数据库连接。如果在一个类文件里看到某个 Servlet 直接 new 了一个 DAO,虽然不愿意看到,但也说明这个项目还处在几个模块各干各的阶段。从 class 文件划分来看,这个资源里 DAO、Service、Servlet 是分开的,至少不是把所有逻辑堆在 JSP 里那种“一锅端”写法。
我第一次拆这类项目时,习惯先找 util 包。util 里通常有一个 DBUtil 类,负责管理数据库连接,避免每个 DAO 都写一遍 Class.forName。你可以在源码包里找一下有没有 DBUtil.java 或 DBConnection.java。如果是拿 class 文件直接部署,可以反编译看一眼,也可以只观察 DAO 方法是否重复获取连接。重复获取连接最大的问题是,每个 DAO 方法各管各的连接,Service 层想加事务的时候会把连接断在 DAO 里,事务根本加不上。这一点在后面第 6 章会再提。
目录结构一般是这样的:
src/ ├── com/shop/entity/ # Goods.java、Order.java、User.java ├── com/shop/dao/ # GoodsDao、OrderDao、UserDao ├── com/shop/service/ # GoodsService、OrderService ├── com/shop/servlet/ # AdminGoodsAddServlet、AdminGoodsEditServlet 等 ├── com/shop/util/ # DBUtil、字符编码工具 └── com/shop/filter/ # LoginFilter 之类的过滤器WebRoot 或 webapp 目录下则是 JSP 页面、css、js、图片,以及 WEB-INF/web.xml。在 IDE 里展开项目时,先确认 src 下的包路径和这里是否一致。不一致也不一定代表错误,但会给你后续找文件增加成本。用 IDEA 打开项目时,如果结构显示异常,可以先在右键 Mark Directory As 里把 src 设为 Sources Root,把 webapp 设为 Web Resources Directory,否则 Tomcat 找不到 JSP。
2.2 订单模块:从购物车到模拟支付的状态流转
订单是这个项目里最有价值的业务闭环。从用户视角看,流程是:进入首页浏览商品,按分类筛选,点商品详情,加入购物车,去购物车结算,提交订单,然后进入一个支付页面,点击“模拟支付”,支付成功后能在个人中心的订单列表里看到状态变化。这个流程里涉及商品表、购物车、订单表、订单项表,每次请求都要想清楚当前是哪个 Servlet 在处理,而不是全部塞进 JSP。
以提交订单为例,最常见的 Servlet 流程是:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从 session 取当前登录用户 User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect("login.jsp"); return; } // 2. 购物车存在 session 中:Map<goodsId, count> Map<Integer, Integer> cart = (Map<Integer, Integer>) request.getSession().getAttribute("cart"); if (cart == null || cart.isEmpty()) { request.setAttribute("error", "购物车为空"); request.getRequestDispatcher("cart.jsp").forward(request, response); return; } // 3. 业务层负责创建订单,内部会插入订单主表和订单项表 OrderService service = new OrderService(); Order order = service.createOrder(user.getId(), cart); // 4. 清空购物车,跳转到模拟支付页 request.getSession().removeAttribute("cart"); request.setAttribute("order", order); request.getRequestDispatcher("pay.jsp").forward(request, response); }这段代码要说明几个参数和行为。request.getSession().getAttribute("cart") 拿到的是购物车 Map,键是商品 id,值是购买数量。cart 为空时不能直接进入下单流程,否则会出现空订单。createOrder 方法里,OrderService 会循环 Map 中的商品,逐个查出价格,计算总价,再生成订单项,同时扣减库存。这个循环操作在入门的项目里经常会漏掉库存校验,比如用户在两个 tab 同时下单,库存扣成负数。你可以在 OrderService 里加一个前置查询:遍历购物车时,比较商品库存和购买数量,不足就直接抛出异常并提示。
购物车为什么放 Session 而不是数据库?答案是实现成本低。Session 存购物车不需要额外的表,也不需要处理跨表事务,只需要在用户会话内共享。代价是用户关闭浏览器或者 Session 超时后购物车就丢了。对于课程设计来说,这个选择是合理的,你甚至可以主动向答辩老师解释这个取舍,比被老师问住要好得多。
2.3 Servlet 单例多线程:别在这类项目里写成员变量
Servlet 的生命周期有且只有一个实例,所有请求线程共享同一个 Servlet 对象。这意味着 Servlet 里的成员变量会被多线程同时访问。很多初学者在这个项目里翻车,是因为在 Servlet 中写了类似这样的代码:
// 典型的错误写法 public class GoodsListServlet extends HttpServlet { private List<Goods> goodsList; // 所有请求共享 protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { goodsList = goodsDao.findAll(); // 会被并发覆盖 request.setAttribute("goodsList", goodsList); request.getRequestDispatcher("goods_list.jsp").forward(request, response); } }为什么错?假设用户 A 请求执行到 goodsList 被赋值,还没转发 JSP;用户 B 的请求同时进来,又给 goodsList 赋了另一批数据,于是 A 的 JSP 页面可能渲染出 B 的数据。解决办法很简单:把 goodsList 放到 doGet 方法内部作为局部变量,或者不用中间变量,直接 request.setAttribute("goodsList", goodsDao.findAll())。这也是为什么传统 JavaWeb 项目里,Servlet 通常只持有 DAO 或 Service 的引用作为成员变量,因为这些对象本身没有用户相关的状态。
从 class 文件里看,如果 Servlet 的成员变量很多,又不全是不变的常量,就要多留个心眼。这个项目里的 AdminGoodsEditServlet 和 AdminGoodsAddServlet 都应该是无状态 Servlet,只通过方法参数传递数据。你复现时可以用全局搜索看成员变量声明,凡是变量名像 tmpList、tmpUser 的,都值得怀疑。
3. 把项目跑起来:JDK、Tomcat、数据库的搭配与部署细节
3.1 运行环境选型:JDK 8 与 Tomcat 8.x 是稳定组合
资源说明里写的是 jdk1.8/jdk1.9,Tomcat8.x/9.x,IDE 支持 Eclipse、MyEclipse、IDEA。我的建议是别追新,选最稳的组合:JDK 8 + Tomcat 8.5 + MySQL 5.7 或 8.0。JDK 9 的模块系统改了一些内部行为,对老式 JSP/Servlet 项目没有正收益,反而可能因为模块化导致某些反射调用出问题。Tomcat 8.5 和 9.0 对本项目差异不大,你用的是 9 也没问题,只要你的 JDK 是 8 就行。
这里给出一个我常用的组合表:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8.0_202 及以上 | 不要用 11 或更高这类新 LTS,除非你打算重构 |
| Tomcat | 8.5.x 或 9.0.x | 8.5 和 9 都支持 Servlet 3.1/4.0,本项目用不到更高特性 |
| MySQL | 5.7 或 8.0 | 注意驱动和连接串匹配 |
| mysql-connector-java | 5.1.49 对应 MySQL5.7;8.0.x 对应 MySQL8 | 版本选错会出现连接报错 |
如果你本机已经安装 MySQL 8.0,而项目里自带的是 5.1 驱动,建议优先把驱动替换成 8.0.x,而不是卸载数据库。反过来,如果你为了图省事用 5.1 驱动连 MySQL 8,也会遇到 SSL 和时区问题。这个细节在第 5 章会专门展开,这里先记住结论。另外,如果你用的是 IDEA,最好把默认构建方式设为本地构建,不要让它自动下载 Maven 依赖,因为这种老项目大概率没有 Maven 配置,IDEA 有时候会多此一举地帮你做一堆无关操作。
3.2 导入数据库脚本:命令行导入与编码坑
资源包里的数据库文件通常以 .sql 结尾,名字可能是 cakeshop.sql 或 shop.sql。在导入前,先用文本编辑器打开文件,搜索 CREATE DATABASE 和 CREATE TABLE,确认库名、表名和你要用的环境是否一致。这一步很重要,因为有些人直接把脚本丢进命令行执行,结果库里多了一张自己都不记得的旧表。如果脚本自带 DROP TABLE,更要小心,它会清掉你手工造的数据。
导入命令本身很简单:
mysql -u root -p < cakeshop.sql执行后没有报错就算成功。如果提示 ERROR 1049 Unknown database,说明脚本里有 CREATE DATABASE 语句,但你的 MySQL 权限不允许建库,或者脚本里压根没有建库语句,只有 USE dbname。这种情况下,先手动建库:
CREATE DATABASE IF NOT EXISTS cakestore DEFAULT CHARSET utf8mb4; USE cakestore; SOURCE cakeshop.sql;SOURCE 命令和 < 重定向的效果一样,都是把文件内容按 SQL 逐行执行。Windows 下用 SOURCE 时,注意路径里的反斜杠要转义,比如 SOURCE C:/db/cakeshop.sql。另外,脚本里的建表语句如果带了TYPE=InnoDB这种老写法,在 MySQL 8 上会报语法错误,需要把 TYPE= 改成 ENGINE=,这也是老资源常见的兼容问题。
导入完成后,执行检查:
SHOW TABLES; SELECT * FROM admin;如果 admin 表能查到 admin 这条记录,说明基础数据已经有了。用户表里应该能查到 user1。如果两个表都没数据,那说明脚本里可能没有插入初始管理员,你需要手工补一条:
INSERT INTO admin (username, password) VALUES ('admin', 'admin');密码字段如果是明文,这样做没问题;如果代码里加了 MD5,就要用 MD5('admin') 插入。具体是什么,打开注册或登录的代码看一下就知道。
3.3 连接配置与部署:db.properties、IDEA Artifacts 与访问路径
数据库导入后,要改项目的数据库连接配置。传统 JSP/Servlet 项目通常在 src 下有一个 db.properties 或 jdbc.properties,用 DBUtil 读取。打开它,核心是这几行:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cakestore?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456逐项解释:driver 是 JDBC 驱动类名,MySQL 5 用 com.mysql.jdbc.Driver,MySQL 8 用 com.mysql.cj.jdbc.Driver;url 里的 localhost:3306 是数据库地址和端口,cakestore 是库名;useUnicode=true 和 characterEncoding=utf8 是一对,告诉驱动用 UTF-8 编码读字符串,这两个不加,中文必乱;username 和 password 要改成你本机的账号密码。如果你是 MySQL 8,url 要追加 serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true。
改完配置,部署是重头戏。用 IDEA 部署这一步,我一般按以下步骤来:
- 在 Project Structure 里确认 project 的 language level 是 8,SDK 是 1.8。
- 检查 Modules 里有没有把依赖 jar 加到 Libraries。
- 配置 Artifacts,新建 Web Application Exploded,名称保持默认。
- 在 Artifacts 的 Output Layout 里,把项目编译输出放入 WEB-INF/classes,把依赖库放入 WEB-INF/lib。
- 添加 Tomcat Server,Deployment 里选中这个 Artifact,Application context 设置为 /cakeshop 或干脆留空用 /。
- 启动 Tomcat,访问
http://localhost:8080/或/cakeshop/,能看到首页就算成功。
很多人在第 4 步漏了依赖库,导致页面能开但一查数据库就 ClassNotFound。这个坑太常见了,后面第 5 章会再说。另外,IDEA 里 Tomcat 默认的 HTTP port 是 8080,如果被占用,可以在 Server 标签页改成 8081,但要记得访问路径跟着改。Eclipse 里直接用 Run As -> Run on Server 会更省事,只不过它的工作目录逻辑和 IDEA 不同,碰到奇怪问题先清空 Tomcat 的 webapps 再部署。
4. 管理员与用户功能落地:登录鉴权、类目维护与订单管理的代码走读
4.1 管理员端:类目管理与商品新增的 Servlet 写法
管理员功能里,类目管理和商品管理是典型的增删改查页面。AdminGoodsAddServlet 负责接收后台商品表单,AdminGoodsEditServlet 负责把修改后的数据更新到库。这两个类几乎就是同一个套路:取参 -> 封装 -> 调 Service -> 跳转。
商品新增的标准写法:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String name = request.getParameter("name"); String priceStr = request.getParameter("price"); String categoryIdStr = request.getParameter("categoryId"); String description = request.getParameter("description"); String image = request.getParameter("image"); if (name == null || name.trim().isEmpty()) { request.setAttribute("error", "商品名称不能为空"); request.getRequestDispatcher("goods_add.jsp").forward(request, response); return; } double price = 0; int categoryId = 0; try { price = Double.parseDouble(priceStr); categoryId = Integer.parseInt(categoryIdStr); } catch (NumberFormatException e) { request.setAttribute("error", "价格或类目格式错误"); request.getRequestDispatcher("goods_add.jsp").forward(request, response); return; } Goods goods = new Goods(); goods.setName(name); goods.setPrice(price); goods.setCategoryId(categoryId); goods.setDescription(description); goods.setImage(image); GoodsService service = new GoodsService(); boolean success = service.addGoods(goods); if (success) { response.sendRedirect("goods_list.jsp"); } else { request.setAttribute("error", "数据库写入失败"); request.getRequestDispatcher("goods_add.jsp").forward(request, response); } }这段代码比基础版本多了两处校验。第一处是 name 的判空,第二处是价格和类目 ID 的 try-catch。别小看这两个校验,表单里只要有一个字段没填,后台就会抛 NumberFormatException,然后整个页面变成 500。课程设计里老师很可能故意填一个空价格来测你的健壮性,这种防御性写法非常加分。注意 setCharacterEncoding("UTF-8") 必须在任何 getParameter 之前,否则后续参数全是乱码。
4.2 用户端:注册、加入购物车与模拟支付的关键代码
用户端最容易实现的登录逻辑是:从表单拿 username 和 password,到 DAO 里查一次,拿到 User 对象就写 Session,没拿到就显示错误。参考代码:
String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { request.getSession().setAttribute("user", user); response.sendRedirect("index.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }这里有一个容易被忽略的细节:如果用户表里的密码字段是明文,那么只要比对字符串相等就行;如果代码里有 MD5 加密,那么 findByUsernameAndPassword 方法内部要把传入的 password 先加密再查询。你拆项目时,可以先看注册 Servlet 里是否调用了某个加密工具类。注册逻辑一般长这样:
String username = request.getParameter("username"); String password = request.getParameter("password"); User existing = userDao.findByUsername(username); if (existing != null) { request.setAttribute("error", "用户名已存在"); request.getRequestDispatcher("register.jsp").forward(request, response); return; } User user = new User(); user.setUsername(username); user.setPassword(password); userDao.insert(user); response.sendRedirect("login.jsp");注册代码里最该注意的地方是重名判断。如果没有这一步,用户表里可以插入两条相同用户名记录,登录时 findByUsernameAndPassword 就可能返回多行,DAO 如果只取第一条,另一个用户就永远登不上。很多入门项目缺少主键唯一约束,所以你复现时要看一眼建表语句里 username 字段有没有 UNIQUE 约束,没有的话建议加上。
购物车加入的代码,前面第 2 章给过一次 Session Map 方案,这里继续深化。加入购物车时,除了记录商品 id 和数量,还要注意 Session 里存的是对象还是 Map。有些项目会把 CartItem 封装成对象,里面放商品信息、单价和小计,好处是购物车页面可以直接显示商品名称和价格,不用再查一遍库。代码大致是:
List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new ArrayList<>(); session.setAttribute("cart", cart); } boolean found = false; for (CartItem item : cart) { if (item.getGoodsId() == goodsId) { item.setCount(item.getCount() + 1); found = true; break; } } if (!found) { CartItem item = new CartItem(goodsId, goodsName, price, 1); cart.add(item); }这段逻辑是:遍历购物车,如果该商品已存在则数量加 1,不存在则新建条目。这里用 List 而不是 Map,是为了在 JSP 里方便地用 foreach 显示每一项。如果你在项目里看到购物车是 Map,也不影响,只是取商品信息时得多一次商品表查询。模拟支付则是把订单状态从待支付改成已支付,它不需要真的接支付网关,只要写一条 update 语句。代码上可以放在 OrderPayServlet 里:
int orderId = Integer.parseInt(request.getParameter("orderId")); orderDao.updateStatus(orderId, 1); // 0待支付,1已支付,2已发货 response.sendRedirect("order_detail.jsp?orderId=" + orderId);4.3 权限控制:用 Filter 拦截未登录请求的常见做法
管理员和普通用户的权限要分开,否则任意访客都能访问后台。常见做法是 web.xml 里配置两个 Filter,或者一个 Filter 里根据 URL 前缀判断。如果是判断前缀,LoginFilter 的返回逻辑要注意排除公共资源,比如:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpSession session = req.getSession(false); String path = req.getRequestURI(); if (session != null && session.getAttribute("user") != null) { chain.doFilter(request, response); } else if (path.endsWith(".css") || path.endsWith(".js") || path.endsWith("login.jsp") || path.contains("LoginServlet")) { chain.doFilter(request, response); } else { ((HttpServletResponse) response).sendRedirect("login.jsp"); } }这里的核心是白名单机制。如果不放行 css/js,登录页的样式会全部丢失,看起来像没加载。如果放行范围太大,比如直接判断 path 包含 "admin",那么后台的 Servlet 也会被拦截逻辑绕过去,用户就可能绕过登录直接访问后台 admin 页面。更稳妥的做法是把后台路径统一放在 /admin 前缀下,然后用一个 AdminFilter 精确拦截 /admin/*。很多课程设计项目因为路径规划混乱,Filter 只能用一个宽泛判断,这也是答辩时容易暴露的问题。你可以在复现时主动重构成 /admin 前缀,一方面代码更清晰,另一方面也体现你理解权限控制。
Session 超时也值得关注。如果你希望用户操作多久之后自动退出,可以在 web.xml 里配置:
<session-config> <session-timeout>30</session-timeout> </session-config>这个单位是分钟。超时后 session 里的 user 对象会被清除,下次访问受保护页面就会跳回登录页。这对购物网站来说是合理的,不然用户挂着登录状态一直不退出,数据安全很难保证。
5. 避坑与排查:部署运行中必然遇到的五个问题
5.1 中文乱码:从 URL 到表结构都要指定 utf8
现象:启动后首页能打开,但商品名称显示成“???”,或者后台添加商品成功后再刷新变成问号。更诡异的是,有的页面正常、有的页面乱码,这是因为不同环节的编码设置不一致。
原因:中文乱码从来不是单一原因。三个地方最容易出问题:数据库表字符集、JDBC 连接串、Servlet 请求编码。只要有一个环节没指定 UTF-8,中文就会在转换过程中被截断。比如建表语句是 DEFAULT CHARSET=utf8,但连接串里没写 characterEncoding=utf8,MySQL 驱动会用系统默认编码读字符串,中文到 Java 这边就变成乱码。
解决:按三层排查。第一步,查数据库字符集:
SHOW CREATE TABLE goods;如果表的 DEFAULT CHARSET 不是 utf8mb4,用下面语句改整张表:
ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二步,改 JDBC 连接串,确保带useUnicode=true&characterEncoding=utf8。第三步,在 Servlet 开头调用request.setCharacterEncoding("UTF-8"),如果你用了框架的编码过滤器,确认过滤器的 init-param 是 UTF-8。另外 JSP 页面顶部必须写<%@ page contentType="text/html;charset=UTF-8" %>,否则浏览器按默认编码解析,还是乱。这个坑的麻烦点在于,每一层都改对才能好,漏一层就继续乱。我见过有人只改了数据库表,不管连接串,乱码依然存在,然后把整个 MySQL 重装了,其实问题根本不在那。
5.2 部署后 404:Artifacts 没导出 lib 或缺 WEB-INF 配置
现象:Tomcat 启动正常,首页能打开,但点击“蛋糕列表”或“管理员登录”后,地址栏变成 404,控制台报 ClassNotFoundException 或 FileNotFoundException。有时候在 IDEA 里不报错,换到 Eclipse 里就报错。
原因:绝大多数时候是缺 jar。IDEA 的 Artifacts 默认不会把外部库全部带到 Tomcat 的类路径里,你需要手动把它加进 WEB-INF/lib。另一个原因是没有 web.xml 或者 web.xml 里没有 Servlet 映射,Tomcat 找不到对应的 URL 路径。如果项目在 src/main/java 下,IDEA 可能没有把修改后的 class 更新到 Artifacts 的 classes 目录,导致访问的还是一份旧 class。
解决:进入 Project Structure -> Artifacts,看 Output Layout 右边区域。正常情况下应该有 WEB-INF/classes 和 WEB-INF/lib,并且 lib 里能看到 mysql-connector-java 等 jar 包。如果没有,在左侧 Available Elements 里选择你的模块 output 和所有库依赖,右键 Put into WEB-INF/classes 和 WEB-INF/lib。改完先 Build -> Rebuild Project,再重启 Tomcat。顺便确认 web.xml 里 Servlet 的 url-pattern 是不是 /adminGoodsAdd 或类似路径,页面表单的 action 要和它一致。如果 URL 对不上,页面就会把地址当成静态资源去解析,最后 404。这个坑特别容易被人忽略,因为它不是代码错误,是部署配置和生产环境不一致。我从那以后每次新建 Artifact 都会先看一眼 lib 区域有没有 jar。
5.3 登录失败:表单字段名与 Servlet 参数名不一致
现象:数据库里存在 user1/123456,控制台也没有异常,但页面始终提示“用户名或密码错误”。更诡异的是,用 admin 登录能成功,用 user1 就不行,于是怀疑是用户数据的问题,折腾半天发现是字段名不对。
原因:这类项目最容易翻车的地方是表单 name 和 getParameter 参数名不一致。比如 JSP 里写<input type="text" name="username">,Servlet 里却用request.getParameter("userName"),大小写不同导致取到 null。还有一种情况是表单用 GET 提交,但 Servlet 只在 doPost 里写了逻辑,doGet 直接返回,请求进来后什么都不执行就转到登录页。管理员能登录而用户不能登录,也有可能是 Cookie 或 Session 的 domain 问题,但在本地部署时概率很低。
解决:先在浏览器里打开登录页,按 F12 看请求头。如果是 POST,看 Form Data 里的字段名,再打开 LoginServlet 的代码,逐字比对 getParameter 的字符串。另一个排查技巧是复制一段:
System.out.println("username=" + username + ", password=" + password);如果打印出来是 null,问题就在参数名;如果打印出来正常,那问题就在 DAO 或 SQL。还有一点,查询数据库时如果用了SELECT * FROM user WHERE username = ? AND password = ?,参数顺序千万不要和赋值顺序不一致。用 PreparedStatement 时,setString(1, username) 和 setString(2, password) 的顺序要和 SQL 里问号的出现顺序一致,否则密码去匹配用户名,结果永远为空。这个错误我觉得比字段名不一致更隐蔽,因为它需要你仔细看 DAO 里的代码。
5.4 驱动连接报错:MySQL 8 驱动与 SSL、时区的关系
现象:启动后首页正常,但访问商品列表或登录等需要查数据库的页面时,页面 500,控制台报The server time zone value '�й�����ʱ��' is unrecognized或者Public Key Retrieval is not allowed。有时控制台还会出现 SSL 警告。
原因:这是 MySQL 8 和新版驱动之间的兼容问题。MySQL 8 要求 JDBC 连接串显式指定时区,否则驱动无法确定服务器时区。同时,MySQL 8 默认使用 caching_sha2_password 认证插件,第一次连接时如果没有 allowPublicKeyRetrieval=true,驱动不会获取服务器公钥,就会拒绝连接。项目里自带的旧驱动通常只有 5.x,对 MySQL 8 的这些行为根本没有处理。
解决:替换驱动 jar 为 mysql-connector-java-8.0.x,并把 db.properties 里 url 改成:
jdbc:mysql://localhost:3306/cakestore?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true改完替换 jar 后,记得重建 Artifacts,否则 Tomcat 还在用旧 jar。如果不想换驱动,也可以把 MySQL 服务器的认证插件改回 mysql_native_password,但对于本地学习项目,换驱动更省事。这个错误是无差别的,只要是 MySQL 8 环境,必现一次。建议你在拿到资源后的半小时内就先把这一步做掉,不然你会怀疑是不是代码写错了。
5.5 启动卡死:端口占用与 Tomcat 旧进程清理
现象:点完 Tomcat 启动按钮,控制台日志停在某个位置不动,界面一直转圈,最后提示Port 8080 was already in use。或者多次启动后,页面访问越来越慢,但 CPU 占用不高。
原因:上一个 Tomcat 实例没有被正常关闭,它的 Java 进程还在后台占着端口。IDEA 的 Stop 按钮有时只是和 Tomcat 断开连接,没有真正 kill 掉进程。如果之前开了多个运行配置,也可能同时启动了多个 Tomcat,互相抢端口。尤其用过 Eclipse、IDEA 混合跑同一个项目时,这两个 IDE 各自会带一个 Tomcat 实例,端口撞在一起。
解决:先用命令查端口占用:
netstat -ano | findstr 8080输出里最后一列是 PID,然后强制结束进程:
taskkill /F /PID 12345如果端口不是 8080 而是 8005(Tomcat 的控制端口)或 8009(AJP 端口),也需要一起清理。改 Tomcat 端口时,要保证 HTTP/1.1、Server 和 AJP Connector 三个端口都不冲突。Tomcat 启动卡死还有一个容易被忽略的原因,是 JDK 版本和 Tomcat 版本不匹配,比如 Tomcat 10 用 JDK8 跑会直接拒绝对某些类库的访问。这个资源是 JSP+Servlet 老项目,建议用 Tomcat 9,别用 10。Tomcat 10 把 javax.servlet 换成了 jakarta.servlet,老代码会直接编译不过或运行时找不到类,这不是配置问题,是 API 变更,这个坑我觉得比端口占用更值得记住。
6. 从能跑到能讲:三个让答辩更稳的进阶加固技巧
6.1 用 Filter 做统一编码和登录校验
与其在每个 Servlet 里写 request.setCharacterEncoding,不如写一个 EncodingFilter,把所有请求先统一处理。web.xml 里配置好 filter 和 mapping,Filter 的 doFilter 第一行调用 setCharacterEncoding,然后放行。这样代码集中,也不会因为漏写导致个别接口乱码。登录校验 Filter 可以和白名单结合,公共路径放行,需要身份的地方强制跳转,这让权限控制从一个 Servlet 的事变成了全局规则。
6.2 给提交订单 Service 补上事务边界
原生 JDBC 项目的 Service 层,十个有九个没有事务。表面上看单用户并发低不会出问题,但一旦在插入订单主表和订单项之间断电或异常,库里就会残留只有主表没有明细的脏数据。补事务的方法是在 Service 方法外用同一个 Connection:
Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 插入订单主表 // 插入订单项 // 扣减库存 conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) conn.close(); }setAutoCommit(false) 是核心,它让三条 SQL 变成一个原子操作。注意 DBUtil 和 DAO 必须都使用同一个连接,如果 DAO 内部各自 getConnection,那这个事务就白写了。我一般会在 DBUtil 里加一个 ThreadLocal 持有当前连接,让同一线程内所有 DAO 共享,这样 Service 层控制事务才有意义。
6.3 订单状态机:把模拟支付讲成完整业务闭环
模拟支付经常被当成一个无脑按钮,但你可以把它扩展成一个简单的状态机。订单状态用整数字段维护:
| status | 含义 | 触发操作 |
|---|---|---|
| 0 | 待支付 | 用户提交订单 |
| 1 | 已支付 | 用户点击模拟支付 |
| 2 | 已发货 | 管理员后台发货 |
| 3 | 已完成 | 用户确认收货 |
页面根据状态显示不同按钮,订单列表查询时按状态过滤。这样讲出来,老师会觉得你不是只会调用一个 update 语句,而是理解了订单在整个生命周期里的状态迁移。我最后一次跑这个项目的时候,就是先把这些边界情况列出来,然后一条一条验证,从那以后我拿到任何 JavaWeb 资源,都强制先跑通部署、加一个事务、补一个编码过滤器,再谈业务代码。希望这份蛋糕甜品店购物网站的拆解能帮你在复现和答辩的时候少走弯路。
本文还有配套的精品资源,点击获取