☰
基于JavaWeb的美食网站源码解析:从Servlet到Spring Boot改造
2026/9/29 19:39:07 网站建设 项目流程

简介:基于JavaWeb的美食网站设计与实现项目源码,面向Java Web入门及进阶开发者,适合用作课程设计、毕业设计或自学练手。项目围绕美食信息浏览、菜谱搜索、烹饪心得分享等场景,完整呈现利用Servlet、JSP、JDBC等技术构建动态Web应用的过程,涵盖用户注册登录、菜谱分类展示、搜索功能、评论系统与用户互动等核心模块。压缩包为zip格式,大小50.34MB,内部源码目录结构清晰(如WebRoot、Servlets、JSP、DAO、Models、Utils等),便于对照学习MVC分层思想、数据库连接与CRUD操作、页面跳转及前后端交互逻辑。源码经实际测试验证可运行,亲测有效,可帮助学习者快速搭建环境、理解JavaWeb开发全流程,并在此基础上扩展功能或完成二次开发。该资源目前已有1558人学习,适合需要实战参考或项目复现的开发者下载使用。

1. 基于 JavaWeb 的美食网站:一份能直接跑起来的设计与实现

一套基于 JavaWeb 的美食网站.zip 解压之后,你得到的不只是几个 JSP 页面,而是一个把“菜品展示—分类检索—购物车—下单结算”整条链路串起来的完整 javaweb 项目案例。这个定位对正在赶课设、毕设或期末实训的人很关键:很多同学的卡点不在会不会写 Servlet,而在于从来没有把 Servlet、JSP、MySQL、Tomcat 这几样东西在 IDEA 里完整拼通过。黑马那种 JavaWeb 笔记刷完了一大半,真动手时还是会在映射配置和数据库连接上卡壳。这套项目源码补齐的正是这一公里。它适合三类人:想把 JavaWeb 请求流转真正看懂的初学者、需要一份能演示的完整案例拿去改的课设选手、以及想搞清一个 Web 项目内部到底怎么分层的从业者。

2. 项目骨架与请求流转:MVC 分层和路由到底怎么走

2.1 技术栈选型:为什么还是 Servlet + JSP + JDBC

这份美食网站走的是 JavaWeb 最经典的一条路:Servlet + JSP + JSTL/EL + JDBC + MySQL,部署容器是 Tomcat。现在新项目都在谈 Spring Boot,但课设、实训和很多学校期末项目仍然要求用这套原始组合,原因很实际:Servlet + JSP 能完整暴露 HTTP 请求从进入到响应离开的全过程,代码里每一行都能对应到协议层面的动作。Spring Boot 会把请求映射、参数封装、视图渲染这些事包得很严实,对初学者来说是个黑匣子,写完了也讲不清楚。

这套技术栈的代码量也很适合一个人短时间啃下来。Servlet 充当控制器接收请求参数、调用业务逻辑、最后转发页面;JSP 负责视图渲染,配合 EL 表达式和 JSTL 标签可以让页面里基本不出现 Java 代码;JDBC 通过连接池拿连接、执行 SQL、把结果集封装成实体。三块东西各管一段,边界清楚,出了问题也容易定位到具体某一个类。项目里常见的组织方式就是 controller、service、dao、entity 四层包结构,如果你的压缩包源码里是别的包名,只要认准“谁在收参数、谁在写 SQL、谁在渲染页面”这三件事,就能快速对号入座。

2.2 解压后先看包结构:别急着导入,先建立代码地图

拿到 zip 包后不要立刻丢给 IDEA 执行,先解压看一眼目录。一个规范的 JavaWeb 项目通常长这样,下面的结构是这套美食网站最常见的组织方式,具体文件命名以压缩包里为准:

