简介:这是一套面向Java Web初学者与课程设计者的简易网上订餐系统源码,基于Java+JSP+MySQL技术栈实现,覆盖Servlet处理、MVC分层、数据库交互等核心Web开发实践。资源共1262个文件,含48个JSP页面(负责用户界面与交互)、44个Java类(如OrderController、UserDaoImpl、JDBCUtil等,体现业务逻辑与数据访问)、44个编译后class文件、42个jar依赖包,以及大量静态资源(491个CSS、415个PNG等)支撑前端展示,整体压缩包47.38MB,结构完整,含SQL建库脚本、Eclipse/IDEA项目配置文件及源码使用说明文档。已有385人学习下载,开箱即可部署运行,适合用于Java Web入门实训、课程设计参考或MVC模式理解实践,尤其便于对照源码梳理请求流转路径、数据库表关联及前后端协作机制。
1. 这不是“毕业设计摆设”:一个能真跑起来的 Java+JSP+MySQL 网上订餐系统,到底长什么样?
你搜“Java+JSP+MySQL 网上订餐系统源码”,十有八九点开的是压缩包名带“毕设”“课程设计”“简易版”的 ZIP 文件——解压后发现:首页能进,登录页能跳,但一点击“下单”就 500 错误;数据库建表脚本里user表缺address字段,order_item表没加外键约束;JSP 页面里混着<% out.print(request.getParameter("id")); %>这种裸输出,连基本 XSS 过滤都没有。这不是代码问题,是交付逻辑断层:它没按“可部署、可调试、可验证”的工程最小闭环来组织。我手上跑通过的这个版本(即标题所指.zip),核心价值不在“功能多全”,而在每个模块都留了可观察入口、每处报错都指向具体文件行号、每次数据变更都能在 MySQL 命令行里立刻查到对应记录。它适合三类人:刚学完 Servlet 生命周期想串起全流程的 Java 新手;需要快速搭个内部测试环境验证业务逻辑的中小团队后端;或是面试前想亲手 debug 一个真实 Web 层交互链路的求职者。别被“简单”二字骗了——它的价值恰恰藏在那些被教科书省略的胶水代码里:比如web.xml里<welcome-file-list>怎么和index.jsp的重定向形成首屏加载闭环,比如DBUtil.java里Connection关闭顺序为什么必须是rs → stmt → conn而不是反过来。接下来,我们就从解压那一刻开始,把它真正跑起来。
2. 解压即启动:用最原始的方式把这套 JSP 系统喂活
这套系统不依赖 Maven 或 Gradle,所有依赖都打在WEB-INF/lib/下,这是 JSP 时代最朴素的部署哲学:让 Tomcat 自己认出 classpath,而不是让构建工具替你猜。我们跳过任何 IDE 导入步骤,直接用命令行和文本编辑器建立掌控感。
2.1 拆包与目录结构校验:先看清它长什么骨架
解压后你会看到典型 Java Web 项目结构:
online-order-system/ ├── WebContent/ ← JSP、HTML、CSS、JS 存放地(Tomcat 静态资源根目录) │ ├── index.jsp │ ├── login.jsp │ ├── order.jsp │ └── WEB-INF/ │ ├── web.xml ← 核心配置:Servlet 映射、欢迎页、过滤器 │ ├── lib/ ← 所有 JAR 包:mysql-connector-java-5.1.47.jar、jstl-1.2.jar 等 │ └── classes/ ← 编译后的 .class 文件(含 DAO、Servlet、工具类) └── src/ ← 原始 Java 源码(供你修改后重新编译)提示:
WebContent是 Eclipse/MyEclipse 时代的标准 Web Root 名称,不是src/main/webapp。若你用 IntelliJ,需在 Project Structure → Modules → Web 中手动指定WebContent为 Web Resource Directory,否则 Tomcat 启动时找不到 JSP。
关键校验点(用ls或资源管理器确认):
WEB-INF/lib/下必须有mysql-connector-java-*.jar(版本 5.1.x 最稳,8.0+ 需改驱动类名);WEB-INF/classes/下应存在com/order/dao/OrderDAO.class、com/order/servlet/LoginServlet.class等编译产物;src/目录存在且包含com.order.dao、com.order.servlet等包结构——这是你后续修改的源头。
2.2 数据库初始化:用最直白的 SQL 脚本建库建表
系统自带db_init.sql(通常在根目录或doc/下),内容类似:
-- 创建数据库 CREATE DATABASE IF NOT EXISTS online_order DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE online_order; -- 用户表 CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, -- 注意:明文存密码!仅用于教学,生产必须 BCrypt 加密 phone VARCHAR(20), address TEXT ); -- 菜品表 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, description TEXT ); -- 订单主表 CREATE TABLE `order` ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status ENUM('pending','confirmed','delivered') DEFAULT 'pending', 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, dish_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL, -- 快照价格,避免菜品调价影响历史订单 FOREIGN KEY (order_id) REFERENCES `order`(id), FOREIGN KEY (dish_id) REFERENCES dish(id) );执行步骤(以 MySQL 8.0 为例):
# 1. 登录 MySQL(假设 root 密码为空) mysql -u root -p # 2. 执行建库建表(注意路径替换为你本地的 sql 文件绝对路径) source /path/to/your/online-order-system/db_init.sql; # 3. 验证表是否创建成功 USE online_order; SHOW TABLES; SELECT COUNT(*) FROM user; -- 应返回 0,初始无用户参数说明:
utf8mb4是必须的,否则中文菜名(如“宫保鸡丁”)可能乱码;ENUM类型用于status字段,比VARCHAR更节省空间且语义明确;order表名用反引号包裹,因order是 MySQL 保留字。
2.3 Tomcat 配置:只改两处,让它认出你的应用
将整个online-order-system/文件夹(不是其子目录)复制到 Tomcat 的webapps/目录下,重命名为order(去掉空格和特殊字符)。然后检查两个关键配置:
conf/context.xml:确保<Context>标签内有reloadable="true"(开发阶段允许热加载):<Context reloadable="true"> <!-- 其他默认配置 --> </Context>conf/server.xml:确认Connector端口未被占用(默认 8080):<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />
启动 Tomcat:
# Linux/Mac $CATALINA_HOME/bin/startup.sh # Windows %CATALINA_HOME%\bin\startup.bat访问http://localhost:8080/order/—— 如果看到首页(通常是index.jsp),说明 Web 容器已加载成功。此时打开浏览器开发者工具 → Network 标签,刷新页面,观察index.jsp的响应状态码是否为 200,Response Headers 中是否有Content-Type: text/html;charset=UTF-8。这是第一个可验证的健康信号。
3. 从登录到下单:跟踪一次完整请求链路,看 Java+JSP 如何协作
这套系统的价值,在于你能清晰看到 HTTP 请求如何穿透 JSP → Servlet → DAO → MySQL。我们以“用户登录”为例,走一遍真实调用栈。
3.1 JSP 页面:不只是模板,更是请求发起者
打开WebContent/login.jsp,关键代码段:
<!-- login.jsp 第 25 行左右 --> <form action="LoginServlet" method="post"> <input type="text" name="username" placeholder="用户名" required> <input type="password" name="password" placeholder="密码" required> <button type="submit">登录</button> </form>注意action="LoginServlet"—— 这不是指向某个.jsp文件,而是直接提交给LoginServlet这个类。这意味着web.xml中必须有对应的<servlet-mapping>:
<!-- WEB-INF/web.xml --> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.order.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/LoginServlet</url-pattern> </servlet-mapping>逻辑说明:JSP 在这里只承担“渲染表单 + 发起 POST 请求”的职责,不处理任何业务逻辑。所有验证、查询、跳转都交给 Servlet。这是 MVC 中 View 和 Controller 的明确分界。
3.2 Servlet 处理:接收参数、调用 DAO、控制跳转
src/com/order/servlet/LoginServlet.java核心逻辑:
public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取表单参数(注意:request.setCharacterEncoding("UTF-8") 必须在 getParameter 前!) request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); // 2. 调用 DAO 查询用户(此处简化,实际应校验密码) UserDAO userDAO = new UserDAO(); User user = userDAO.findByUsername(username); // 3. 业务判断与跳转 if (user != null && password.equals(user.getPassword())) { // 登录成功:存入 session,重定向到首页 request.getSession().setAttribute("user", user); response.sendRedirect("index.jsp"); } else { // 登录失败:设置错误信息,转发回 login.jsp(保持 URL 不变) request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } } }参数说明:
request.setCharacterEncoding("UTF-8")是中文参数不乱码的关键,必须在getParameter()之前调用;response.sendRedirect()是客户端重定向(URL 变),request.getRequestDispatcher().forward()是服务器端转发(URL 不变),二者语义完全不同;session.setAttribute()将用户对象存入会话,后续页面可通过session.getAttribute("user")获取。
3.3 DAO 层:用 JDBC 原生操作 MySQL,理解连接池为何重要
src/com/order/dao/UserDAO.java中findByUsername方法:
public User findByUsername(String username) { String sql = "SELECT id, username, password, phone, address FROM user WHERE username = ?"; User user = null; Connection conn = null; PreparedStatement pstmt = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); // 从连接池获取连接 pstmt = conn.prepareStatement(sql); pstmt.setString(1, username); // 防止 SQL 注入 rs = pstmt.executeQuery(); if (rs.next()) { user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setPassword(rs.getString("password")); user.setPhone(rs.getString("phone")); user.setAddress(rs.getString("address")); } } catch (SQLException e) { e.printStackTrace(); // 开发期打印,生产应记日志 } finally { // 必须按 rs → pstmt → conn 顺序关闭!否则连接泄漏 DBUtil.close(rs, pstmt, conn); } return user; }逻辑说明:
PreparedStatement预编译 SQL,天然防注入;DBUtil.close()是封装好的关闭方法,其内部严格遵循if (rs != null) rs.close()→if (pstmt != null) pstmt.close()→if (conn != null) conn.close()顺序。若颠倒(如先关 conn),rs和pstmt就无法关闭,导致连接池耗尽。
4. 避坑指南:这 4 个错误让 90% 的人卡在启动第一分钟
这套系统看似简单,但新手常因环境细节翻车。以下是我在带实习生时统计出的最高频、最隐蔽的 4 个坑,每个都附带现象、根因和一招解决。
4.1 现象:访问http://localhost:8080/order/报 404,但http://localhost:8080/能看到 Tomcat 欢迎页
原因:online-order-system文件夹未直接放在webapps/下,而是放在了webapps/online-order-system/子目录中,导致 Tomcat 将其识别为context path为/online-order-system,而非/order。
解决:将文件夹重命名为order,并确保路径为TOMCAT_HOME/webapps/order/(不是webapps/online-order-system/)。验证方式:启动后查看TOMCAT_HOME/logs/catalina.out,搜索Deploying web application directory,确认日志中出现Deploying web application directory [/path/to/tomcat/webapps/order]。
4.2 现象:登录时输入中文用户名,后台查不到,数据库里存的是乱码(如æŽå)
原因:MySQL 服务端、数据库、表、连接 URL 四层编码未统一为utf8mb4。常见漏点是 JDBC URL 缺少?useUnicode=true&characterEncoding=utf8mb4参数。
解决:修改src/com/order/util/DBUtil.java中的连接字符串:
// 错误写法(缺参数) private static final String URL = "jdbc:mysql://localhost:3306/online_order"; // 正确写法(补全编码参数) private static final String URL = "jdbc:mysql://localhost:3306/online_order?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai";同时确认 MySQL 配置文件my.cnf中[mysqld]段落有:
[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci4.3 现象:点击“下单”按钮后页面空白,Tomcat 日志报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver
原因:MySQL 8.0+ 驱动类名从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,而源码中仍用旧类名。
解决:打开src/com/order/util/DBUtil.java,找到Class.forName("com.mysql.jdbc.Driver");这行,改为:
// MySQL 5.1.x 用此行 // Class.forName("com.mysql.jdbc.Driver"); // MySQL 8.0+ 必须用此行 Class.forName("com.mysql.cj.jdbc.Driver");并确认WEB-INF/lib/下的mysql-connector-javaJAR 版本 ≥ 8.0(如mysql-connector-java-8.0.33.jar)。
4.4 现象:登录成功后跳转到index.jsp,但页面顶部显示null,用户信息未渲染
原因:index.jsp中读取 session 的代码写成了<%= session.getAttribute("user").getUsername() %>,但未判空。当 session 中user为 null 时,JSP 编译后抛NullPointerException,页面中断渲染。
解决:在index.jsp顶部添加空值校验:
<% User user = (User) session.getAttribute("user"); if (user == null) { response.sendRedirect("login.jsp"); return; // 立即终止后续 JSP 执行 } %> <!-- 后续安全使用 user.getUsername() --> 欢迎,<%= user.getUsername() %>!5. 让它真正可用:三个必须动手改的“生产级补丁”
这套源码的教学价值在于“可调试”,但离“可交付”还差三步。我不会教你加 Spring Boot 或 Vue,而是用最原始的 Java+JSP 方式,打上三个低成本、高回报的补丁。改完后,它就能在测试环境稳定跑一周以上。
5.1 补丁一:给所有表加created_at和updated_at时间戳字段
当前user、dish、order表都没有时间记录,导致无法追溯数据何时创建/修改。手动为每张表添加:
-- 对 user 表 ALTER TABLE user ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP, ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; -- 对 dish 表 ALTER TABLE dish ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP, ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; -- 对 order 表(注意:order 是保留字,需加反引号) ALTER TABLE `order` ADD COLUMN created_at DATETIME DEFAULT CURRENT_TIMESTAMP, ADD COLUMN updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;为什么重要:
ON UPDATE CURRENT_TIMESTAMP是 MySQL 的自动更新机制,无需在 Java 代码中手动 set 时间。后续所有 INSERT/UPDATE 操作,数据库会自动维护updated_at。这是审计追踪的基石,也是排查“谁在什么时候改了数据”的唯一依据。
5.2 补丁二:在 DAO 层统一加入日志输出,让每次数据库操作可见
在src/com/order/dao/BaseDAO.java(或每个 DAO 的基类)中,添加日志方法:
import java.util.logging.Logger; public abstract class BaseDAO { protected static final Logger logger = Logger.getLogger(BaseDAO.class.getName()); protected void logSQL(String sql, Object... params) { StringBuilder sb = new StringBuilder("Executing SQL: ").append(sql); if (params.length > 0) { sb.append(" with params: "); for (int i = 0; i < params.length; i++) { sb.append(params[i]); if (i < params.length - 1) sb.append(", "); } } logger.info(sb.toString()); } }然后在UserDAO.findByUsername()开头加入:
logSQL("SELECT id, username, password, phone, address FROM user WHERE username = ?", username);效果:启动 Tomcat 后,
logs/catalina.out里会出现类似INFO: Executing SQL: SELECT ... with params: 张三的日志。当你发现某个页面加载慢,直接 grep 日志就能定位是哪条 SQL 慢,而不是靠猜。
5.3 补丁三:用Filter统一处理中文乱码,告别每个 Servlet 写setCharacterEncoding
创建src/com/order/filter/EncodingFilter.java:
public class EncodingFilter implements Filter { public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; // 统一设置请求和响应编码 req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }并在web.xml中注册:
<filter> <filter-name>EncodingFilter</filter-name> <filter-class>com.order.filter.EncodingFilter</filter-class> </filter> <filter-mapping> <filter-name>EncodingFilter</filter-name> <url-pattern>/*</url-pattern> <!-- 拦截所有请求 --> </filter-mapping>玄学经验:这个 Filter 必须放在
web.xml中所有其他 Filter(如登录拦截)之前,否则后续 Filter 获取的request.getParameter()仍是乱码。它是整个 Web 层的“编码守门员”,一劳永逸。
6. 验证与压测:用 curl 和 MySQL 命令行做最小闭环测试
最后一步,别急着点鼠标。用两条命令,证明你的系统真的“活”了,且数据流是通的。这是工程师的“后悔药”——当 UI 出问题时,你能绕过前端,直击后端和数据库。
6.1 用 curl 模拟登录,验证 Servlet 接口
# 发送 POST 登录请求(假设已有用户 'admin' 密码 '123') curl -X POST http://localhost:8080/order/LoginServlet \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" \ -d "password=123" \ -i # -i 参数显示响应头,确认是否 302 重定向预期响应:
HTTP/1.1 302 Found Location: index.jsp Set-Cookie: JSESSIONID=ABC123...; Path=/order; HttpOnly关键点:看到
302 Found和Location: index.jsp,说明 Servlet 逻辑正确;Set-Cookie头证明 session 已创建。如果返回200 OK且 body 是 HTML,说明response.sendRedirect()未生效,可能是web.xml中LoginServlet的 mapping 路径错了。
6.2 用 MySQL 命令行验证数据写入
登录 MySQL,执行:
USE online_order; -- 查看用户表(确认 admin 用户存在) SELECT id, username, created_at FROM user WHERE username = 'admin'; -- 查看最近一条订单(模拟下单后) SELECT o.id, o.total_price, o.status, u.username FROM `order` o JOIN user u ON o.user_id = u.id ORDER BY o.created_at DESC LIMIT 1;参数说明:
LIMIT 1配合ORDER BY ... DESC是查“最新一条”的标准写法;JOIN语句把订单和用户信息关联,一眼看出是谁下的单。如果这里查不到数据,说明 DAO 层的insert方法没执行,或事务未提交(检查OrderDAO.addOrder()中是否有conn.commit())。
6.3 一个血泪经验:永远在finally块里关连接,哪怕只有一行代码
我见过太多人把conn.close()写在try块末尾,结果rs.next()抛异常后,conn就永远不关闭了。正确的姿势只有一种:
Connection conn = null; try { conn = DBUtil.getConnection(); // ... 执行查询 } catch (SQLException e) { e.printStackTrace(); } finally { if (conn != null) { try { conn.close(); // 这里必须判空再关 } catch (SQLException e) { e.printStackTrace(); } } }但这还不够——真正的工业级写法是DBUtil.close(rs, pstmt, conn),因为它把“判空 + 关闭 + 捕获异常”封装成一行。你不需要记住顺序,只需要相信这个方法。这就是为什么我坚持让你从DBUtil.java开始读源码:它不是装饰,是系统的呼吸节律。
希望帮到你。
本文还有配套的精品资源,点击获取