简介:基于JavaWeb技术实现的必胜客在线订餐系统项目,面向JavaWeb初学者、课程设计与毕业设计人群,完整模拟在线订餐核心流程,覆盖用户注册登录、菜单浏览、购物车、下单支付、订单管理等业务,后端同时提供菜品管理、订单处理和用户信息维护等功能。压缩包共1307个文件,整体约66.88MB,包含79个Java源文件、126个class编译文件、45个jar依赖库、44个JSP页面、44个HTML页面、1个SQL脚本,以及大量png、jpg、gif图片素材,源码、数据库脚本和页面资源分区存放,便于按模块查阅与二次开发。目前已有2515人学习/下载。项目在MVC分层架构、Servlet与JSP协同工作、JDBC/ORM数据库访问、AJAX前后端交互、SQL注入防护等方面均有完整示例,可帮助读者理解JavaWeb完整开发流程;配合数据库脚本与前端页面,既能用于功能调试和代码研读,也能作为毕业设计或课程设计的直接参考。
1. 这个 JavaWeb 项目,解决的不是"点餐"而是"练手"
拿到一个"必胜客在线订餐系统"的 JavaWeb 项目压缩包时,很多人的第一反应是把它当成一个能直接部署上线的商业系统。我的看法刚好相反:这种基于 JSP + Servlet + MySQL 的经典 JavaWeb 项目,最大的价值不是"能用",而是让你在课程设计、毕设或者第一份 Java 开发工作面试前,用最小成本把整条链路跑通。它麻雀虽小,五脏俱全:用户登录、菜品分类、购物车、订单提交,每一样都是 JavaWeb 面试里反复被问的东西。这篇笔记就顺着这个 zip 里的常见结构,把它拆开、跑通、改明白。
2. 拆开 zip 看骨架:JavaWeb 在线订餐系统的结构、选型与运行链路
一个完整的 JavaWeb 项目完整案例 MySQL 版,不是一堆 .java 文件的集合,而是一整套可以被 IDEA 直接识别的工程。你解压这类 zip 后,通常能看到 Maven 的 pom.xml、src 目录、web 目录和一堆 jsp 页面。我在帮朋友排查这类项目时,发现大多数人跑不起来,不是代码写得不对,而是根本不理解这些目录扮演什么角色。先花二十分钟把结构看懂,后面能少踩一半坑。
2.1 经典三层架构:Servlet 做控制、JSP 做视图、JDBC 做持久化
在线订餐系统的标准做法是把代码切成三层。控制层用 Servlet 接收浏览器发来的请求,解析参数后调用业务层;业务层处理登录校验、订单金额计算这类规则;数据访问层用 JDBC 或者 MyBatis 访问 MySQL,把结果封装成实体类。这样的分层不是为了好看,而是让"改需求"的成本可控。比如你要把密码从明文改成 MD5 加密,只需要动业务层一个方法;如果你在 JSP 里到处写 JDBC,改起来就是翻车现场。
视图层用 JSP 渲染 HTML。JSP 的优势是可以在页面里直接写 Java 片段,方便把菜单列表、购物车条目动态输出;缺点是页面逻辑写多了会变得很乱,所以这个项目里一般只让 JSP 做输出,不做数据库操作。我见过一些初学者把查询代码写在 JSP 的<%%>里,结果页面一打开就报 NullPointerException,这种写法也不利于后续改成前后端分离。
| 层次 | 所在目录 | 职责 | 常见文件 |
|---|---|---|---|
| 控制层 | controller | 接收请求、解析参数、跳转页面 | LoginServlet、OrderServlet |
| 业务层 | service | 登录校验、订单金额计算、事务边界 | UserService、OrderService |
| 数据层 | dao | 执行 SQL、把 ResultSet 变成对象 | UserDao、DishDao |
这张表是这类项目最通用的分工方式。你翻开任何一份像样的 JavaWeb 作业源码,大概率都能找到这三个包的影子。看代码前先在 IDE 里展开src/main/java,看看有没有 controller、service、dao、entity 这四类包,如果没有,说明这个项目多半是教程里的单文件示例,拿来做毕设要谨慎。
2.2 为什么这个题目值得用 JSP+Servlet,而不是直接上 Spring Boot
你可能觉得都什么年代了还在用 Servlet。但这类"必胜客在线订餐系统"的经典 JavaWeb 项目,恰恰适合作为教学案例,原因是它足够"薄"。Spring Boot 帮你屏蔽了太多细节:内嵌 Tomcat、自动配置数据源、依赖注入全自动,你反而不知道请求到底是怎么从浏览器走进数据库的。用 JSP+Servlet 时,整个调用链是可见的:URL 映射到哪个 Servlet,Servlet 里怎么拿 Connection,Connection 怎么执行 SQL,SQL 结果怎么变成页面表格,每一步都能下一行断点。
从答辩和面试角度看,老技术栈也不扣分。面试官问"JavaWeb 项目里 session 和 cookie 的区别",你如果说用的是 Spring Boot,可能根本没有直接操作过 session;而在这个项目里,登录状态就是你自己写的 session 存储,订单事务就是你自己调的 setAutoCommit(false)。当然,如果目标是投大厂实习,建议先跑通这个版本,再用 Spring Boot 重写一遍,两套都熟了最好。
还有一层很现实的原因:这类项目的运行环境要求低。一台普通笔记本装个 JDK、Tomcat、MySQL 就能跑,不需要额外配置 Nacos、Redis 这些中间件。你把它放进简历,写"独立开发在线订餐系统",面试官不会觉得夸张;但如果写"基于微服务的订餐系统",反而容易被追问服务拆分和分布式事务,把自己逼到墙角。选型不是越新越好,而是能解释清楚为什么这样选。
2.3 从浏览器到数据库:一次"提交订单"请求的完整生命周期
为了让后面的代码不至于悬空,这里先给出一份典型目录结构,也是我打开这类 zip 后第一眼看的东西:
pizza-online/ ├── pom.xml # Maven 配置,JavaWeb 依赖都在这里 ├── src/main/java/ │ ├── com/pizza/controller/ # Servlet 控制层 │ │ ├── LoginServlet.java │ │ ├── DishServlet.java │ │ └── OrderServlet.java │ ├── com/pizza/service/ # 业务层 │ ├── com/pizza/dao/ # 数据访问层,JDBC 写在这里 │ └── com/pizza/entity/ # 实体类,对应数据库表 ├── src/main/resources/ │ └── db.properties # 数据库连接配置 └── web/ ├── WEB-INF/ │ ├── web.xml # Servlet 映射与欢迎页 │ └── jsp/ # 页面文件按模块拆分 ├── index.jsp ├── login.jsp ├── menu.jsp └── cart.jsp你把这个目录当成一张地图。浏览器请求/login时,Tomcat 根据 web.xml 或注解找到 LoginServlet;LoginServlet 调用 Service 类;Service 调用 DAO;DAO 用 db.properties 里的连接串访问 MySQL。任何一层报错,日志里都会带出那一层的类名,你能顺着调用栈快速定位问题。这也是为什么我强烈建议不要用联网数据库,而是本地装一个 MySQL 5.7 或 8.0,把数据脚本跑一遍,亲眼看数据落进表里。
网上很多参考代码,包括一些培训班整理的黑马 JavaWeb 笔记数据,表名都喜欢用t_user、t_dish、t_cart这种短名字,你拿到手第一件事应该是打开 SQL 脚本,看它的字段命名风格。不要直接拿我们后面的建表语句去套别人的项目,字段名对不上会报Unknown column,这类错误在 JavaWeb 项目的评论区里每天都能看到。
3. 先把数据立住:必胜客订餐系统的 MySQL 表设计与初始化脚本
数据是整个 JavaWeb 项目的命脉。很多人的项目页面都能打开,一点"提交订单"就报错,问题大多出在表结构不匹配。比如前台页面传回菜品 id,后台 SQL 里却写了 dish_id,而表字段是 id,自然查不到数据。我一般拿到这样的 zip 后,第一件事不是读代码,而是打开里面的 .sql 文件,把表结构和代码里的字段名对照一遍,这个过程比看十遍页面对我有用得多。
3.1 六张核心表:用户、菜品、分类、购物车、订单、订单明细的字段设计
在线订餐系统的常见做法是设计六张表。用户表存账号和收货信息;分类表把比萨、小食、饮料分开;菜品表保存价格和图片;购物车表维护"谁在什么时间想买什么";订单表记录一次购买行为;订单明细表记录这张订单里具体有哪些菜、数量多少、单价多少。这样设计不是为了凑表数,而是让数据能回答几类问题:某个用户买过什么、某个菜品卖了多少、某张订单金额怎么算出来的。
需要注意订单和订单明细为什么要拆两张表。因为一张订单可能包含多个菜品,如果只在订单表里放一个菜品字段,就没法支持"一份订单 3 个披萨 2 杯饮料"这种自然语义。拆开后,订单表只管总金额和状态,明细表一行一个菜品,通过 order_id 关联,这样统计"招牌披萨卖了多少份"也就是一条 SUM 查询。
3.2 建库建表 SQL:完整脚本与参数说明
下面这段 SQL 是这类项目最常用的一套基础脚本,建议你直接复制到 Navicat 或命令行里执行,注意先看一眼注释里的编码和后端配置是否一致:
CREATE DATABASE IF NOT EXISTS pizza DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE pizza; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, phone VARCHAR(20), address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL ) ENGINE=InnoDB; CREATE TABLE t_dish ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, image VARCHAR(255), description VARCHAR(500), CONSTRAINT fk_dish_category FOREIGN KEY (category_id) REFERENCES t_category(id) ) ENGINE=InnoDB; CREATE TABLE t_cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, dish_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_cart_dish FOREIGN KEY (dish_id) REFERENCES t_dish(id) ) ENGINE=InnoDB; CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINE=InnoDB; CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, dish_id INT NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES t_order(id), CONSTRAINT fk_item_dish FOREIGN KEY (dish_id) REFERENCES t_dish(id) ) ENGINE=InnoDB;这里有几个参数值得单独说明。字符集选择 utf8mb4,是因为它能存表情符号和特殊符号,老旧的 utf8 在插入"比萨🍕"这类文本时会报错。金额字段选 DECIMAL(10,2) 而不是 FLOAT,是因为浮点数在计算总价时会出现 0.1+0.2 不等于 0.3 的精度问题,做订单系统用货币精度的 DECIMAL 才稳。每张表都指定 ENGINE=InnoDB,是为了让事务和外键约束真正生效,MyISAM 虽然读得快但不支持外键,项目里很容易出现孤儿数据。
注意:外键约束名在同一个数据库里必须全局唯一。我看到过有人把两张表的外键都命名为
fk_user,执行第二张表时直接报错。稳妥做法是每个外键都带表名和字段名,例如fk_cart_user、fk_item_order。
另外,order 是 MySQL 8.0 里的一个保留关键字,所以订单表名不能用order,很多人习惯写成t_order或tb_order来避开。如果你坚持用order做表名,每次写 SQL 都要加反引号,开发时很容易忘记。表名统一加t_前缀是这个项目里最常见的选择,能省掉不少麻烦。
3.3 初始化数据与联表查询的注意事项
建完表后,至少要往菜品表里插入几行数据,不然页面只能看到一个空壳。网上有些黑马 JavaWeb 笔记数据脚本里会插入几十道菜,我建议你第一次跑通时只插 6 到 8 行,减少排查干扰。下面是一段最小初始化数据:
INSERT INTO t_category (name) VALUES ('比萨'), ('小食'), ('饮料'); INSERT INTO t_dish (category_id, name, price, description) VALUES (1, '超级至尊比萨', 79.00, '火腿、蘑菇、青椒'), (1, '海鲜比萨', 89.00, '虾仁、鱿鱼、蟹柳'), (2, '黄金鸡块', 25.00, '外酥里嫩'), (3, '可乐', 12.00, '冰镇更爽');执行完初始化数据后,记得用一条联表查询验证表关系是否正确。比如要展示"菜单页面:菜品名、分类名、价格",常见做法是让 SQL 同时关联菜品表和分类表:
SELECT d.id, d.name, c.name AS category_name, d.price FROM t_dish d JOIN t_category c ON d.category_id = c.id ORDER BY c.id, d.id;这条 SQL 虽然不是代码,但它帮你在写 Servlet 之前先确认了字段名和关联方式。如果这里能查出正确结果,后面的 DAO 层只要照抄字段名就不会出大问题;如果这里报Unknown column,说明你的 Java 代码里某个实体类字段和表字段对不上,先去改实体类或者 SQL 别名,而不是在代码里打补丁。记住这个原则:数据库能查出来的,Java 才可能查到。
4. 从登录到下单:核心业务代码如何一步步落地
看完表结构,下一步就是把用户真正能用起来的功能点亮。这一章我按"登录 → 看菜单 → 加购物车 → 提交订单"的顺序讲,这几个功能覆盖了 JavaWeb 项目最核心的 Session 管理、JDBC 查询和事务控制。
4.1 登录与 Session 管理:一段最常见的 doPost 代码
登录是几乎所有在线系统的入口。常见的做法是前端表单把用户名和密码 POST 到 LoginServlet,Servlet 校验通过后把用户对象放进 Session,后续页面就能通过 session.getAttribute 拿到当前登录人。下面这段代码是这类项目最标准的样子:
@WebServlet("/login") public class LoginServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 不设编码,中文用户名很容易变成乱码 req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao userDao = new UserDao(); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); // 重定向可以避免刷新页面时重复提交表单 resp.sendRedirect(req.getContextPath() + "/menu"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }这段代码有三个参数值得你关注。第一个是req.setCharacterEncoding("UTF-8"),它必须放在读取任何参数之前,否则 Tomcat 8 以下版本解析中文会乱码。第二个是sendRedirect和forward的区别:重定向是浏览器发第二次请求,地址栏会变,适合登录成功后的跳转;转发是服务端内部跳转,地址栏不变,适合把错误信息带回登录页。第三个是 Session 的作用范围,默认是会话级别,也就是浏览器不关就有效;如果你希望七天免登录,还得叠加 Cookie,那是后话。
4.2 菜品列表与按分类查询:JDBC 查询的封装方式
菜单页的核心是"根据分类 id 查出菜品列表"。你可以直接在 Servlet 里写 JDBC,但一个项目里有多个查询时,代码会越来越重。我一般会让 Servlet 只做参数接收和页面转发,真正的 SQL 扔给 DAO 层。下面是一个简化版的 DishServlet:
@WebServlet("/menu") public class DishServlet extends HttpServlet { private DishService dishService = new DishService(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String kindId = req.getParameter("kindId"); // 没有分类参数时传 null,DAO 里判断后查询全部 List<Dish> dishes = dishService.findByCategory(kindId); req.setAttribute("dishes", dishes); req.getRequestDispatcher("/menu.jsp").forward(req, resp); } }对应的 DAO 里,使用 JDBC 的常见写法是这样的:
public List<Dish> findByCategory(String kindId) { List<Dish> list = new ArrayList<>(); String sql = "SELECT id, category_id, name, price, image FROM t_dish"; if (kindId != null && !kindId.isEmpty()) { sql += " WHERE category_id = ?"; } try (Connection conn = JdbcUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (kindId != null && !kindId.isEmpty()) { ps.setInt(1, Integer.parseInt(kindId)); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Dish dish = new Dish(); dish.setId(rs.getInt("id")); dish.setCategoryId(rs.getInt("category_id")); dish.setName(rs.getString("name")); dish.setPrice(rs.getBigDecimal("price")); dish.setImage(rs.getString("image")); list.add(dish); } } } catch (Exception e) { // 实际项目应该打日志,而不是简单 printStackTrace e.printStackTrace(); } return list; }这里有两个容易写错的地方。第一,不要用字符串拼接 SQL 的WHERE category_id = " + kindId,那样会产生 SQL 注入风险;用占位符?配合 PreparedStatement 是标准做法。第二,Integer.parseInt(kindId)可能会抛 NumberFormatException,你在前端点击分类时传的是数字,但别人可能手动改 URL 传一个 abc,所以 DAO 里最好先做一次类型判断,或者让 Servlet 层直接捕获异常。对于这种课程设计体量的项目,PreparedStatement 加 try-with-resources 已经足够,不需要再引入连接池框架。
4.3 购物车与订单提交:事务处理怎么加
购物车有两种常见实现方式:一种是把购物车数据放在 Session 里,简单但换设备就丢;另一种是建一张 t_cart 表,这里代码演示的是落库方案。加购的 Servlet 大致是:拿到当前用户 id 和菜品 id,先查购物车有没有这条记录,有就把数量加一,没有则插入新行。这步操作本身不复杂,复杂的是提交订单时要把"插入订单、插入多行明细、清空购物车"放在同一个事务里。
我把最关键的 createOrder 方法抽出来:
public boolean createOrder(Order order, List<OrderItem> items) { Connection conn = null; try { conn = JdbcUtil.getConnection(); // 关闭自动提交,这三个操作要么全成功,要么全失败 conn.setAutoCommit(false); // 1. 插入订单,拿到自增 id String insertOrder = "INSERT INTO t_order (order_no, user_id, total_amount, status) VALUES (?, ?, ?, 0)"; PreparedStatement psOrder = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, order.getOrderNo()); psOrder.setInt(2, order.getUserId()); psOrder.setBigDecimal(3, order.getTotalAmount()); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); int orderId = keys.next() ? keys.getInt(1) : 0; // 2. 插入订单明细 String insertItem = "INSERT INTO t_order_item (order_id, dish_id, price, quantity) VALUES (?, ?, ?, ?)"; PreparedStatement psItem = conn.prepareStatement(insertItem); for (OrderItem item : items) { psItem.setInt(1, orderId); psItem.setInt(2, item.getDishId()); psItem.setBigDecimal(3, item.getPrice()); psItem.setInt(4, item.getQuantity()); psItem.addBatch(); } psItem.executeBatch(); // 3. 清空购物车 PreparedStatement psCart = conn.prepareStatement("DELETE FROM t_cart WHERE user_id = ?"); psCart.setInt(1, order.getUserId()); psCart.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { JdbcUtil.close(conn); } }这段代码最核心的是第 3 行的conn.setAutoCommit(false)。默认情况下,每条 SQL 执行完就自动提交,如果插入订单成功、插入明细失败,数据就残了:订单表有记录,明细表却什么都没有。把自动提交关掉,三个操作打包成一个原子操作,任何一步抛异常就回滚,这是订单系统的基本功。
注意:
.addBatch()能减少网络往返,但一次批处理的数据量过大时反而会拖慢内存和事务。这个项目里一个订单最多十几条明细,用批量插入没有问题;如果你以后在真实系统里处理批量导入,还需要给 batch 设置合适的上限。
.addBatch()是批量插入,避免在 for 循环里逐条 executeUpdate 造成大量网络往返。注意订单明细的 price 字段应该从数据库读出的当前价格传入,而不是直接取页面传过来的价格,否则前端可以篡改金额。真正上线时还要校验库存,但在这个练手项目里,能做到事务回滚,答辩时已经能让老师看出你懂原理了。
5. IDEA 运行 JavaWeb 项目的配置与踩坑:从解压到看到首页的 5 个坑
再好的代码,跑不起来就等于零。这一章专门回答"idea运行javaweb项目配置到底怎么弄"。我用的是 IntelliJ IDEA 加 Tomcat 8.5 加 MySQL 8.0 的组合,这也是大多数 JavaWeb 项目完整案例的标准环境。你在照着做之前,先检查自己电脑的 JDK 版本、Tomcat 版本和 MySQL 版本,版本不一致时,很多报错不是你代码的问题,而是组件不匹配。
5.1 新建运行配置:Tomcat、Artifact、JRE 三个必填项
用 IDEA 打开从 zip 解压出来的 Maven 项目后,第一步不是写代码,而是配运行环境。常见做法是在 Run 菜单里打开 Edit Configurations,新增一个 Tomcat Server -> Local。三个必填项分别是:
- Application server:选择本机 Tomcat 安装目录,IDEA 会自动识别版本。
- JRE:选择项目使用的 JDK,建议 JDK 8 或 11,Tomcat 8.5 配 JDK 8 最稳。
- Deployment:把项目 Artifact 以 exploded 方式部署,Application context 填
/pizza。
这三项缺一不可。很多人漏掉 Deployment,直接点运行,IDEA 虽然启动了 Tomcat,但浏览器访问http://localhost:8080/会看到 Tomcat 默认首页而不是你的项目首页。还有的人漏选 JRE,Tomcat 能启动但编译时找不到 servlet-api,一访问接口就 500。
配置完运行配置后,还要在 Deployment 里把pizza-online:war exploded关联到/pizza,这样访问地址才是http://localhost:8080/pizza/。如果你看到的是 404,先回到这个页面,看看是否真的有一个 artifact 被加进去了。这个操作就像插电源插头,看起来很简单,但它决定了整台机器能不能亮。
5.2 连接 MySQL 的配置文件与 web.xml 配置
项目能不能读到数据库,取决于 db.properties 里的连接参数。下面是一份典型配置:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/pizza?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456这里最常翻车的三个参数。第一,driver 类名:MySQL 8.0 的驱动是com.mysql.cj.jdbc.Driver,如果你用的是旧驱动com.mysql.jdbc.Driver,会提示 ClassNotFoundException。第二,serverTimezone=Asia/Shanghai不能少,MySQL 8.0 默认时区跟中国本地时间不一致,不加会报时区错误。第三,useSSL=false是为了省去证书告警,你本地连数据库不需要 SSL 加密。
如果你的项目没有用 Maven,而是直接把 jar 包放在 web/WEB-INF/lib 下,记得确认 mysql-connector-j 的版本和 JDK 匹配。我见过一个项目里同时存在两个不同版本的 MySQL 驱动 jar 包,结果加载了错误的 Driver 类,搞得人一头雾水。这种黑匣子问题,最好的排查办法是先写一个最简单的 Java main 方法测试DriverManager.getConnection,能连上再让 Tomcat 去连。
再补一段老项目里常见的 web.xml。如果你用的是纯注解,web.xml 可以很薄,但很多 zip 里的项目为了兼容旧容器,仍然保留了完整的 Servlet 映射,你需要能看懂:
<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_3_1.xsd" version="3.1"> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.pizza.controller.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping> </web-app>看到<url-pattern>/login</url-pattern>就明白了:如果类上有@WebServlet("/login"),web.xml 里还重复配置,Tomcat 启动时可能会报重复映射。我在实战中一般只保留 welcome-file-list,Servlet 映射全部用注解。这样好处是 URL 和类定义在同一处,排查的时候不用来回翻文件。
5.3 常见问题排查:5 个坑的现象、原因与解决
下面这五条,基本覆盖了这类 JavaWeb 项目跑不起来时八成的问题。每一条都是我的血泪经验。
现象:Tomcat 能启动,浏览器全是 404。原因:Artifact 没有部署到运行配置里,或者请求路径跟 Servlet 的 @WebServlet 映射不一致。解决:重新打开 Run Configuration,确认 Deployment 里有 artifact;再检查访问地址,类上写的是
/login,请求就得是/pizza/login,多一层 context path 是正常的。现象:登录页输入中文用户名,跳转后变成乱码。原因:页面的 POST 请求编码和 Servlet 读取编码不一致。解决:在 Servlet 的 doPost 最开头加
req.setCharacterEncoding("UTF-8"),同时保证 JSP 页面头部也有<%@ page contentType="text/html;charset=UTF-8" %>。现象:IDEA 启动 Tomcat 时报端口 8080 被占用。原因:上一次没正常关停,或者别的程序占用了端口。解决:在 Tomcat conf/server.xml 里把 Connector 的 port 改成 8081,或者找到占用进程结束它。改端口后记得项目访问地址也要跟着变。
现象:Maven 依赖下载失败,pom.xml 里 servlet-api、mysql-connector 一直红。原因:中央仓库连接不稳定,或者本地仓库缓存了损坏的 jar。解决:在 Maven settings.xml 里配置阿里云镜像仓库,把原来的镜像注释掉,再执行 mvn clean 重新下载。这一步能解决绝大多数"依赖永远下载不下来"的玄学问题。
现象:启动时提示找不到 Driver,但在项目里明明看到 jar 包。原因:jar 包没有被打进最终的 artifact,或者作用域是 provided。解决:在 pom.xml 中确认 mysql-connector-j 的 scope 是 runtime 或 compile,然后重新 build artifact;如果是非 Maven 项目,把 jar 放进 WEB-INF/lib 后,在 Project Structure 里加入 Library。
这五条里,前两条占了我日常答疑的一半以上。你照着顺序检查,比漫无目的地百度关键词快很多。如果遇到没列出来的报错,我的习惯是先把 IDEA 的 Console 拉到最底部,看第一行Caused by,那个才是有价值的信息,而不是看着满屏的 at com.pizza... 发懵。
6. 把课程设计变成作品集项目:验证、打磨与答辩加分项
当你把系统跑起来,别急着关 IDEA,先照着用户流程自测一遍:注册、登录、选菜、加购物车、下单、再看订单列表。我习惯用一张表格记录每个功能点的预期结果和实际结果,能跑通只是第一步,真正加分的是你能说清楚每个报错背后的原因。为了让项目更耐看,三个常见的进阶做法是:密码明文改 MD5 加盐、菜单列表加分页、在 Filter 里统一做登录校验。这三个改动都不大,但能在答辩时展示你对安全、性能和重复代码的关注。
再有一个很实际的做法:给数据库脚本加一点演示数据,并在 README 里写明运行步骤,包括 JDK、Tomcat、MySQL 的版本。面试官或老师拿到这个 zip,能十分钟内按你的文档跑通,这个项目的好感度会明显上升。不要只丢一个"必杀技"给人家,运行环境不写,对方第一步就放弃了。
最后说点我自己的教训。第一次做这类系统时,我为了显得高级,在 JSP 里塞了大量 Java 脚本片段,结果一到答辩老师问"这段查询为什么不放 DAO",我支支吾吾答不上来。后来我学会一个习惯:每写完一个功能,先反问自己三个问题——这个请求是谁发起的?它经过了哪些层?每层改了什么?这三个问题能帮你避免把项目做成黑匣子。希望帮到你。
本文还有配套的精品资源,点击获取