
简介这是一套基于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负责交互与样式并附带1个sql脚本用于快速建库。功能覆盖前台商品推荐与分类展示、购物车增删改、用户注册登录、个人信息与收货地址维护、下单付款及订单查询、关键字搜索后台支持订单状态流转、用户管理、商品类目与商品信息的增删改查。目前已有127人学习下载适合作为全栈入门练手与二次开发的基础工程。1. 蛋糕店售卖网站为什么 JSP Servlet 至今仍是毕设最稳的起手式打开招聘软件搜 Java 后端满屏 Spring Boot但打开学校毕设选题表JSP Servlet 依然是出现频率最高的关键词之一。这不是技术倒退而是场景匹配——蛋糕店售卖网站这类中小型 Web 项目业务逻辑清晰、数据表不超过十张、并发量几乎为零用 JSP Servlet 反而比 Spring Boot 更容易讲清楚「请求从浏览器到数据库再回到页面」的完整链路。答辩老师问「你的请求怎么走的」你能从web.xml一路讲到doGet再讲到 JDBC 查询每一层都是自己写的没有黑盒。这篇文章面向正在做 JSP 毕设、或者刚学完 Servlet 想找个完整项目练手的人把蛋糕店售卖网站从建表到下单跑通的全过程拆开讲包括环境配置、代码结构、参数设置和那些只有真正跑过才会遇到的坑。2. 环境搭建与项目骨架IDEA 新建 JSP 项目到 Tomcat 跑通2.1 IDEA 新建 JavaWeb 项目与目录结构约定很多人卡在第一步不是代码写不出来而是 IDEA 里新建的项目结构跟教程对不上。IDEA 旗舰版有内置的 Java Enterprise 模板社区版没有这是第一个分叉点。我一般用 Maven 的webapp骨架来建这样不管哪个版本 IDEA 都能跑而且后面打包 war 也顺。新建项目时选择 Maven勾选Create from archetype找到org.apache.maven.archetypes:maven-archetype-webapp。建完之后目录长这样cake-shop/ ├── pom.xml └── src/ └── main/ ├── java/ # 自己建放 Servlet 和 DAO ├── resources/ # 自己建放数据库配置 └── webapp/ ├── WEB-INF/ │ └── web.xml ├── css/ ├── js/ └── index.jspjava和resources两个目录 Maven 骨架不会自动生成需要手动创建并在 IDEA 里标记为 Sources Root 和 Resources Root。这一步不做后面 Servlet 编译报「找不到符号」是必然的。pom.xml里需要加三个依赖Servlet API、JSP API、MySQL 驱动。Servlet API 的 scope 要设成provided因为 Tomcat 自带打包进 war 会冲突。dependencies !-- Servlet APITomcat 已提供scope 必须为 provided -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样由容器提供 -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驱动需要打进 war 包 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies参数说明javax.servlet-api用 4.0.1 对应 Tomcat 9如果你用 Tomcat 10包名已经变成jakarta.servlet所有 import 都要改这是新手最容易翻车的地方。MySQL 驱动 8.x 的类名是com.mysql.cj.jdbc.DriverURL 里必须带时区和 SSL 参数否则启动就报连接失败。2.2 Tomcat 配置与第一个 Servlet 跑通IDEA 里配置 Tomcat 的入口在 Run → Edit Configurations → 加号 → Tomcat Server → Local。关键填三处Tomcat 安装目录、Deployment 里加 Artifact选war exploded、Application context 设成/cake。war exploded和war的区别值得说一句exploded 模式支持热更新改了 JSP 刷新页面就生效改了 Java 代码点一下重新部署就行war 模式每次都要重新打包开发阶段纯属折磨自己。上线才用 war。配好之后写第一个 Servlet 验证链路package com.cake.servlet; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; // WebServlet 替代 web.xml 配置urlPatterns 就是访问路径 WebServlet(/hello) public class HelloServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置响应类型和编码防止中文乱码 resp.setContentType(text/html;charsetUTF-8); resp.getWriter().write(蛋糕店 Servlet 跑通了); } }逻辑说明WebServlet(/hello)注解告诉 Tomcat 这个类处理/cake/hello的请求。继承HttpServlet后只需要覆写doGet或doPost。resp.setContentType必须在getWriter()之前调用顺序反了编码设置不生效中文会变成问号——这个坑我见过至少十个人踩。启动 Tomcat浏览器访问http://localhost:8080/cake/hello看到中文就说明环境通了。看不到就按这个顺序排查Tomcat 日志有没有报错、Artifact 有没有部署、context path 对不对、端口有没有被占用。3. 数据库设计与 DAO 层蛋糕店的数据表怎么建、JDBC 怎么封3.1 蛋糕店核心表结构与建表 SQL蛋糕店售卖网站的数据模型不复杂但表之间的关系要想清楚。核心就四张表用户、蛋糕商品、订单、订单明细。分类可以单独一张表也可以直接在商品表里用字段区分毕设规模我建议直接字段区分少一张表少一堆关联查询。-- 用户表 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), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 蛋糕商品表 CREATE TABLE cake ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category VARCHAR(20) NOT NULL, -- 奶油蛋糕/慕斯/芝士/定制 price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, image VARCHAR(200), description TEXT, status TINYINT DEFAULT 1 -- 1上架 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 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, -- 0待付款 1已付款 2已发货 3已完成 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明字符集必须用utf8mb4蛋糕名称里可能有 emoji 或者特殊字符utf8存不进去。price用DECIMAL(10,2)不用FLOAT金额计算浮点误差是经典翻车点。orders表名用复数是因为order是 SQL 关键字不写反引号直接报语法错误。3.2 JDBC 工具类封装与 DAO 模式每个 Servlet 里都写一遍DriverManager.getConnection是灾难。标准做法是抽一个DBUtil工具类再按表建 DAO 类。package com.cake.util; import java.sql.*; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/cake_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; 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); } // 统一关闭资源顺序ResultSet - Statement - Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明URL 里的serverTimezoneAsia/Shanghai不加会报时区错误useSSLfalse不加会有警告日志。静态代码块保证驱动只加载一次。close方法的关闭顺序不能反先关 ResultSet 再关 Statement 最后关 Connection反了会抛异常。DAO 层以蛋糕查询为例package com.cake.dao; import com.cake.entity.Cake; import com.cake.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class CakeDao { // 查询所有上架蛋糕 public ListCake findAll() { ListCake list new ArrayList(); String sql SELECT * FROM cake WHERE status 1 ORDER BY id DESC; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); 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; } }参数说明用PreparedStatement不用Statement一是防 SQL 注入二是预编译性能更好。try-with-resources自动关闭资源比手动 close 干净。rs.getBigDecimal(price)对应实体类的BigDecimal类型别用getDouble精度会丢。4. Servlet 业务层与 JSP 页面从商品列表到下单的完整链路4.1 Servlet 生命周期与请求分发设计Servlet 的生命周期是面试必问也是理解请求处理的关键。init()在第一次访问时调用一次service()每次请求都调用destroy()在 Tomcat 关闭时调用。实际开发中我们覆写doGet和doPostservice()会自动根据请求方法分发。蛋糕店网站的 Servlet 设计我一般按业务模块分CakeServlet管商品列表和详情CartServlet管购物车OrderServlet管下单和订单查询UserServlet管登录注册。每个 Servlet 内部用action参数区分具体操作避免类爆炸。WebServlet(/cake) public class CakeServlet extends HttpServlet { private CakeDao cakeDao new CakeDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null) action list; switch (action) { case list: listCakes(req, resp); break; case detail: cakeDetail(req, resp); break; default: resp.sendRedirect(cake?actionlist); } } private void listCakes(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListCake cakes cakeDao.findAll(); req.setAttribute(cakes, cakes); // 转发到 JSPrequest 域数据带过去 req.getRequestDispatcher(/cake_list.jsp).forward(req, resp); } private void cakeDetail(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int id Integer.parseInt(req.getParameter(id)); Cake cake cakeDao.findById(id); req.setAttribute(cake, cake); req.getRequestDispatcher(/cake_detail.jsp).forward(req, resp); } }逻辑说明forward是服务器内部跳转URL 不变request 域数据保留sendRedirect是浏览器重新请求URL 变request 域数据丢失。列表页用 forward 因为要把数据带到 JSP操作完成后用 redirect 防止刷新重复提交。4.2 JSP 页面数据渲染与 EL/JSTL 表达式JSP 里写 Java 代码块% %是十年前的做法现在用 EL 表达式${}和 JSTL 标签。需要在pom.xml加 JSTL 依赖JSP 页面顶部引入标签库。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html headtitle蛋糕列表/title/head body h2今日蛋糕/h2 div classcake-grid c:forEach items${cakes} varcake div classcake-card img src${pageContext.request.contextPath}/images/${cake.image} alt${cake.name} h3${cake.name}/h3 p classpricefmt:formatNumber value${cake.price} pattern#,##0.00//p a href${pageContext.request.contextPath}/cake?actiondetailid${cake.id}查看详情/a /div /c:forEach /div /body /html参数说明${pageContext.request.contextPath}拿到项目上下文路径保证部署到不同路径下链接都对。fmt:formatNumber格式化金额保留两位小数。c:forEach的items属性对应 Servlet 里req.setAttribute(cakes, ...)的 key。4.3 购物车与下单的 Session 管理购物车数据存 Session 里不存数据库因为未登录用户也要能加购。Session 的本质是服务器端的一块内存通过 JSESSIONID 这个 Cookie 跟浏览器关联。WebServlet(/cart) public class CartServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); HttpSession session req.getSession(); // 购物车用 MapInteger, Integer 存蛋糕ID - 数量 MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } if (add.equals(action)) { int cakeId Integer.parseInt(req.getParameter(id)); cart.merge(cakeId, 1, Integer::sum); resp.sendRedirect(cart?actionview); } else if (view.equals(action)) { // 根据 cart 里的 ID 查蛋糕详情组装展示数据 ListCartItem items new ArrayList(); BigDecimal total BigDecimal.ZERO; for (Map.EntryInteger, Integer entry : cart.entrySet()) { Cake cake new CakeDao().findById(entry.getKey()); CartItem item new CartItem(cake, entry.getValue()); items.add(item); total total.add(cake.getPrice().multiply( new BigDecimal(entry.getValue()))); } req.setAttribute(items, items); req.setAttribute(total, total); req.getRequestDispatcher(/cart.jsp).forward(req, resp); } } }逻辑说明cart.merge(cakeId, 1, Integer::sum)是 Java 8 的写法如果 key 存在就把值加 1不存在就设为 1。下单时遍历 cart插入 orders 和 order_item 两张表整个过程要放在一个事务里任何一步失败都回滚。5. 避坑与排查JSP Servlet 项目最容易翻车的五个地方5.1 中文乱码POST 和 GET 处理方式不一样现象表单提交中文数据库里存进去是问号或者乱码。原因Tomcat 8 以后 GET 请求默认 URI 编码是 UTF-8但 POST 请求体默认是 ISO-8859-1。很多人只在 JSP 页面写了pageEncodingUTF-8那只管页面本身不管请求参数。解决在 Servlet 的doPost第一行加req.setCharacterEncoding(UTF-8)。GET 请求如果还有乱码检查 Tomcat 的server.xml里 Connector 有没有URIEncodingUTF-8。最省事的做法是写一个 CharacterEncodingFilter在web.xml里配/*拦截所有请求统一设置编码。5.2 数据库连接池缺失导致连接耗尽现象项目跑一段时间后报Too many connections或者每次请求都卡几秒。原因每次请求都DriverManager.getConnection新建连接用完 close 掉。频繁创建销毁连接开销很大而且如果某处忘了 close连接就泄漏了。解决引入 Druid 或 HikariCP 连接池。毕设项目用 Druid 就够了加一个依赖配一个druid.properties把DBUtil.getConnection改成从DataSource拿。连接池的maxActive设 20 就够initialSize设 5。这个改动不大但效果立竿见影答辩时也是个加分项。5.3 JSP 页面报 404路径和部署的两个检查点现象Servlet 写好了访问就是 404。原因九成是路径问题。要么WebServlet的路径和访问 URL 对不上要么 JSP 放在WEB-INF下面直接访问被拦。解决先看 Tomcat 启动日志里有没有Mapping信息确认 Servlet 注册成功。然后检查访问 URL 是不是http://localhost:8080/contextPath/servletPath。WEB-INF下的 JSP 不能直接通过浏览器访问只能通过 Servlet forward 过去这是规范不是 bug。如果用了web.xml配置检查url-pattern和servlet-name有没有对应上。5.4 表单重复提交刷新页面就多一条订单现象下单成功后按 F5 刷新又生成一条一模一样的订单。原因表单提交后 Servlet 用 forward 跳转到结果页浏览器地址栏还是 POST 的 URL刷新就会重新提交。解决提交处理完成后用resp.sendRedirect跳转这是 PRGPost-Redirect-Get模式。如果业务上需要防重可以在 Session 里放一个 token表单提交时带上Servlet 校验后立即删除。毕设项目用 redirect 就够了token 机制可以作为扩展讲。5.5 打包 war 部署到独立 Tomcat 后图片不显示现象IDEA 里跑一切正常打成 war 放到 Tomcat 的webapps下图片全部裂开。原因图片路径写的是绝对路径或者相对于项目根的路径IDEA 里 context path 是/cake独立 Tomcat 部署后 war 包名决定了 context path路径就变了。解决所有静态资源引用统一用${pageContext.request.contextPath}开头。图片上传目录不要放在项目目录里放在 Tomcat 外部的一个固定路径通过配置读取。如果图片是打包进 war 的确认src/main/webapp/images目录下的文件确实被打进去了有时候 Maven 的resources插件配置不对会漏掉。6. 从能跑到能答辩三个让项目看起来更专业的技巧第一个技巧是加一层简单的 Service。很多毕设项目 Servlet 直接调 DAO能跑但显得薄。抽一个CakeService接口加CakeServiceImpl实现Servlet 只依赖 ServiceDAO 只被 Service 调用。这样做的好处是答辩时你可以讲「我用了分层架构Controller-Service-DAO 三层各司其职」而且后面想加缓存或者事务控制改 Service 层就行不用动 Servlet。第二个技巧是把下单逻辑用事务包起来。订单主表和明细表必须同时成功或同时失败这是事务的典型场景。JDBC 手动事务的写法是conn.setAutoCommit(false)所有 SQL 执行完后conn.commit()catch 块里conn.rollback()。注意事务里的 Connection 必须是同一个不能从连接池拿两次。这个点答辩老师很爱问能讲清楚说明你真的理解数据库。第三个技巧是给项目加一个简单的过滤器做登录校验。建一个LoginFilter实现Filter接口在doFilter里判断 Session 里有没有用户信息没有就跳登录页。web.xml里配置拦截/order/*和/cart/*。这样未登录用户访问订单页会被拦住逻辑上更完整。过滤器的chain.doFilter是放行不调用就是拦截这个机制讲清楚也是加分项。我自己做第一个 JSP 项目的时候在中文乱码上耗了一整个下午最后发现是setCharacterEncoding写在了getParameter后面。后来养成的习惯是Servlet 的doPost第一行永远是编码设置第二行才是取参数。这个顺序看起来是小事但能省掉很多莫名其妙的调试时间。希望帮到你。本文还有配套的精品资源点击获取