news 2026/10/22 18:12:43

JSP+Servlet实战:从零搭建蛋糕店售卖网站,吃透JavaWeb核心

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+Servlet实战:从零搭建蛋糕店售卖网站,吃透JavaWeb核心

简介:这份资源是一套基于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人学习,适合对照源码理解MVC分层、连接池配置与前后台权限控制的实现思路。

1. 蛋糕店售卖网站:为什么 JSP + Servlet 这套“老技术栈”还值得动手做一遍

打开招聘软件搜 Java 后端,满屏 Spring Boot、微服务、分布式,但真到课程设计、毕业设计或者公司内部一个轻量级展示型站点,JSP + Servlet 这套组合反而经常被翻出来用。蛋糕店售卖网站就是典型场景:商品列表、详情、购物车、下单、后台管理,功能边界清晰,数据量不大,用最原始的 JavaWeb 三层结构就能跑通,不需要引入一堆中间件。很多人觉得这套东西过时了,但如果你连 Servlet 的生命周期、请求转发和重定向的区别、JSP 九大内置对象都说不清楚,直接上框架也只是在抄配置。这个标题背后真正要解决的问题是:用最少的依赖,把一套完整的电商前台加后台从零搭起来,同时把 JavaWeb 的核心机制吃透。适合在校学生做课设、转行的人补基础、以及需要快速交付一个内部小站点的开发者。下面按实际动手顺序拆开讲,从环境配置到数据库设计再到部署排错,每一步都给出可复现的做法。

2. 环境与项目骨架:IDEA 里跑通第一个 Servlet 需要几步

2.1 JDK、Tomcat、IDEA 的版本搭配与踩坑点

这套技术栈对版本比较敏感,不是随便装一个就能跑。JDK 建议用 8 或 11,Tomcat 用 8.5 或 9.0,IDEA 用 2021 之后的社区版就够。注意 Tomcat 10 开始把 javax.servlet 包名改成了 jakarta.servlet,如果你照着老教程写 import javax.servlet.*,在 Tomcat 10 上直接编译报错。我一般会先确认三件事:java -version输出是不是 1.8 或 11,Tomcat 的 lib 目录下有没有 servlet-api.jar,IDEA 的 Project Structure 里 SDK 和 Language Level 是否一致。常见翻车现场是 JDK 装了 17,Tomcat 用 9,编译时提示“无效的目标发行版”,其实就是 Language Level 没降下来。

配置环境变量时,JAVA_HOME 指向 JDK 根目录,CATALINA_HOME 指向 Tomcat 根目录,Path 里加上 %JAVA_HOME%\bin 和 %CATALINA_HOME%\bin。验证方式是命令行敲startup.bat,浏览器访问 localhost:8080 能看到 Tomcat 默认页,说明 Tomcat 本身没问题。这一步不过,后面全是白搭。

2.2 用 IDEA 建 Web 项目并写第一个能返回 JSON 的 Servlet

新建项目选 Java Enterprise,勾选 Web Application,IDEA 会自动生成 web 目录和 WEB-INF/web.xml。如果没有这个模板,就建普通 Java 项目,手动在项目根下建 web 文件夹,再在 Project Structure 的 Facets 里添加 Web,把 web.xml 的路径指到 WEB-INF 下。

下面是一个最小可用的 Servlet,访问后返回一段 JSON,用来验证环境是否通了:

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; import java.io.PrintWriter; // 用注解方式注册,不用再改 web.xml @WebServlet("/hello") public class HelloServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 设置响应类型和编码,防止中文乱码 resp.setContentType("application/json;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.write("{\"code\":200,\"msg\":\"蛋糕店服务已启动\"}"); out.flush(); out.close(); } }

逻辑说明:@WebServlet("/hello")把访问路径映射到 /hello,浏览器请求http://localhost:8080/项目名/hello就会进 doGet。setContentType里的 charset 必须写,否则返回的中文在浏览器里是问号。参数方面,如果你把注解换成 web.xml 配置,就要写 servlet 和 servlet-mapping 两组标签,路径要带斜杠。写完代码后,在 IDEA 右上角 Run/Debug Configurations 里添加 Tomcat Server,Deployment 选项卡里把 artifact 加进去,Application context 设成 /cake,启动后访问对应地址即可。这一步跑通,说明整个链路没有阻塞,可以开始设计数据库了。

3. 数据库与实体设计:蛋糕店售卖网站的表结构和连接池怎么定

3.1 五张核心表:用户、商品、分类、订单、订单明细

蛋糕店售卖网站的数据模型不复杂,但字段要一次想清楚,不然后面改表很痛苦。我一般会建五张表:user 存用户信息,category 存蛋糕分类,product 存商品,orders 存订单主表,order_item 存订单明细。注意 orders 不要写成 order,那是 SQL 关键字,会报语法错误,这是血泪经验。

