资讯详情

资讯详情

JavaWeb蛋糕商城实战:JSP+Servlet+MySQL从建表到部署全流程

简介一套基于JavaWeb的蛋糕商城项目源码适合Java初学者、毕业设计或课程实践使用。资源为ZIP压缩包共225个文件主要包含52个Java源文件、52个class编译文件、23个JSP页面、16个CSS样式、5个JS脚本及11个JAR依赖库另有SQL脚本和图片素材压缩包约10.97MB。目前已有6784人学习下载。项目完整覆盖客户管理、商品管理和订单管理三大模块支持用户注册登录、个人信息修改管理员可添加、删除客户并重置密码商品侧包含购物车基于CookieJSON、商品分类与类目管理、搜索、文件上传以及横幅/热销/新品推荐订单模块实现用户提交订单和管理员查询、删除订单。源码层次清晰附带数据库脚本可直接导入IDE运行适合学习JavaWeb分层开发、Servlet与JSP协作、Cookie/JSON购物车等实现细节也可作为二次开发或课程设计的基础。1. 为什么 90% 的人不是卡在业务逻辑而是卡在“跑不起来”你搜“JavaWeb 蛋糕商城”大概率是在赶课程设计或毕业设计。这个项目真正的业务逻辑并不复杂用户注册登录、浏览蛋糕、加购物车、下单管理员维护分类和商品、修改订单状态两个下午能写完。真正卡住你的往往是另一件事IDEA 里 Tomcat 配了半天还是 404、JSP 页面中文全是问号、MySQL 驱动报错、购物车一刷新就全没了。这些坑没有一个是玄学全部有标准解法。这篇笔记按照我带学生做项目的顺序从建表到部署一步步拆开讲适合需要一套完整 JavaWeb 案例的人也适合黑马笔记翻了几遍但还想看一套能跑通代码的人。2. JavaWeb 蛋糕商城技术选型JSPServletMySQL 这套组合为什么还是主流2.1 什么场景下 JSPServletMySQL 反而比 Spring Boot 更稳你在网上搜 JavaWeb 蛋糕商城十有八九会看到有人劝你直接上 Spring Boot。我的看法是先看验收要求。很多课程设计题目写得明明白白——“基于 JSP/Servlet 的网上蛋糕商城”或者“不得使用 Spring 框架”。这种情况下你硬上 Spring Boot老师要么看不懂你的架构要么直接按题目要求扣分得不偿失。另一个原因是数据规模。蛋糕商城这种题目用户量几十个、商品几十条根本用不到 Spring 的依赖注入价值。JSPServlet 的三层结构——JSP 负责展示、Servlet 负责接收请求、DAO 负责数据库操作——已经能把问题拆得很清楚你写代码时脑子里始终有一条完整的请求链路。Spring Boot 对内嵌 Tomcat、自动配置这些做了封装反而让你少看到很多东西答辩时老师一问“Servlet 生命周期”你答不上来项目再好也打了折扣。这套组合还有一个现实优势参考案例多。黑马程序员笔记、头歌实训平台上的 JavaWeb 案例几乎都是同一套 JSPServletMySQL 模板。你卡住的时候百度搜索“javaweb 项目完整案例 mysql”能找到大量同构代码你用的是 Spring Boot反而要自己翻译两边。技术选型不只选框架还要选“你遇到问题时谁能帮你”。2.2 目录结构与 Maven 依赖一个能直接跑的最小骨架我建议用 IDEA 直接创建 Maven webapp 骨架项目省去手动复制 lib 目录这一步。标准的 JavaWeb 蛋糕商城目录长这样cake-shop/ ├── pom.xml └── src/main/ ├── java/com/cakeshop/ │ ├── entity/ # User, Cake, Category, Orders, OrderItem │ ├── dao/ # UserDao, CakeDao, CategoryDao, OrdersDao │ ├── service/ # OrdersService事务控制放这里 │ ├── controller/ # LoginServlet, RegisterServlet, CartServlet... │ ├── filter/ # LoginFilter, EncodingFilter │ └── util/ # DBUtilDruid 连接池 └── webapp/ ├── index.jsp ├── css/ js/ images/ ├── WEB-INF/ │ ├── web.xml │ └── jsp/ # 放在 WEB-INF 下防止直接 URL 访问 └── admin/ # 管理端页面单独放一个目录pom.xml 里最关键的四个依赖是这样配的dependencies dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependencies四个依赖的作用和边界要心里有数。servlet-api 和 jsp-api 的 scope 必须是 provided因为 Tomcat 自己带了这两个 jar你不加 provided 的话打包出的 war 里会重复打入一份部署时和容器自带版本冲突报各种奇奇怪怪的方法找不到错误。jstl 是 JSP 页面里用来遍历蛋糕列表的标签库没有它c:forEach直接报错。druid 是连接池比 DriverManager 裸连稳得多后面会配合 DruidDataSource 读取配置文件初始化。2.3 IDEA 里配置 Tomcatwar exploded 和 Application context 的闭环IDEA 运行 JavaWeb 项目配置是新手翻车重灾区流程并不长按下面顺序一次走通。先到官网下载 Tomcat 8.5 或 9.0千万不要下载 Tomcat 10原因在第 5 章单独讲。下载解压后IDEA 里打开 Run - Edit Configurations点左上角加号找到 Tomcat Server - Local。在 Server 标签页的 Application server 处选择你的 Tomcat 目录此时 IDEA 会自动检测出 Tomcat 版本。然后切到 Deployment 标签页这就是 404 问题的高发区。点加号选 Artifact会看到两个选项cake-shop:war 和 cake-shop:war exploded。新手理解中的“部署”是把 war 包丢进 webapps但 IDEA 调试阶段必须选 war exploded它表示把项目编译产物按目录结构映射到 Tomcat 里这才支持热部署和断点调试。选中后把下方 Application context 改成/这样浏览器访问http://localhost:8080/就能直达你的 index.jsp不用每次输入项目名。最后回到 Server 标签页你还会看到两个端口HTTP port 默认 8080JMX port 默认 1099。如果 1099 被占用Tomcat 能启动但控制台会一直刷错误日志改成 1098 或 1088 即可。点击启动按钮看到Connected to server并且浏览器出现 index.jsp 页面这套 JavaWeb 项目最小配置就算闭环了。之后你改了 Java 代码按 CtrlShiftF10 重新编译IDEA 会自动把新编译产物同步到 Tomcat不需要重启容器。3. 蛋糕商城数据库设计6 张表、字段级注释与三个取舍3.1 6 张核心表的关系模型蛋糕商城按功能域拆分最少需要 6 张表。用户表管前台登录和管理员身份分类表与蛋糕表做一对多关联订单表和订单项表承接购物车提交流程最后加一张评价表让项目在答辩时有“扩展功能”可讲。注意购物车我没有单独建表原因在第 4 章会展开。具体关系是一个用户下多个订单一个订单包含多个订单项每个订单项指向一个具体的蛋糕快照。分类表在逻辑上是一颗树但蛋糕商城这种规模只有一级分类就够了做成 child_id 指向 parent_id 的无限级分类属于过度设计只会让 JSP 页面写起来格外痛苦。用户表1────N订单表1────N订单项表N────1蛋糕表 蛋糕表N────1分类表 用户表1────N评价表N────1蛋糕表订单表和订单项表的拆分是必须的。如果不拆一个订单买三种蛋糕就要在订单表里存三行记录那么“订单状态”“下单时间”“收货地址”这些字段就要重复存三遍。拆开后订单表存一次公共信息订单项表用 order_id 关联每一行只描述“买了哪个蛋糕、买了几份、当时多少钱”。3.2 初始化脚本建库建表与测试数据我用 MySQL 8.0 建表的完整脚本如下字符集统一用 utf8mb4。商品表和订单表里有关键字风险的是description和price没有直接用 MySQL 关键字但desc这种缩写是保留字字段名宁可用description全称也不要图省事。CREATE DATABASE IF NOT EXISTS cake_shop DEFAULT CHARACTER SET utf8mb4; USE cake_shop; CREATE TABLE tb_user ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 密码建议存MD5值, real_name VARCHAR(50) COMMENT 收件人姓名, phone VARCHAR(20) COMMENT 联系电话, address VARCHAR(255) COMMENT 默认收货地址, is_admin TINYINT NOT NULL DEFAULT 0 COMMENT 是否管理员1是0否, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB COMMENT用户表; CREATE TABLE tb_category ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序值越小越靠前 ) ENGINEInnoDB COMMENT蛋糕分类表; CREATE TABLE tb_cake ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 商品ID, category_id INT NOT NULL COMMENT 所属分类ID, name VARCHAR(100) NOT NULL COMMENT 蛋糕名称, description TEXT COMMENT 商品描述, price DECIMAL(10,2) NOT NULL COMMENT 单价, image_url VARCHAR(255) COMMENT 图片相对路径, stock INT NOT NULL DEFAULT 100 COMMENT 库存, is_deleted TINYINT DEFAULT 0 COMMENT 逻辑删除1下架0正常, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT蛋糕商品表; CREATE TABLE tb_orders ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 订单ID, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 订单编号, user_id INT NOT NULL COMMENT 下单用户ID, total_amount DECIMAL(10,2) NOT NULL COMMENT 订单总金额冗余字段, receiver_name VARCHAR(50) NOT NULL COMMENT 收件人, receiver_phone VARCHAR(20) NOT NULL COMMENT 联系电话, receiver_address VARCHAR(255) NOT NULL COMMENT 收货地址, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付1已支付2已发货3已完成4已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT订单表; CREATE TABLE tb_order_item ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 订单项ID, order_id INT NOT NULL COMMENT 所属订单ID, cake_id INT NOT NULL COMMENT 蛋糕ID, cake_name VARCHAR(100) NOT NULL COMMENT 蛋糕名称快照, cake_image VARCHAR(255) COMMENT 图片快照, price DECIMAL(10,2) NOT NULL COMMENT 成交单价快照, quantity INT NOT NULL COMMENT 购买数量 ) ENGINEInnoDB COMMENT订单项表; CREATE TABLE tb_review ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 评价ID, order_item_id INT NOT NULL COMMENT 关联订单项, user_id INT NOT NULL COMMENT 评价人, content VARCHAR(500) COMMENT 评价内容, score TINYINT DEFAULT 5 COMMENT 评分1到5, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT商品评价表;建表脚本里藏着两个容易被忽略的参数约定。第一个是用 DECIMAL(10,2) 而不是 FLOAT 存价格FLOAT 有精度误差算总价时多个商品相加会出现 39.999999 之类的结果DECIMAL 是定点数金额场景必须用它。第二是is_deleted存在 tb_cake 表里做逻辑删除订单项里的cake_name、cake_image、price三个字段则是快照。这两个设计的理由下一节展开讲。再补一段测试数据让项目跑起来至少能看到三个分类、六个蛋糕INSERT INTO tb_user (username, password, real_name, phone, address, is_admin) VALUES (admin, e10adc3949ba59abbe56e057f20f883e, 管理员, 13800000000, 校内, 1); INSERT INTO tb_category (name, sort) VALUES (慕斯蛋糕, 1), (奶油蛋糕, 2), (芝士蛋糕, 3); INSERT INTO tb_cake (category_id, name, description, price, image_url, stock) VALUES (1, 草莓慕斯, 当季草莓搭配轻乳酪慕斯6寸, 128.00, /images/cake1.jpg, 100), (1, 芒果慕斯, 台农芒果果肉夹层6寸, 118.00, /images/cake2.jpg, 100), (2, 动物奶油水果蛋糕, 进口淡奶油加新鲜水果8寸, 168.00, /images/cake3.jpg, 80), (2, 巧克力黑森林, 巧克力碎加樱桃夹心8寸, 158.00, /images/cake4.jpg, 80), (3, 轻乳酪芝士, 日式轻乳酪配方6寸, 138.00, /images/cake5.jpg, 50), (3, 榴莲千层, 猫山王榴莲果肉8寸, 188.00, /images/cake6.jpg, 50);注意密码存的是e10adc...这是 123456 经过 MD5 加密后的值。课程设计阶段数据库直接放明文密码也能跑通但答辩时老师只要问一句“密码怎么处理”你就会处于被动。加一层 MD5 成本极小DAO 层查询时先对用户输入的密码做同样处理再比对即可。3.3 三个取舍订单快照、逻辑删除、分类表第一个取舍是订单项为什么要存cake_name和price快照。假设管理员后来把“草莓慕斯”价格从 128 改成 168如果订单项表不存快照历史订单显示的价格会跟着变用户下单时明明付了 128订单详情里却显示 168这是严重的业务错误。订单一旦生成商品名称、图片、单价都必须固定在订单项表里之后商品表的改动不影响历史订单。第二个取舍是商品表用is_deleted逻辑删除而不是直接DELETE FROM tb_cake WHERE id ?。订单项表用 cake_id 关联商品表如果你物理删除蛋糕历史订单里的关联就断裂了订单详情页显示不出蛋糕名称和图片。逻辑删除只在查询列表时加WHERE is_deleted 0让蛋糕从页面消失但订单关联数据永远完整。第三个取舍是分类表不加 parent_id。很多教程会教你做无限级分类父分类套子分类表格设计成parent_id INT DEFAULT 0。蛋糕商城这种规模分类只会有一层你强行做成树形JSP 里渲染分类下拉框就要递归查数据库代码复杂度翻倍评分不会因此提高。你可以在答辩时说“当前需求单级分类足够扩展时加 parent_id 字段即可”这是设计边界不是缺陷。4. 登录、商品列表、购物车、下单Servlet 与 JSP 的请求流转实录4.1 登录与 Session转发和重定向为什么不能混用登录逻辑是理解 JavaWeb 全局会话的关键。用户提交用户名密码后LoginServlet 要做三件事调用 DAO 验证身份、把用户对象写进 Session、跳转到首页。这里有个最常见的翻车点登录成功后用 forward 转发到 index.jsp登录失败后用 sendRedirect 重定向回 login.jsp顺序搞反会导致 Session 数据丢失。先看正确的登录 Servlet 写法WebServlet(/login) public class LoginServlet 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 MD5Util.md5(req.getParameter(password)); User user userDao.findByUsernameAndPassword(username, password); if (user null) { req.setAttribute(msg, 用户名或密码错误); // 登录失败转发到登录页保留错误提示 req.getRequestDispatcher(/login.jsp).forward(req, resp); } else { // 登录成功把用户写进 Session HttpSession session req.getSession(); session.setAttribute(loginUser, user); session.setMaxInactiveInterval(30 * 60); // 重定向到首页避免刷新页面时重复提交表单 resp.sendRedirect(req.getContextPath() /index); } } }这段代码的关键在最后两个跳转的区别。forward是一次请求内部的转发浏览器地址栏不变request 里的msg属性能带到 login.jsp 显示错误提示sendRedirect是浏览器重新发起一次新请求地址栏会变化request 里的属性全部丢失但 Session 里的属性不受影响。所以登录成功后必须用重定向否则用户按 F5 刷新首页浏览器会重新提交一次 POST 请求造成重复登录的错觉。再说 Session 的有效期设置。session.setMaxInactiveInterval(30 * 60)表示客户端 30 分钟内没有新请求服务器就销毁这个 Session。不设置的话默认取 Tomcat 的 web.xml 里配置值通常是 30 分钟。如果你部署后发现用户“用着用着就掉线”先查这一句如果你测试时发现每次重启 Tomcat 后 Session 就没了那是正常的因为 Session 对象本来就在服务器内存里。4.2 商品列表与分页PreparedStatement 的参数化查询商品列表页是蛋糕商城访问量最大的页面。我一般给首页做一个简单的分页每页 6 个商品用pageNum和pageSize两个参数控制。CakeDao 的查询方法长这样public ListCake findPage(int pageNum, int pageSize) { String sql SELECT * FROM tb_cake WHERE is_deleted 0 ORDER BY id ASC LIMIT ? OFFSET ?; ListCake list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, (pageNum - 1) * pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Cake cake new Cake(); cake.setId(rs.getInt(id)); cake.setName(rs.getString(name)); cake.setPrice(rs.getBigDecimal(price)); cake.setImageUrl(rs.getString(image_url)); list.add(cake); } } } catch (Exception e) { throw new RuntimeException(分页查询失败, e); } }这里必须用 PreparedStatement 而不是 Statement两个理由。第一是防 SQL 注入用户搜索蛋糕名称时输入 OR 11这类字符串Statement 拼 SQL 会让条件恒真查出全表数据PreparedStatement 把参数和 SQL 模板分开编译参数永远被当成普通字符串处理。第二是写起来更省事你不需要自己拼接引号。LIMIT ? OFFSET ?是 MySQL 的分页语法LIMIT 是每页条数OFFSET 是跳过多少条。前端传来的 pageNum 是第几页用户在第 1 页时 OFFSET 是 0第 2 页时 OFFSET 是 6所以计算式是(pageNum - 1) * pageSize。很多新手在这里直接传 pageNum 进去导致第一页和第二页数据重叠。4.3 购物车放 Session 还是放数据库搜索“JavaWeb 蛋糕商城购物车”时你会看到两种做法用数据库建购物车表或者用 Session 存购物车对象。课程设计阶段我强烈建议用 Session原因很直接用户没有登录也能浏览商品、加购如果购物车放数据库你就要给每个未登录用户生成一个临时 token还要设过期时间Token 串存 Cookie每次请求来回校验整套流程工作量比购物车本身还大。购物车的数据结构用HashMap就够WebServlet(/cart/add) public class CartServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int cakeId Integer.parseInt(req.getParameter(cakeId)); CakeDao cakeDao new CakeDao(); Cake cake cakeDao.findById(cakeId); HttpSession session req.getSession(); // 购物车对象整体存进 Sessionkey是蛋糕ID MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new HashMap(); } CartItem item cart.get(cakeId); if (item null) { cart.put(cakeId, new CartItem(cake, 1)); } else { item.setQuantity(item.getQuantity() 1); } session.setAttribute(cart, cart); resp.sendRedirect(req.getContextPath() /cart.jsp); } }CartItem 是一个简单的 POJO持有 Cake 对象和 quantity 数量字段。用MapInteger, CartItem的好处是key 是蛋糕 ID加购同一款蛋糕时直接命中数量加 1不会出现购物车列表里同一款蛋糕两行记录结算时遍历 map 的 values 就能拿到购物车条目。Session 购物车的边界问题也暴露得很清楚用户关掉浏览器Session 销毁购物车清空。这不是缺陷你可以在答辩时主动说出这个局限并补一句“如果要实现跨会话保存购物车可以扩展一张购物车表用 user_id 关联本设计按课程要求采用 Session 方案”这就把一个局限说成了你的设计权衡。4.4 订单提交与库存扣减一个事务方法的正确姿势单笔订单涉及三个写操作往 tb_orders 插主表记录、往 tb_order_item 插明细、把蛋糕库存减掉对应数量。这三个操作要么全部成功、要么全部失败不能出现“订单生成了但库存没扣”的脏数据。处理方式是给它们套上同一个数据库事务我把它放在 OrdersService 层public void createOrder(Orders order, ListCartItem items) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启手动事务 OrdersDao ordersDao new OrdersDao(); OrderItemDao itemDao new OrderItemDao(); CakeDao cakeDao new CakeDao(); int orderId ordersDao.insert(conn, order); for (CartItem item : items) { itemDao.insert(conn, orderId, item); int rows cakeDao.reduceStock(conn, item.getCakeId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足); } } conn.commit(); // 全部成功提交 } catch (Exception e) { if (conn ! null) { conn.rollback(); // 任何一步失败全部回滚 } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { conn.setAutoCommit(true); // 恢复自动提交 conn.close(); // 连接归还给 Druid 连接池事务状态必须复原 } } }关键点有三个。setAutoCommit(false)之前先说清楚MySQL 默认每条 SQL 自动提交如果不开事务订单插入成功、明细插入失败时订单主记录已经落库无法撤回。只有把自动提交关掉commit之前的所有 SQL 都不会真正写入磁盘。reduceStock返回 int 是受影响的记录数这是判断库存是否足够的可靠手段。UPDATE 语句SET stock stock - ? WHERE id ? AND stock ?如果库存不足条件不成立更新影响行数为 0方法返回 0事务回滚。你不用先把库存查出来在 Java 里做减法再更新那个操作非原子高并发下两个请求同时读到库存 3各自减 2最后库存变成 -1。finally 块里setAutoCommit(true)经常被忽略。Druid 连接池里的 Connection 是复用的如果这次事务结束时保持手动提交状态连接还回池子里下次其他线程拿到这个连接时还是手动提交所有 SQL 都不生效这种 bug 极难排查。事务边界是 JavaWeb 项目里最值得多花十分钟检查的地方。5. IDEA 运行 JavaWeb 项目避坑清单5 条翻车记录与救命解法5.1 现象IDEA 能启动 Tomcat浏览器却 404Tomcat 控制台显示启动成功但浏览器访问http://localhost:8080/一直 404这是 JavaWeb 新手第一坑。原因九成出在 Deployment 配置上你只添加了 Tomcat Server没有在 Deployment 标签页添加 Artifact。没有 ArtifactTomcat 启动后 webapps 里是空的自然没有任何页面可以访问。另一个常见原因是选中了带:war后缀的 Artifact。IDEA 里 Artifact 有两种cake-shop:war是打包成 war 文件需要额外配置外部 Tomcat 的部署路径cake-shop:war exploded是直接把编译输出目录映射进去。调试阶段必须选 exploded并把 Application context 改成/。如果你两个都加了Tomcat 会尝试同时部署两份后启动的那个覆盖先启动的也会出现页面时好时坏。解决路径只有一条Run - Edit Configurations选中你的 Tomcat打开 Deployment 标签页清空现有 Artifact 列表重新点加号选cake-shop:war explodedApplication context 填/然后重启。注意启动后左下角弹出的“Deployment is available at”提示它显示的路径就是你实际访问的根路径。5.2 现象JSP 页面中文全是问号或乱码JSP 里的中文显示成????或者一堆乱码原因涉及三层编码。第一层是 JSP 文件本身的编码文件开头要写% page pageEncodingUTF-8 %IDEA 右下角显示的文件编码也必须是 UTF-8不能是 GBK。第二层是响应头编码同一个 page 指令里要加contentTypetext/html; charsetUTF-8告诉浏览器用 UTF-8 解码。两层必须同时写少了哪一个都会乱码。第三层是 GET 请求参数的中文乱码。用户搜索“芝士蛋糕”请求到达 Servlet 时参数已经乱掉你req.setCharacterEncoding(UTF-8)也没用因为这个方法是管 POST 表单的。Tomcat 8.5 之后对 GET 请求的 URI 默认用 UTF-8 解码如果你用的是 Tomcat 7 或更早版本需要在 Tomcat 的 server.xml 里给 Connector 加URIEncodingUTF-8或者升级 Tomcat。与其折腾不如直接记住中文项目一律用 Tomcat 8.5 以上编码标准统一成 UTF-8不要动 Tomcat 配置。5.3 现象CSS/JS 全部 404接口却正常项目里明明放了 css/style.cssJSP 里用link relstylesheet hrefcss/style.css引用浏览器却报 404但 Servlet 接口访问正常。这个问题九成出现在你加了登录过滤器之后。LoginFilter 的拦截路径写成/*把静态资源的请求也拦截了过滤器里发现用户未登录直接重定向到 login.jsp。解决方向不是给静态资源加白名单而是让 Spring 式的思维走开——JavaWeb 里你本来就要手动处理。两个方案选一个第一种是过滤器里判断请求路径request.getRequestURI().contains(/css/)就直接filterChain.doFilter放行第二种是把 css、js 目录访问约定放到/static/前缀下过滤器直接放行前缀为/static/的请求。我通常用第二种因为判断前缀比逐个后缀判断干净。还有一个隐蔽点JSP 页面里相对路径容易写错。首页在index.jsp它引用css/style.css没问题但页面跳转到/product/detail?id3这种深路径后浏览器 URL 变成了http://localhost:8080/product/detail相对路径css/style.css会被解析成http://localhost:8080/product/css/style.css自然 404。所有 JSP 里引用静态资源一律写href${pageContext.request.contextPath}/css/style.css。5.4 现象MySQL 8 连接报 Public Key Retrieval is not allowed数据库连接池启动时报Public Key Retrieval is not allowed原因是 MySQL 8.0 默认使用 caching_sha2_password 认证插件客户端第一次连服务器需要获取公钥做密码加密传输JDBC 驱动出于安全考虑默认不允许自动获取。druid.properties 里把 allowPublicKeyRetrieval 设置为 true并强制指定驱动类名driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/cake_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse usernameroot password你的密码 initialSize5 maxActive20serverTimezoneAsia/Shanghai也是必需品MySQL 8 连接字符串不指定时区JDBC 驱动会抛异常连接直接失败。useSSLfalse是关闭 SSL 加密本地开发环境没必要开。这三个参数是在 JavaWeb 项目里跑 MySQL 8 的标准配置缺一个都有对应报错。5.5 现象Tomcat 10 启动后所有 Servlet 都报 ClassNotFound这个坑最阴险。你下载了 Tomcat 10项目能启动但一访问 Servlet 就报ClassNotFoundException: javax.servlet.http.HttpServlet或者所有用了 Servlet 的类全部编译失败。原因Tomcat 10 从javax.servlet迁移到了jakarta.servlet命名空间所有传统的 JavaWeb 案例代码用的是 javax 包在 Tomcat 10 里完全不可用。解决方式只有一句话换 Tomcat。课程设计项目一律用 Tomcat 8.5 或 9.0不要用任何标注 10 以上的版本。IDEA 配置的 Core Tomcat 版本是从本地目录读取的你下载 Tomcat 10 后配置哪怕写对代码里的 javax 也改不动。黑马笔记和大部分网上的 JavaWeb 案例全都基于 javax你在 Tomcat 9 下运行它们不会有任何问题。还有一点不要试图把代码里的 javax 批量替换成 jakarta会有大量第三方标签库不一致属于给自己挖坑。6. 交作业前的最后一小时验收演练与三个加分改动6.1 验收演练脚本从启动到下单 5 分钟走完很多项目当时写着没问题演示前一紧张就翻车。我的做法是准备一条固定的验收脚本每次改动后都走一遍。第一条路径是用户侧注册新用户、登录、进入首页、点进蛋糕详情、加购两个商品、购物车调整数量、提交订单、去模拟支付修改订单状态第二条路径是管理员侧用 admin 账号登录、新增一个蛋糕分类、上传蛋糕图片、上架蛋糕、把测试订单标记为已发货第三条路径是边界验证未登录直接访问购物车页面、清空购物车后提交订单、把某款蛋糕库存改成 1 再下两单。演示时老师最常问的三个问题是为什么密码要 MD5 加密、购物车为什么放 Session 不放数据库、订单金额为什么不直接查蛋糕表。这三个问题的答案在上文对应的设计说明里你用自己的话讲一遍比背概念有用。回答时要主动展示你的设计权衡——比如当面说“订单金额冗余在订单表里因为商品价格会变历史订单必须保留当时的价格快照”这比被动答问题更容易拿分。6.2 三个加分改动图片虚拟路径、定时关单、订单号生成基础功能之外我建议做这三个改动工作量和收益比最高。第一个改动是图片上传和访问。把图片文件上传到你电脑的某个磁盘目录比如D:/uploads而不是塞进项目里然后在 IDEA 的 Tomcat 配置里加一个 Deployment - External Source把D:/uploads映射成虚拟路径/upload/。这样蛋糕图片的 URL 可以写成http://localhost:8080/upload/cake1.jpg不占项目资源也不会因为重新部署 Tomcat 把图片清空。第二个改动是订单超时自动取消。用一个ScheduledExecutorService每分钟扫描一次订单表把超过 30 分钟仍未支付的订单状态改成 4已取消。课程设计阶段不引入 Quartz 这类框架用 JDK 自带定时器就够了配置起来也就 20 行代码。这个功能在答辩时很有说服力因为它体现你对“状态流转”和“定时任务”的理解而且是很多教程案例里没有的。第三个改动是订单号生成策略。新手喜欢直接用数据库自增 ID 当订单号但订单号在页面上展示用户会从序号猜测你们的单量。常见做法是用时间戳加随机数DateTimeFormatter.ofPattern(yyyyMMddHHmmss).format(LocalDateTime.now())拼上ThreadLocalRandom.current().nextInt(1000, 9999)生成 17 位订单号写入 order_no 字段。自增 ID 留在表里当主键对外统一用 order_no这个设计在真实项目里也是标配。我的习惯是每次交作业前把项目重新打包成 war放到独立 Tomcat 的 webapps 里启动一次确认不是“只在 IDEA 里会跑”。这样做能提前暴露绝对路径写死、依赖缺失、Druid 配置读不到这类部署期问题避免演示时只剩尴尬的 CPU 占用率告诉老师你真的点了启动按钮。这套 JavaWeb 蛋糕商城从选型、建表到部署的完整链路照着走一遍希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →