简介:这是一套基于JSP与Servlet技术栈实现的JavaWeb蛋糕店售卖网站完整源码,面向正在学习JavaWeb开发、需要课程设计或毕业设计参考的初学者与进阶开发者。项目采用IDEA 2017.3.5开发,运行于Tomcat 8.5.35,数据库为MySQL 5.7,并引入c3p0连接池与DBUtil工具类,技术选型贴近企业常见实践。压缩包共290个文件,约19.11MB,其中58个jsp页面承载前后台视图,55个java文件实现业务逻辑,另有20个xml配置、14个js脚本、12个css样式及82张jpg商品图等静态资源,结构完整。功能覆盖前台商品推荐与分类展示、购物车增删改、用户注册登录、个人信息与收货地址维护、下单付款、订单查询及关键字搜索;后台支持订单状态流转、用户管理、商品类目与商品信息的增删改查。目前已有127人学习,适合对照源码理解MVC分层、数据库连接池配置与前后台交互流程,快速搭建可运行的电商练手项目。
1. 蛋糕店售卖网站:为什么 JSP + Servlet 至今仍是毕设最稳的起点
如果你正在为毕设选题发愁,或者刚学完 JavaWeb 想找一个能跑通全流程的项目练手,蛋糕店售卖网站是个被低估的选择。它不像商城那样庞大到无从下手,也不像博客那样单薄到撑不起论文。商品展示、分类浏览、购物车、下单、后台管理——电商的核心链路一个不少,但业务边界清晰,用 JSP + Servlet 就能完整实现,不需要被 Spring 全家桶的配置淹没。
我见过太多人一上来就冲 SpringBoot,结果光依赖版本冲突就耗掉一周,业务代码一行没写。JSP + Servlet 的好处在于:请求怎么进来、参数怎么取、页面怎么渲染,每一步都是透明的。你写的每一行代码都能在浏览器里看到直接反馈,这对建立信心太重要了。这篇文章会从零开始,把蛋糕店网站的环境搭建、数据库设计、核心功能实现、部署打包完整走一遍,中间踩过的坑也会如实交代。适合有 Java 基础、学过 HTML 和 SQL、想用传统技术栈完成一个完整 Web 项目的同学。
2. 环境搭建与项目骨架:从 IDEA 新建 JSP 项目到 Tomcat 跑通第一个页面
2.1 工具链选型:IDEA + Tomcat + MySQL 的版本搭配
工具链的版本搭配是第一个容易翻车的地方。我一般推荐这套组合:JDK 8 或 JDK 11、Tomcat 8.5 或 9.0、MySQL 5.7 或 8.0、IntelliJ IDEA 社区版或旗舰版都行。为什么不用 JDK 17+?因为 Tomcat 9 以下的版本对高版本 JDK 的模块化系统支持不完善,容易出ClassNotFoundException或者IllegalAccessError。Tomcat 10 虽然支持 JDK 17,但它把javax.servlet包名改成了jakarta.servlet,网上绝大多数教程和代码片段还是javax的写法,新手照着敲会大面积报错。
MySQL 8.0 和 5.7 的区别主要在驱动类名和连接 URL 参数上。8.0 用com.mysql.cj.jdbc.Driver,URL 需要加时区参数serverTimezone=Asia/Shanghai;5.7 用com.mysql.jdbc.Driver,不加时区也能跑。我建议直接用 8.0,因为新装环境基本都是 8.0,没必要降级。
IDEA 新建项目时选 “Java Enterprise” 还是 “Java”?如果你用的是旗舰版,选 Java Enterprise 可以直接勾选 Web Application 模板,自动生成web.xml和目录结构。社区版没有这个模板,需要手动创建src/main/webapp/WEB-INF目录和web.xml文件。手动创建也不复杂,但要注意目录层级不能错。
2.2 目录结构与 web.xml 配置:3 个必须检查的路径
标准 Maven Web 项目的目录结构是这样的:
cake-shop/ ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/cake/ │ │ │ ├── dao/ │ │ │ ├── entity/ │ │ │ ├── servlet/ │ │ │ └── util/ │ │ ├── resources/ │ │ └── webapp/ │ │ ├── WEB-INF/ │ │ │ └── web.xml │ │ ├── css/ │ │ ├── js/ │ │ ├── images/ │ │ └── index.jsp三个必须检查的路径:第一,webapp目录必须在src/main/下,不是src/下;第二,WEB-INF目录名必须全大写,里面的web.xml文件名也必须全小写;第三,JSP 文件放在webapp根目录或子目录下,不要放在WEB-INF里面,否则浏览器无法直接访问。
web.xml的版本声明也要注意。如果用 Servlet 4.0(Tomcat 9),头部的web-app标签版本写4.0;如果用 Servlet 3.1(Tomcat 8.5),写3.1。写错了 Tomcat 启动时会报解析错误。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>这段配置只做了一件事:指定index.jsp为默认首页。welcome-file-list里的文件名要和webapp根目录下的实际文件名完全一致,大小写敏感。
2.3 pom.xml 依赖:servlet-api 和 mysql-connector 的 scope 陷阱
Maven 项目的pom.xml里,Servlet API 的依赖必须加providedscope。因为 Tomcat 运行时自带 Servlet API 的 jar 包,如果你打成compilescope,部署时会和 Tomcat 自带的包冲突,报ClassCastException或者NoSuchMethodError。
<dependencies> <!-- Servlet API:scope 必须是 provided --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSP API:同样 provided --> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.3</version> <scope>provided</scope> </dependency> <!-- MySQL 驱动:scope 是 compile,打包时要带上 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- JSTL 标签库:用于 JSP 页面循环和判断 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> </dependencies>JSTL 的依赖容易被忽略。如果你在 JSP 里用<c:forEach>标签循环展示蛋糕列表,没有这个依赖会报The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved。注意 JSTL 1.2 的 groupId 是javax.servlet,不是javax.servlet.jsp.jstl,后者是旧版本。
2.4 在 IDEA 里配置 Tomcat 并跑通第一个 JSP 页面
IDEA 旗舰版配置 Tomcat 的入口在 Run → Edit Configurations → 左上角 + → Tomcat Server → Local。关键配置项有三个:Application server 指向你解压的 Tomcat 目录;HTTP port 默认 8080,如果被占用改成 8081;Deployment 标签页里点 + → Artifact,选择cake-shop:war exploded。war exploded模式支持热部署,改完 JSP 不用重启 Tomcat 就能看到效果。
社区版没有 Tomcat Server 选项,需要装 Smart Tomcat 插件。装完后在 Run → Edit Configurations 里选 Smart Tomcat,配置 Tomcat 路径和 webapp 目录。
配置完成后,在webapp下新建index.jsp,写一行<h1>蛋糕店首页</h1>,点运行。浏览器打开http://localhost:8080/cake-shop/,看到标题就说明环境通了。如果报 404,检查 Deployment 里的 Application context 是不是/cake-shop,访问 URL 要带上这个前缀。如果报 500,看 IDEA 控制台的异常堆栈,大概率是web.xml版本声明和 Tomcat 版本不匹配。
3. 数据库设计与实体建模:蛋糕品类、订单状态和用户角色的表结构怎么定
3.1 五张核心表:从蛋糕商品到订单明细的字段设计
蛋糕店网站的数据库不需要太复杂,五张表就能覆盖核心业务:用户表、蛋糕商品表、商品分类表、订单表、订单明细表。我一般会再加一张管理员表,但为了简化,可以把管理员和普通用户合并到一张表里,用role字段区分。
CREATE DATABASE cake_shop DEFAULT CHARACTER SET utf8mb4; USE cake_shop; -- 用户表:role 字段区分普通用户和管理员 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, phone VARCHAR(20), address VARCHAR(200), role TINYINT DEFAULT 0 COMMENT '0-普通用户 1-管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ); -- 蛋糕商品表 CREATE TABLE cake ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image VARCHAR(200), description TEXT, status TINYINT DEFAULT 1 COMMENT '1-上架 0-下架', FOREIGN KEY (category_id) REFERENCES category(id) ); -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0-待付款 1-已付款 2-已发货 3-已完成 4-已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ); -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, cake_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (cake_id) REFERENCES cake(id) );几个字段设计的细节值得展开。price用DECIMAL(10,2)而不是FLOAT或DOUBLE,因为浮点数在金额计算时会出现精度丢失,0.1 + 0.2 不等于 0.3 这种问题在订单金额上绝对不能出现。orders表名用复数,因为order是 SQL 关键字,建表时不报错但查询时容易出问题。订单状态用TINYINT加注释,比用字符串存储更省空间,查询效率也更高。
3.2 实体类与 DAO 层:JDBC 工具类的封装与连接池的取舍
实体类就是和数据库表一一对应的 Java 类,字段名用驼峰命名对应下划线的列名。比如category_id对应categoryId,create_time对应createTime。手写 getter/setter 太累,IDEA 里 Alt + Insert 可以自动生成。
DAO 层负责所有数据库操作。最基础的做法是写一个DBUtil工具类,封装获取连接、关闭资源的方法:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/cake_shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 依次关闭,每个都 try-catch 防止前面的异常影响后面的关闭 try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这个工具类每次调用getConnection()都会新建一个物理连接,性能不高但够用。如果要做连接池,可以用 Druid 或 HikariCP,但配置复杂度会上升。毕设项目用原生 JDBC 完全够,答辩时老师也不会因为没用连接池扣分。注意 URL 里的useSSL=false和serverTimezone=Asia/Shanghai,MySQL 8.0 不加这两个参数会报连接错误。
3.3 分页查询:LIMIT 偏移量计算和总数统计的两个 SQL
蛋糕列表页需要分页,每页显示 8 个蛋糕。分页查询需要两条 SQL:一条查当前页的数据,一条查总记录数。
// 查询当前页数据 public List<Cake> findByPage(int page, int pageSize) { List<Cake> list = new ArrayList<>(); String sql = "SELECT * FROM cake WHERE status = 1 ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); // 偏移量 ps.setInt(2, pageSize); 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.setImage(rs.getString("image")); list.add(cake); } } catch (SQLException e) { e.printStackTrace(); } return list; } // 查询总记录数 public int countAll() { String sql = "SELECT COUNT(*) FROM cake WHERE status = 1"; 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; }LIMIT的第一个参数是偏移量,计算公式是(当前页码 - 1) * 每页条数。页码从 1 开始,第 1 页偏移量是 0,第 2 页偏移量是 8,以此类推。总页数用Math.ceil(totalCount * 1.0 / pageSize)计算,注意要转成浮点数再除,否则整数除法会丢精度。比如总数 17 条,每页 8 条,17 / 8 = 2,但实际需要 3 页。
4. Servlet 请求处理与 JSP 页面渲染:从商品列表到购物车的完整链路
4.1 Servlet 生命周期:init、service、destroy 在蛋糕店项目里的实际体现
Servlet 的生命周期分三个阶段:init()在 Servlet 第一次被访问时调用一次,service()每次请求都调用,destroy()在 Tomcat 关闭时调用。在蛋糕店项目里,CakeListServlet的init()可以用来加载数据库驱动或者初始化缓存,但大多数情况下不需要重写。service()方法会根据请求方式(GET/POST)分发到doGet()或doPost(),我们实际写业务逻辑是在这两个方法里。
一个容易踩的坑:Servlet 是单例的,多个请求共享同一个 Servlet 实例。所以不要在 Servlet 里定义成员变量来存请求相关的数据,否则并发时会串数据。所有请求相关的变量都必须在方法内部声明为局部变量。
@WebServlet("/cake/list") public class CakeListServlet extends HttpServlet { // 不要在这里定义 request 相关的成员变量 @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取页码参数,默认第 1 页 String pageStr = request.getParameter("page"); int page = (pageStr == null || pageStr.isEmpty()) ? 1 : Integer.parseInt(pageStr); int pageSize = 8; // 2. 调用 DAO 查询数据 CakeDao cakeDao = new CakeDao(); List<Cake> cakeList = cakeDao.findByPage(page, pageSize); int totalCount = cakeDao.countAll(); int totalPages = (int) Math.ceil(totalCount * 1.0 / pageSize); // 3. 把数据存入 request 域,转发给 JSP request.setAttribute("cakeList", cakeList); request.setAttribute("currentPage", page); request.setAttribute("totalPages", totalPages); // 4. 转发到 JSP 页面渲染 request.getRequestDispatcher("/cake_list.jsp").forward(request, response); } }@WebServlet("/cake/list")注解代替了web.xml里的<servlet>和<servlet-mapping>配置,更简洁。访问路径是http://localhost:8080/cake-shop/cake/list?page=2。request.setAttribute()存的数据只在当前请求的转发过程中有效,转发用forward(),浏览器地址栏不变。如果用sendRedirect()重定向,request 域的数据会丢失。
4.2 JSP 页面用 JSTL 循环渲染蛋糕列表:c:forEach 和 c:if 的配合
JSP 里尽量避免写 Java 代码块<% %>,用 JSTL 标签和 EL 表达式更清晰。展示蛋糕列表用<c:forEach>循环:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head><title>蛋糕列表</title></head> <body> <div class="cake-grid"> <c:forEach items="${cakeList}" var="cake"> <div class="cake-card"> <img src="${pageContext.request.contextPath}/images/${cake.image}" alt="${cake.name}"> <h3>${cake.name}</h3> <p class="price">¥<fmt:formatNumber value="${cake.price}" pattern="#.00"/></p> <a href="${pageContext.request.contextPath}/cake/detail?id=${cake.id}">查看详情</a> </div> </c:forEach> </div> <!-- 分页导航 --> <div class="pagination"> <c:if test="${currentPage > 1}"> <a href="${pageContext.request.contextPath}/cake/list?page=${currentPage - 1}">上一页</a> </c:if> <span>第 ${currentPage} / ${totalPages} 页</span> <c:if test="${currentPage < totalPages}"> <a href="${pageContext.request.contextPath}/cake/list?page=${currentPage + 1}">下一页</a> </c:if> </div> </body> </html>${pageContext.request.contextPath}获取的是项目部署的上下文路径,也就是/cake-shop。所有链接和静态资源路径都要带上它,否则部署到 Tomcat 后路径会 404。<fmt:formatNumber>用来格式化价格,pattern="#.00"保证显示两位小数。<c:if>控制上一页/下一页按钮的显示条件,第一页不显示上一页,最后一页不显示下一页。
4.3 购物车实现:Session 存储 vs 数据库存储的选型对比
购物车有两种实现方式:存在 Session 里或者存数据库。Session 方式简单,不需要建表,但用户换浏览器或清 Cookie 后购物车就丢了。数据库方式持久化好,但每次增删改查都要操作数据库,代码量大。
对于蛋糕店项目,我建议用 Session 存购物车。因为毕设演示时不会涉及换设备登录的场景,Session 足够用,而且代码量少很多。购物车的结构可以用Map<Integer, Integer>,key 是蛋糕 ID,value 是数量。
@WebServlet("/cart/add") public class CartAddServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int cakeId = Integer.parseInt(request.getParameter("cakeId")); int quantity = Integer.parseInt(request.getParameter("quantity")); // 从 Session 获取购物车,没有就新建 HttpSession session = request.getSession(); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); } // 如果购物车里已有该商品,数量累加;否则新增 cart.put(cakeId, cart.getOrDefault(cakeId, 0) + quantity); session.setAttribute("cart", cart); response.sendRedirect(request.getContextPath() + "/cart/list"); } }getOrDefault(cakeId, 0)是 Java 8 的方法,如果 key 不存在返回默认值 0。sendRedirect()重定向到购物车列表页,防止用户刷新页面时重复提交。注意重定向的路径也要带request.getContextPath()。
4.4 订单提交:事务处理与库存扣减的原子性保证
下单是最容易出 bug 的环节。用户点击提交订单后,需要做三件事:往orders表插一条订单记录、往order_item表插多条明细、扣减cake表的库存。这三步必须在一个事务里完成,任何一步失败都要回滚。
public boolean createOrder(int userId, Map<Integer, Integer> cart) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 计算总价并插入订单 BigDecimal totalPrice = BigDecimal.ZERO; for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { Cake cake = getCakeById(conn, entry.getKey()); totalPrice = totalPrice.add(cake.getPrice().multiply(new BigDecimal(entry.getValue()))); } int orderId = insertOrder(conn, userId, totalPrice); // 2. 插入订单明细并扣减库存 for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { int cakeId = entry.getKey(); int qty = entry.getValue(); // 扣库存时加 stock >= qty 条件,防止超卖 String sql = "UPDATE cake SET stock = stock - ? WHERE id = ? AND stock >= ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, qty); ps.setInt(2, cakeId); ps.setInt(3, qty); if (ps.executeUpdate() == 0) { conn.rollback(); return false; // 库存不足,回滚 } } insertOrderItem(conn, orderId, cakeId, qty); } conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { if (conn != null) conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }关键点在UPDATE cake SET stock = stock - ? WHERE id = ? AND stock >= ?。stock >= ?这个条件保证了库存不会被扣成负数。如果executeUpdate()返回 0,说明库存不足,立即回滚。所有数据库操作共用同一个Connection,不能从DBUtil重新获取连接,否则事务不生效。
5. 避坑与排查:JSP + Servlet 项目里最容易翻车的 5 个地方
5.1 中文乱码:POST 请求和 GET 请求的解决方式不一样
现象:表单提交后,数据库里存的中文变成???或者乱码。
原因:Tomcat 8 以上默认用 UTF-8 解码 GET 请求,但 POST 请求体默认用 ISO-8859-1 解码。JSP 页面如果没声明pageEncoding,编译时也会用平台默认编码。
解决:POST 请求在 Servlet 第一行加request.setCharacterEncoding("UTF-8");GET 请求需要在 Tomcat 的server.xml里给 Connector 加URIEncoding="UTF-8"。JSP 页面头部必须写<%@ page contentType="text/html;charset=UTF-8" language="java" %>。数据库连接 URL 加characterEncoding=utf8,建表时用utf8mb4字符集。
5.2 404 错误:路径拼写、web.xml 版本和 Artifact 配置的三重检查
现象:浏览器访问某个 Servlet 或 JSP 时返回 404。
原因:路径拼写错误、web.xml版本声明和 Tomcat 不匹配、IDEA 的 Artifact 没包含新文件。
解决:先检查 URL 是否带了正确的上下文路径(/cake-shop),再检查@WebServlet注解的路径和访问路径是否一致。如果web.xml头部版本写的是 4.0 但 Tomcat 是 8.5,改成 3.1。IDEA 里 File → Project Structure → Artifacts,确认war exploded里包含了webapp目录下的所有文件。改完 Artifact 后需要重新部署。
5.3 数据库连接失败:驱动类名、时区和 SSL 参数的常见错误
现象:Tomcat 启动时报Communications link failure或Unknown database。
原因:MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,不是com.mysql.jdbc.Driver;连接 URL 没加serverTimezone会报时区错误;MySQL 8.0 默认要求 SSL 连接,不加useSSL=false会报 SSL 警告。
解决:URL 写成jdbc:mysql://localhost:3306/cake_shop?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8。确认 MySQL 服务已启动,数据库cake_shop已创建,用户名密码正确。如果用的是 MySQL 5.7,驱动类名用com.mysql.jdbc.Driver,URL 可以不加时区参数。
5.4 JSTL 标签不解析:uri 写错和 jar 包冲突
现象:JSP 页面直接显示<c:forEach>标签源码,或者报The absolute uri cannot be resolved。
原因:taglib的 uri 写错,或者 JSTL 的 jar 包没引入,或者和 Tomcat 自带的 jar 包冲突。
解决:JSTL 1.2 的 uri 是http://java.sun.com/jsp/jstl/core,不是http://java.sun.com/jstl/core。pom.xml里加javax.servlet:jstl:1.2依赖。如果还不行,检查WEB-INF/lib下有没有jstl-1.2.jar和standard-1.1.2.jar,有的话删掉,让 Maven 统一管理。
5.5 事务不回滚:Connection 不是同一个,自动提交没关
现象:下单时库存扣了但订单没生成,或者订单生成了但库存没扣。
原因:DAO 方法内部各自从DBUtil.getConnection()获取了新连接,不在同一个事务里;或者忘了conn.setAutoCommit(false)。
解决:把Connection作为参数传给所有 DAO 方法,确保整个事务共用一个连接。setAutoCommit(false)必须在第一条 SQL 执行前调用。commit()之后要setAutoCommit(true)恢复默认状态,否则连接归还连接池后会影响后续使用。
6. 打包部署与进阶技巧:war 包生成、Tomcat 部署和几个提效习惯
项目开发完后需要打包成 war 文件部署到 Tomcat。Maven 项目在pom.xml里加<packaging>war</packaging>,然后执行mvn clean package,target目录下会生成cake-shop.war。把 war 文件复制到 Tomcat 的webapps目录下,启动 Tomcat 会自动解压部署。访问地址是http://localhost:8080/cake-shop/。
如果不想每次手动复制,可以在 IDEA 的 Maven 面板里双击package,然后在 Run/Debug Configurations 的 Tomcat 配置里,Deployment 标签页选择war而不是war exploded。这样每次运行都会自动打包部署。
几个提效习惯值得养成。第一,把常用的数据库操作封装成BaseDao泛型类,增删改查的通用逻辑写一次,子类只需要提供 SQL 和参数映射。第二,JSP 页面里的公共部分(头部导航、底部版权)抽成header.jsp和footer.jsp,用<%@ include file="header.jsp" %>引入。第三,Servlet 里获取参数的代码抽成工具方法,比如getIntParam(request, "page", 1),避免到处写Integer.parseInt和空值判断。
最后一个血泪教训:数据库密码不要硬编码在DBUtil里。我一般会写一个db.properties放在src/main/resources下,用Properties类加载。这样换环境时只改配置文件,不用重新编译 Java 代码。虽然毕设项目没人会攻击你,但这个习惯从开始就养成,以后做真实项目会省很多事。
希望帮到你。
本文还有配套的精品资源,点击获取