表名关键字段说明
userid, username, password, phone, address密码存 MD5,不存明文
categoryid, name, sort_order分类排序用整数,越小越靠前
productid, name, price, image, stock, category_id, descriptionprice 用 decimal(10,2)
ordersid, user_id, total_price, status, create_timestatus 用 0/1/2 表示待付款/已付款/已发货
order_itemid, order_id, product_id, quantity, priceprice 记录下单时的单价

建表时字符集统一用 utf8mb4,排序规则 utf8mb4_general_ci,否则 emoji 和部分生僻字存不进去。主键用 int 自增,外键在逻辑上关联即可,物理外键看情况加,小项目加了反而影响插入效率。

3.2 JDBC 工具类与数据库连接配置

不要在每个 Servlet 里都写 DriverManager.getConnection,那样代码重复且连接无法复用。常见做法是抽一个 DBUtil 类,用静态代码块加载驱动,提供 getConnection 和 close 方法。如果想让连接池更规范,可以引入 Druid 或 C3P0,但纯 JSP + Servlet 项目用简单的连接池就够了。

package com.cake.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/cake_shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { // MySQL 8 用 com.mysql.cj.jdbc.Driver,5.x 用 com.mysql.jdbc.Driver Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("数据库驱动加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } 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 里的 serverTimezone 必须加,MySQL 8 不加会报时区错误。参数方面,useUnicode 和 characterEncoding 保证中文正常,如果你用的是 MySQL 5.7,驱动类名换成 com.mysql.jdbc.Driver。连接关闭顺序是先 ResultSet 再 Statement 再 Connection,反了会报错。写完这个类,先写一个 main 方法测试能否拿到连接,通了再往下做。

4. 前台与后台功能落地:商品展示、购物车和订单提交怎么写

4.1 商品列表与详情页:JSP 里怎么用 JSTL 遍历数据

Servlet 负责查数据,JSP 负责展示,这是 MVC 的基本分工。商品列表的 Servlet 从数据库查出 List ,塞进 request 域,转发到 product_list.jsp。JSP 页面用 JSTL 的 c:forEach 遍历,不要用脚本片段写 Java 代码,那样后期没法维护。

// ProductListServlet.java 核心片段 List<Product> list = productService.findAll(); req.setAttribute("products", list); req.getRequestDispatcher("/product_list.jsp").forward(req, resp);
<%-- product_list.jsp 核心片段 --%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${products}" var="p"> <div class="cake-item"> <img src="${pageContext.request.contextPath}/upload/${p.image}" alt="${p.name}"> <h3>${p.name}</h3> <p>¥${p.price}</p> <a href="${pageContext.request.contextPath}/product/detail?id=${p.id}">查看详情</a> </div> </c:forEach>

逻辑说明:${pageContext.request.contextPath}拿到项目上下文路径,避免硬编码 /cake 导致换项目名就 404。参数方面,items 里的 products 必须和 Servlet 里 setAttribute 的 key 一致,var 是循环变量名。图片路径存在数据库里只存文件名,展示时拼上下文路径和 upload 目录。如果页面显示空白,先看 Servlet 有没有转发成功,再看 JSTL 的 jar 包有没有放进 WEB-INF/lib。

4.2 购物车用 Session 还是数据库:两种方案的取舍

购物车有两种做法:未登录时放 Session,登录后合并到数据库。小项目为了简单,可以全程用 Session 存一个 Map<Integer, CartItem>,key 是商品 id,value 是数量和单价。好处是不用建表,坏处是换浏览器就丢。如果要做登录后持久化,就建 cart 表,用户登录时把 Session 里的数据合并进去。

// 加入购物车 Map<Integer, CartItem> cart = (Map<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } int productId = Integer.parseInt(req.getParameter("id")); CartItem item = cart.get(productId); if (item == null) { item = new CartItem(productId, 1, productService.findById(productId).getPrice()); cart.put(productId, item); } else { item.setQuantity(item.getQuantity() + 1); } resp.sendRedirect(req.getContextPath() + "/cart/list");

逻辑说明:session 里存 Map,key 用商品 id 保证同一商品只累加数量。参数方面,quantity 增加时要判断库存,超过库存就提示。重定向用 sendRedirect,不要用 forward,否则刷新页面会重复提交。这里容易踩的坑是 Session 超时时间,默认 30 分钟,可以在 web.xml 里配 session-config 调整。

4.3 订单提交与事务处理:保证订单主表和明细同时写入

下单是最容易出问题的地方。用户点提交后,要往 orders 插一条,再往 order_item 插多条,还要扣库存。这三步必须在一个事务里,任何一步失败都要回滚。JDBC 默认自动提交,要手动 setAutoCommit(false)。

Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 插入订单主表 String orderSql = "INSERT INTO orders(user_id,total_price,status,create_time) VALUES(?,?,?,NOW())"; PreparedStatement ps = conn.prepareStatement(orderSql, Statement.RETURN_GENERATED_KEYS); ps.setInt(1, userId); ps.setBigDecimal(2, totalPrice); ps.setInt(3, 0); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); int orderId = 0; if (rs.next()) orderId = rs.getInt(1); // 2. 插入订单明细并扣库存 for (CartItem item : cart.values()) { String itemSql = "INSERT INTO order_item(order_id,product_id,quantity,price) VALUES(?,?,?,?)"; PreparedStatement ps2 = conn.prepareStatement(itemSql); ps2.setInt(1, orderId); ps2.setInt(2, item.getProductId()); ps2.setInt(3, item.getQuantity()); ps2.setBigDecimal(4, item.getPrice()); ps2.executeUpdate(); String stockSql = "UPDATE product SET stock=stock-? WHERE id=? AND stock>=?"; PreparedStatement ps3 = conn.prepareStatement(stockSql); ps3.setInt(1, item.getQuantity()); ps3.setInt(2, item.getProductId()); ps3.setInt(3, item.getQuantity()); if (ps3.executeUpdate() == 0) { throw new RuntimeException("库存不足"); } } conn.commit(); } catch (Exception e) { if (conn != null) conn.rollback(); throw e; } finally { DBUtil.close(conn, null, null); }

逻辑说明:RETURN_GENERATED_KEYS拿到刚插入的订单 id,用于关联明细。扣库存的 SQL 带了stock>=?条件,防止超卖,如果影响行数为 0 说明库存不够,直接抛异常触发回滚。参数方面,totalPrice 用 BigDecimal 不要用 double,避免精度丢失。事务里所有操作共用同一个 Connection,不能从连接池里拿多个连接。这段代码跑通后,整个下单流程就闭环了。

5. 避坑与排查:JSP + Servlet 项目最容易翻车的五个地方

5.1 中文乱码:请求参数和响应输出各管各的

现象:表单提交中文,后台收到的是乱码;或者后台返回中文,页面显示问号。原因:GET 请求的编码由 Tomcat 的 server.xml 决定,POST 请求要手动 setCharacterEncoding,响应输出要 setContentType。解决:在 Servlet 里第一行写req.setCharacterEncoding("UTF-8"),响应写resp.setContentType("text/html;charset=UTF-8")。Tomcat 8 之后 GET 默认 UTF-8,但 POST 不设一定乱。数据库连接 URL 也要带 characterEncoding=utf8。

5.2 404:路径对不上,九成是上下文路径没拼

现象:浏览器访问某个 Servlet 报 404。原因:要么 @WebServlet 的路径写错,要么 JSP 里的链接没加${pageContext.request.contextPath}。解决:先看 Tomcat 启动日志有没有映射信息,再检查访问地址是否带了项目名。IDEA 里 Application context 设成 /cake,访问就必须是 localhost:8080/cake/hello,少一段都不行。

5.3 500:空指针和驱动未加载是常客

现象:页面报 500,日志里一堆堆栈。原因:最常见的是 DBUtil 里驱动类名写错,或者 ResultSet 关闭后还在用。解决:看日志第一行 Caused by,如果是 ClassNotFoundException 就检查 mysql-connector-java 的 jar 有没有放进 WEB-INF/lib;如果是 NullPointerException 就检查 request.getParameter 有没有做空判断。注意 Tomcat 的 lib 目录和项目 lib 目录不要混,驱动放项目里更稳妥。

5.4 Session 丢失:浏览器禁用 Cookie 或超时

现象:登录后跳转其他页面又变成未登录。原因:Session 依赖 JSESSIONID 这个 Cookie,浏览器禁用 Cookie 就丢;或者 Session 超时。解决:检查浏览器 Cookie 设置,或者在 URL 里用response.encodeURL()重写。超时时间在 web.xml 里配<session-timeout>60</session-timeout>,单位是分钟。

5.5 图片上传后不显示:路径和权限两个坑

现象:上传成功但页面图片裂开。原因:一是存到了项目编译输出目录,重启就没了;二是路径拼错。解决:存到项目外的固定目录,比如 D:/cake_upload,数据库只存文件名,展示时用虚拟路径映射或者直接读文件流。如果存项目内,要存到 webapp/upload 下,且 IDEA 里要配置热部署,否则新文件不生效。

6. 进阶技巧:用过滤器统一编码和登录校验,少写重复代码

到这一步,网站基本能跑了,但你会发现每个 Servlet 都要写 setCharacterEncoding,每个后台页面都要判断 session 里有没有用户。这种重复代码用 Filter 解决最合适。Filter 是 Servlet 规范里的组件,在请求到达 Servlet 之前拦截,可以统一处理编码、登录校验、日志记录。

先写一个编码过滤器:

package com.cake.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); // 放行,交给下一个过滤器或 Servlet } }

逻辑说明:@WebFilter("/*")拦截所有请求,chain.doFilter是放行,不写这行请求就断了。参数方面,如果你只想拦截特定路径,把 /* 改成 /admin/* 即可。注意 Filter 的执行顺序按类名字母序,多个过滤器时要注意。

再写一个登录校验过滤器,只拦后台路径:

@WebFilter("/admin/*") public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

逻辑说明:从 Session 里取 loginUser,为空就重定向到登录页,不为空才放行。参数方面,/admin/*表示只拦后台,前台商品页不受影响。这里有个细节:登录页本身不能拦,否则死循环,所以登录 Servlet 的路径不要放在 /admin 下。

验证过滤器是否生效,可以故意清掉 Session 再访问后台,看是否跳登录页。我一般会在过滤器里加一行日志,输出请求路径和拦截结果,排查时一目了然。这套东西加上之后,代码量能少三分之一,后期加权限控制也方便。

最后说个习惯:每次改完代码,先 clean 再 rebuild,再重启 Tomcat,不要依赖热部署,JSP + Servlet 的热部署经常玄学失效。数据库操作前先备份,改表结构之前想清楚字段类型。这套技术栈虽然老,但把每个环节都动手写一遍,对 HTTP 协议、Servlet 容器、MVC 分层的理解会比直接上框架扎实得多。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 18:12:43

Lumerical FDTD脚本搭建平面实体:从入门到参数化扫描实战

1. 为什么平面实体值得单独拎出来讲做FDTD仿真的人&#xff0c;绕不开结构搭建这一步。不管你是做光栅耦合器、微环谐振腔、还是超表面透镜&#xff0c;最终都要落到“在仿真区域里放一个什么东西”这个最朴素的问题上。Lumerical的FDTD Solutions提供了图形界面画结构的方式&a…

作者头像 李华
网站建设 2026/10/22 18:12:38

集合合并运算:并查集原理、实现与工程实践

1. 从一个集合合并的运算结果说起{aaa,bbb,ccc},{bbb,ddd},{eee,fff},{ggg},{ddd,hhh}这组数据合并之后得到{aaa,bbb,ccc,ddd,hhh},{eee,fff},{ggg}&#xff0c;这个结果乍一看有点反直觉&#xff1a;为什么前三个集合被揉成了一个&#xff0c;而后两个却原封不动地保留了下来&…

作者头像 李华
网站建设 2026/10/22 18:12:39

命令行待办事项应用实战:从设计到日常使用

1. 为什么我坚持用命令行做待办事项管理我一直有个执念&#xff1a;日常待办这件事&#xff0c;不应该被一个占着几百兆内存的图形应用绑架。两年前我换回命令行做待办事项应用&#xff0c;把任务管理彻底搬进终端之后&#xff0c;最大的感受不是"变酷了"&#xff0c…

作者头像 李华
网站建设 2026/10/22 2:24:05

节后第一天面对 80 个取数需求:我是如何用 ChatBI 把自己解放出来的

国庆长假后开工第一天&#xff0c;早上八点五十分。 我刚端着一杯冰美式坐到工位上&#xff0c;正准备弯腰去给猫包里的英短猫 Null 倒两勺冻干&#xff0c;企业微信的提示音就跟放鞭炮一样噼里啪啦炸开了。 点开工单系统&#xff0c;待办列表赫然显示&#xff1a;82 个未处理。…

作者头像 李华
网站建设 2026/10/14 9:17:02

Grok订阅省钱攻略:避开重复付费,用好年付与API免费额度

1. 先把Grok的账单结构摸清楚 1.1 别傻傻分不清的三个“Grok入口” 很多人一上来就问“Grok会员怎么买最便宜”&#xff0c;但第一步不是比价格&#xff0c;而是搞清楚你付钱之后到底买了哪个版本的Grok。 xAI目前把Grok的使用场景分成了好几个体系。最常见的三个入口是&…

作者头像 李华
网站建设 2026/10/14 9:16:59

TCP可靠传输机制与常见坑:从三次握手到粘包调优

看到这个标题&#xff0c;老开发者可能会心一笑——TCP协议都快五十岁了&#xff0c;怎么还"来了"&#xff1f;但真正在项目里被网络问题折腾过的人&#xff0c;大概都能体会这标题里"可靠"两个字的分量。面试时几乎必背的三次握手、四次挥手&#xff0c;到…

作者头像 李华