food-web/ ├── src/ │ ├── main/java/com/food/ │ │ ├── controller/ // Servlet 控制器,接收前端参数 │ │ │ ├── UserServlet.java │ │ │ ├── DishServlet.java │ │ │ └── CartServlet.java │ │ ├── service/ // 业务逻辑层,事务控制基本在这一层 │ │ │ ├── DishService.java │ │ │ └── OrderService.java │ │ ├── dao/ // 数据访问层,只关心 SQL 和结果集 │ │ │ ├── DishDao.java │ │ │ └── UserDao.java │ │ ├── entity/ // 实体类,与数据库表字段一一对应 │ │ │ ├── Dish.java │ │ │ └── User.java │ │ ├── util/ // 连接池、字符串处理等工具 │ │ └── filter/ // 编码过滤器、登录过滤器 │ ├── main/resources/ │ │ ├── jdbc.properties // 数据库连接配置 │ │ └── c3p0-config.xml // 连接池配置 │ └── main/webapp/ │ ├── static/ // css、js、图片 │ ├── WEB-INF/ // web.xml 和依赖 jar 包 │ ├── index.jsp // 首页入口 │ ├── dish_list.jsp // 菜品列表 │ ├── dish_detail.jsp // 菜品详情 │ ├── cart.jsp // 购物车页面 │ └── login.jsp // 登录页 └── sql/ └── food_web.sql // 数据库初始化脚本

这个结构里最值得先看的是 controller 目录,因为 Web 项目的入口全在 Servlet 上。无论是 web.xml 还是 @WebServlet 注解,都会把 URL 地址映射到某个 Java 类,打开这些文件就等于打开了一张站点路由表。理解了路由表,后面所有业务功能都能顺着 URL 找到对应代码。

2.3 一条请求的完整旅程:从“点川菜”到菜品列表

我习惯用一个真实场景把这条链路串起来讲:用户打开首页,看到分类导航栏,点了一下“川菜”。从前端到数据库,这条请求会经过四个环节。

<!-- 前端触发:分类链接中携带 action 和 category 两个参数 --> <a href="${pageContext.request.contextPath}/dish?action=list&category=川菜"> 川菜 </a>
// DishServlet 接收请求 @WebServlet("/dish") public class DishServlet extends HttpServlet { private DishService dishService = new DishService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); if ("list".equals(action)) { // 从请求中取出分类参数,交给 service 层查询 String category = req.getParameter("category"); List<Dish> dishes = dishService.queryByCategory(category); // 把查询结果放进 request 域,转发给 JSP 渲染 req.setAttribute("dishes", dishes); req.getRequestDispatcher("/dish_list.jsp").forward(req, resp); } } }

这里的请求参数设计沿用了 JavaWeb 项目常见的“一个 Servlet 对应多种操作”的做法:action 字段决定调用哪个业务方法,category 是查询条件。setCharacterEncoding 放在 Servlet 入口是为了保证参数在解析前已经是 UTF-8 编码,这一步少写了,中文关键词基本必乱。forward 是服务端跳转,浏览器地址栏不会变化,request 域中的数据可以直接带到 JSP 页面。

<!-- dish_list.jsp 中使用 EL 和 JSTL 遍历循环渲染列表 --> <c:forEach items="${dishes}" var="dish"> <div class="dish-card"> <h3>${dish.name}</h3> <p>价格:${dish.price} 元</p> <p>月售:${dish.sales}</p> </div> </c:forEach>

JSP 页面只负责取数据、循环、输出,不写 Java 代码。items="${dishes}" 从 request 域中取出 DishServlet 放入的列表,var="dish" 定义循环变量,${dish.name} 调用的是 Dish 实体类中名为 getName() 的 getter 方法。这种写法让页面保持干净,也方便以后把视图层替换成 FreeMarker 或其他模板引擎,service 和 dao 层可以原封不动搬走。

3. 核心业务模块落地:菜品搜索、分页与购物车状态管理

3.1 数据库设计:从 SQL 脚本看业务边界

压缩包里的 sql/food_web.sql 是整个项目的数据基础。导入之前建议先打开看一眼建表语句,你会发现这套业务核心就是围绕“用户、菜品、订单”三张主轴展开。常见的表设计大致如下:

表名主要职责常见字段
user系统用户id, username, password, phone
category菜品分类id, name, sort_order
dish菜品信息id, name, price, image, sales, description, cid
cart_item购物车明细id, uid, dish_id, quantity
order订单主表id, order_no, uid, total_price, status, create_time
order_detail订单明细id, order_id, dish_id, price, quantity
comment菜品评论id, uid, dish_id, content, create_time

每张表的命名和留言、分类、订单状态这类附加表不一定完全一致,但看完这个清单,你就能理解项目里每个 Servlet 都在操作什么数据。DishServlet 主要围绕 dish 表和 category 表工作,CartServlet 操作的是 Session 里的购物车对象,OrderServlet 下单时会同时写 order 表和 order_detail 表,事务就发生在这一步。导入脚本的命令很简单:

mysql -uroot -p --default-character-set=utf8 source D:/workspace/food-web/sql/food_web.sql;

--default-character-set=utf8 这个参数经常被漏掉。如果你的 MySQL 客户端默认字符集不是 UTF-8,建表时中文注释和表数据会出现乱码,后面页面怎么调都白费。source 后面接的是 sql 文件的绝对路径,Windows 下注意路径分隔符要用正斜杠或者双反斜杠,否则 MySQL 会报找不到文件的错。

3.2 菜品分页与搜索:LIMIT 计算和 SQL 注入

美食网站菜品一多就必须分页。项目里的分页查询通常集中在 DishDao 中,最常见的实现是基于 MySQL 的 LIMIT 子句。先看核心查询方法:

public List<Dish> findDishesWithCondition(String keyword, int pageNum, int pageSize) { // 拼接动态条件时优先使用 StringBuilder + 占位符 StringBuilder sql = new StringBuilder("SELECT * FROM dish WHERE 1=1 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { // 模糊搜索:前后都拼接百分号 sql.append("AND name LIKE ? "); params.add("%" + keyword.trim() + "%"); } // 按销量倒序,取当前页的数据 sql.append("ORDER BY sales DESC LIMIT ?, ?"); // pageNum 从 1 开始,数据库 offset 从 0 开始 params.add((pageNum - 1) * pageSize); params.add(pageSize); // 使用 PreparedStatement 执行,参数不拼接进 SQL 字符串 // 这是防止 SQL 注入的第一道防线 return jdbcTemplate.query(sql.toString(), params.toArray(), new DishRowMapper()); }

这段代码里有三个关键点值得细说。第一,LIKE 关键字的 % 是拼在参数值里而不是拼在 SQL 语句里,这样写 PreparedStatement 才能正确识别通配符。第二,LIMIT 的 offset 计算是分页最容易出错的地方:页面传过来的 pageNum 如果是 1,数据库的偏移量应该是 0,所以必须做 (pageNum - 1) * pageSize 的换算,不然第一页会少一条数据或者直接查空。第三,所有用户输入都通过 ? 占位符传参,即便搜索框里输入了 SQL 片段,也只会被当作普通字符串处理。

配套的还有一个查询总数的方法,通常长这样:

SELECT COUNT(*) FROM dish WHERE name LIKE ?;

总记录数除以 pageSize 向上取整得到总页数,这个值要放进 request 域传给 JSP 生成分页按钮。我见过不少项目把 count 查询和列表查询混在一个方法里做,结果每次翻页都要重复查询两次数据,页面性能在数据量小时看不出问题,到几百条菜品时就能明显感到卡顿。养成两个方法拆开的习惯,后面接 Spring Boot 也好迁移。

3.3 购物车与 Session:会话状态怎么存

购物车是 JavaWeb 项目里最典型的 Session 应用场景。用户打开浏览器,选了几道菜放到购物车,再点结算,这期间服务端需要记住“这个用户买了什么”。整套美食网站的购物车实现通常在 CartServlet 中,核心代码是:

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { HttpSession session = req.getSession(); // 从 Session 中取出购物车对象,没有则新建一个 Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); session.setAttribute("cart", cart); } // 接收菜品 id 和数量参数 String dishId = req.getParameter("dishId"); String quantity = req.getParameter("quantity"); // 项目里常见的写法是购物车内部维护一个 Map<Dish, Integer> cart.addDish(Integer.parseInt(dishId), Integer.parseInt(quantity)); // 跳回菜品列表页 resp.sendRedirect(req.getContextPath() + "/dish?action=list"); }

购物车放在 Session 而不是 Cookie 里,主要是两个原因。一是 Cookie 有 4KB 的大小限制,购物车装不了几道菜就超了;二是 Cookie 每次请求都会原样发到服务端,购物车数据放里面既占带宽又容易被篡改。Session 数据保存在服务端内存,客户端只持有 sessionId,安全性好得多。代价是服务端重启或者浏览器关闭,Session 随之消失,所以记得提醒使用者:改完代码重启 Tomcat 后购物车清空是正常现象。

如果你要在这个项目上做二次开发,下一步最值得动的地方就是把购物车落库。利用 3.1 里的 cart_item 表,用户每次加购时同步写数据库,这样刷新不掉数据,也能支撑多设备同步。改造思路很简单:CartServlet 在写入 Session 的同时调用 CartDao.insert 方法,查询购物车时优先读数据库。这一处改动面试时讲出来,比单纯背 Session 概念要有说服力得多。

4. 在 IDEA + Tomcat 里跑通全流程:从导入 ZIP 到浏览器验证

4.1 导入项目:区分 Maven 和带 lib 两种结构

拿到 zip 包先看根目录有没有 pom.xml。有 pom.xml 说明是 Maven 项目,没有则大概率是传统的 lib 目录管理模式。两种结构在 IDEA 中的导入方式完全不同。

如果是 Maven 项目,在 IDEA 里点 File → New → Project from Existing Sources,选中解压目录,选择 Maven,IDEA 会自动读取 pom.xml 并下载依赖。这一步需要联网,且首次加载会比较慢,耐心等右下角进度条跑完。如果是带 lib 的普通 JavaWeb 项目,同样选择 Project from Existing Sources,但导入完成后需要手动设置依赖:File → Project Structure → Modules → Dependencies,把 webapp/WEB-INF/lib 目录下的 jar 包全部加入。

导入过程中最容易翻车的是 JDK 版本不匹配。美食网站这种项目的 pom.xml 里如果指定了 Java 8,而你本机只装了 JDK 17,IDEA 会让你重新选择 SDK。这里不建议硬选高版本,因为老项目的依赖包未必兼容新编译目标。我一般会在 Project Structure 里创建一个新的 SDK,指到本地已装的 JDK 8 目录,再重新编译一遍,成功率最高。

4.2 数据库初始化与连接配置

把 zip 里的 sql 脚本导入 MySQL 后,下一步就是改连接配置。这套项目的配置文件通常叫 jdbc.properties 或 db.properties,位于 src/main/resources 下。打开后你会看到类似下面的内容:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/food_web?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

这四行配置每一行都是坑。driver 这一行要注意区分 MySQL 版本:MySQL 5.7 及以下一般用 com.mysql.jdbc.Driver,MySQL 8.0 以上必须用 com.mysql.cj.jdbc.Driver,用错就会报 ClassNotFoundException。url 里的 characterEncoding=UTF-8 是中文不乱码的关键,useSSL=false 是为了关掉 MySQL 8 默认开启的 SSL 警告,serverTimezone=Asia/Shanghai 则是解决 MySQL 8 对时区敏感导致的连接超时问题。username 和 password 改成你自己数据库的用户名密码,不要直接沿用压缩包里的默认值。

改完配置后启动项目,如果数据库连接还是失败,优先检查 MySQL 服务有没有启动,以及 food_web 这个数据库名是否存在。命令行里执行 mysql -uroot -p -e "show databases;" 可以快速确认,如果列表里没有 food_web,回到第 3.1 节重新导入一遍 SQL 脚本。

4.3 配置 Tomcat 并启动验证

项目导入、数据库就绪后,剩下就是配置 Tomcat 跑起来。IDEA 里运行 JavaWeb 项目的标准步骤是这样:

Run → Edit Configurations → 左上角 + → Tomcat Server → Local。在 Server 标签页选择 Tomcat 安装目录,在 Deployment 标签页点 + → Artifact,选择项目的 exploded war 包,Application context 建议改成 /food_web,这样访问地址简短且不容易和本机其他项目冲突。配置完成后启动 Tomcat,控制台出现类似下面的日志就说明部署成功:

INFO: Deployment of web application archive [food_web] has finished INFO: Starting ProtocolHandler ["http-bio-8080"]

浏览器访问 http://localhost:8080/food_web/index.jsp,按下面五步验证:

  1. 首页图片和中文文字是否正常显示,没有乱码
  2. 点击“川菜”分类,URL 变成 /dish?action=list&category=川菜,列表能刷新
  3. 搜索一个菜品关键词,搜索结果能按销量排序
  4. 点“加入购物车”,cart.jsp 能看到数量和总价
  5. 注册一个新用户后登录,session 能记录登录状态

这五步走完,这套美食网站就算真正跑通了。如果你访问后报 404 或者页面空白,先回第 2.2 节重新核对项目结构,再回到第 5 章逐条排查,问题基本都在那几个常见位置。

5. 常见问题与避坑:启动、编码、数据库连接的几个拦路虎

5.1 一访问就报 404:URL pattern 与项目路径对不上

现象:Tomcat 正常启动,控制台也没有报错,但浏览器访问 http://localhost:8080/food_web/index.jsp 时出现 404 页面。

原因:最常见的有三种。一是 Artifact 的名字和 Application context 不一致,项目实际部署路径不是你访问的路径;二是 web.xml 里 Servlet 的 url-pattern 没配对,比如访问 /dish,实际映射却是 /DishServlet;三是 index.jsp 没有放在 webapp 根目录下,被塞进了 WEB-INF 里面,导致直接访问不到。

解决:先去 IDEA 的 Run → Edit Configurations 看 Deployment 标签下的 Application context,确认是 /food_web。再打开 web.xml 或各 Servlet 类上的 @WebServlet 注解,逐个核对 URL。最后确认 JSP 文件在 webapp 根目录或子目录下,WEB-INF 下的页面只能通过服务端 forward 访问,不能直接在地址栏输入。

5.2 中文显示成问号或乱码:三个环节缺一不可

现象:菜品名称、用户昵称在页面上显示为 ??????,或者 JSP 页面里的中文正常,数据库中查询出来的中文全乱。

原因:JavaWeb 的编码链路涉及三处:请求参数编码、JSP 页面编码、数据库连接编码。常见情况是只设置了 JSP 的 pageEncoding,忽略了 Servlet 入口的 setCharacterEncoding,POST 请求的中文在进入业务逻辑前就成了乱码。另一种常见情况是数据库连接 URL 里没加 characterEncoding=UTF-8,JDBC 写入时用的是服务端默认字符集。

解决:三个地方全部统一成 UTF-8。JSP 文件第一行声明 pageEncoding="UTF-8";过滤器中强制 req.setCharacterEncoding("UTF-8");jdbc.properties 的 URL 上带 useUnicode=true&characterEncoding=UTF-8。如果数据库里已经存了乱码,需要先清理数据再用 UTF-8 重新导入 SQL 脚本。

5.3 MySQL 8 连接失败:驱动版本和时区

现象:启动项目后报 Communications link failure 或 Access denied for user,但用户名密码明明是对的。

原因:高版本 MySQL 对连接要求更严格。MySQL 8 开始驱动类从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver,而且这个驱动要求连接 URL 里显式指定 serverTimezone,否则连接会超时。很多项目包用的是五六年之前的代码,配置里还是老驱动和老 URL。

解决:检查三个点:默认驱动是否已升级到 mysql-connector-java 8.x,配置改成 com.mysql.cj.jdbc.Driver;URL 末尾补上 serverTimezone=Asia/Shanghai;如果 MySQL 8 默认的 caching_sha2_password 插件导致认证失败,可以在 MySQL 里把用户改成 mysql_native_password 加密方式再试。

5.4 ClassNotFoundException:lib 没有被 IDEA 打包进 Artifacts

现象:代码编译没问题,控制台也没有语法报错,但启动后访问任意一个 Servlet 就抛 ClassNotFoundException,提示找不到某个 jar 包里的类。

原因:普通 JavaWeb 项目手动导入 lib 目录后,IDEA 的编译器编译时能看到这些 jar,但打包成 Artifact 时默认不会自动带上外部依赖。运行过程中容器找不到这些类,就会在第一次使用时报 ClassNotFoundException。

解决:File → Project Structure → Artifacts,在选中的 Web Artifact 下方的 Available Elements 里,把 WEB-INF/lib 下的依赖全部右键 Put into /WEB-INF/lib,然后重新 Build Artifact。做完这一步再看输出的 war 包,解压确认 lib 目录里有 jar 文件再启动。

5.5 改了 JSP 却没反应:Tomcat 部署缓存和浏览器缓存

现象:修改了 dish_list.jsp 的页面样式或结构,刷新页面还是老样子,有时重启 Tomcat 也解决不了。

原因:两个层面。一是 IDEA 里 Tomcat 部署方式如果选的不是 exploded,每次改动都需要重新打包重部署;二是浏览器端 JSP 响应没有带 no-cache 头,本地缓存了旧页面。

解决:开发阶段尽量选 exploded artifact 部署,改完 JSP 后 IDEA 会自动同步到 Tomcat 目录。浏览器按 Ctrl+F5 强制刷新跳过缓存。如果重启后还是旧页面,去 Tomcat 的 work/Catalina 目录删掉对应项目的缓存文件夹,再用 Clean 清一次 IDEA 的 target 目录,这招基本能解决九成以上“改了没生效”的问题。

6. 把 Servlet 项目改造成 Spring Boot 风格:从实训代码到简历项目

这个压缩包能跑通验收只是第一步,如果你想让它在简历上发挥更大价值,我很推荐沿着“换壳不改芯”的思路改造成 Spring Boot。改造的核心不是重写业务,而是保留 service 和 dao 层,只把 controller 层从 Servlet 换成 Spring MVC 的注解风格。因为项目当初做了合理分层,service 层写业务逻辑时并不依赖 HttpServletRequest 这些 Servlet API,所以迁移成本极低。

原 Servlet 写法是:

@WebServlet("/dish") public class DishServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String category = req.getParameter("category"); // ... } }

改成 Spring Boot 的 Controller 是:

@RestController @RequestMapping("/api/dish") public class DishController { private final DishService dishService; public DishController(DishService dishService) { this.dishService = dishService; } @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "8") int size) { return Result.success(dishService.pageList(page, size)); } }

对比下来你会发现,参数从 req.getParameter 变成了 @RequestParam 注解,返回值从转发 JSP 变成了 JSON 对象,但 service 层的 pageList 方法几乎原封不动。这就是我强调要保住 service 层独立性的原因。改成 Spring Boot 后,原来那些 JSP 可以退居二线,前端用 Vue 或原生 HTML 调用 /api/dish/list 接口,美食网站的菜品数据完全不需要重复编写。

改造完的验证方法很简单,浏览器直接访问:

curl "http://localhost:8080/api/dish/list?page=1&size=8"

能返回菜品列表的 JSON,说明业务层完整迁移成功。从那以后我每次拿到一个新的 JavaWeb 项目包,都会按固定三步走:先跑通首页,再改一处业务逻辑验证代码是不是真的看懂了,最后检查有没有硬编码的 SQL 和数据库密码,确认没有才往简历上放。这套流程替我省了不少返工的时间,也希望能帮到你少踩几个坑。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询