JavaWeb校园订餐系统实战:从Servlet到MVC的完整项目构建

JavaWeb校园订餐系统实战:从Servlet到MVC的完整项目构建 简介本资源是一套完整的基于JavaWeb技术的校园订餐系统毕业设计级项目源码面向Java初学者、高校计算机专业学生及JavaWeb课程实践者旨在通过真实业务场景帮助学习者掌握Servlet、JSP与MySQL整合开发的核心流程。压缩包共723个文件主体包含68个JSP页面实现登录、菜单展示、订单管理等交互视图、32个Java类含Servlet控制器与Model实体类、48个HTML静态页、181个JS脚本支撑前端交互逻辑、47个JPG图片资源及7个XML配置文件如web.xml与数据库连接配置整体大小为9.8MB。已有219人下载学习。源码结构清晰严格遵循MVC分层设计涵盖用户认证、菜品查询、购物车管理、订单提交等完整业务链并附带SQL建表语句与EclipseTomcat开发环境适配配置便于直接导入运行与二次开发。1. 项目概述从零到一构建一个能跑起来的校园订餐系统最近在整理硬盘翻出来一个几年前带学生做的课程设计项目——一个基于JavaWeb的校园订餐系统。这个项目虽然算不上多么高精尖但它麻雀虽小五脏俱全从数据库设计、后端逻辑到前端页面完整地走通了一个Web应用从设计到实现的全流程。对于正在学习JavaWeb或者想找一个完整项目来练手、丰富简历的朋友来说这类“校园订餐系统”的源码和实现思路价值可能远超一个孤立的“Hello World”或零散的API练习。它帮你把Servlet、JSP、JDBC、MVC这些抽象的概念串联成一个看得见、摸得着、能点餐、能下单的真实系统。这个系统的核心目标很明确为校园内的师生提供一个在线订餐的平台。想象一下学生中午下课后不用挤食堂提前在手机上选好食堂档口的餐品下单支付算好时间直接去取或者选择配送如果系统支持。对食堂管理者而言也能提前预估备餐量减少浪费。我们当时实现的版本包含了用户学生/教职工注册登录、浏览餐厅与菜品、加入购物车、提交订单、模拟支付以及简单的后台管理管理菜品、查看订单等功能。技术栈就是最经典的JavaWeb三件套Servlet JSP MySQL开发工具是IntelliJ IDEA用Tomcat作为服务器。下面我就把这个项目的核心设计思路、关键代码实现、以及那些只有亲手做过才会遇到的“坑”和技巧系统地拆解一遍。2. 系统整体设计与架构拆解2.1 为什么选择经典MVC模式而非SSM/Spring Boot拿到“校园订餐系统”这个需求第一个要决定的就是技术架构。现在市面上Spring Boot大行其道为什么我们这个项目仍坚持使用最基础的ServletJSP这背后有几点教学和实战的考量。首先学习路径的平滑性。对于JavaWeb初学者直接上Spring Boot和SSMSpringSpringMVCMyBatis框架很容易被大量的注解、自动配置和“约定大于配置”的理念搞晕。你可能会成功运行一个项目但并不清楚HTTP请求到底是如何被接收、处理最后渲染成页面的。而Servlet是JavaWeb的基石所有高级框架最终都是在Servlet规范之上做的封装。从Servlet/JSP入手你能清晰地看到web.xml中的配置如何将URL映射到一个具体的Java类Servlet这个类如何获取请求参数request.getParameter如何处理业务逻辑最后如何跳转或重定向到JSP页面。这个过程虽然繁琐但能帮你建立最扎实的Web请求-响应生命周期认知。其次项目复杂度的匹配度。校园订餐系统的核心业务——用户管理、商品浏览、购物车、订单生成——在数据模型和交互逻辑上属于中等复杂度。它不需要微服务拆分不需要复杂的消息队列或分布式事务。经典的MVC模式完全能够胜任Servlet作为控制器Controller接收请求JavaBean或普通的POJO作为模型Model承载数据JSP作为视图View展示页面。这种清晰的分层让项目结构一目了然非常适合作为第一个综合性项目来理解MVC的职责分离思想。注意在真实的、迭代快速的商业项目中我们几乎一定会选择Spring Boot等框架因为它们提供了无与伦比的开发效率、生态集成和可维护性。但作为学习项目“从轮子造起”的经历无比珍贵。这就像学开车先用手动挡理解了离合、油门和变速箱的配合再开自动挡会更容易理解车的运行逻辑。2.2 核心功能模块与数据库设计蓝图在动手写代码之前我们必须把系统要做什么、数据怎么存想清楚。我们采用自顶向下的方式先梳理出核心功能模块再设计支撑这些功能的数据库表。核心功能模块主要分为前台用户端和后台管理端用户端用户模块注册、登录、个人信息管理。餐厅/菜品展示模块按食堂、分类如盖饭、面食展示菜品支持图片、价格、描述查看。购物车模块添加菜品、修改数量、删除菜品、清空购物车。订单模块从购物车生成订单选择送餐地址或取餐点、支付方式模拟查看订单历史及状态。管理端简易版菜品管理对菜品进行增删改查CRUD。订单管理查看所有订单更新订单状态如“已接单”、“配送中”、“已完成”。围绕这些功能我们设计了核心的几张数据库表。这里以MySQL为例展示关键表结构用户表 (user)CREATE TABLE user ( id int(11) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL UNIQUE COMMENT 登录用户名, password varchar(255) NOT NULL COMMENT 密码存储密文, real_name varchar(50) DEFAULT NULL COMMENT 真实姓名, phone varchar(20) DEFAULT NULL COMMENT 手机号, address varchar(255) DEFAULT NULL COMMENT 常用送餐地址, user_type tinyint(1) DEFAULT 0 COMMENT 用户类型0-普通用户1-管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;菜品表 (food)CREATE TABLE food ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 菜品名称, price decimal(10,2) NOT NULL COMMENT 价格, description text COMMENT 菜品描述, image_url varchar(500) DEFAULT NULL COMMENT 图片存储路径或URL, canteen_id int(11) DEFAULT NULL COMMENT 所属食堂ID, category_id int(11) DEFAULT NULL COMMENT 分类ID如盖饭、面食, stock int(11) DEFAULT 999 COMMENT 库存简化模型可无限, status tinyint(1) DEFAULT 1 COMMENT 状态1-上架0-下架, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表 (order) 与订单明细表 (order_item)这是核心的交易表。由于一个订单可能包含多个菜品我们采用主-子表结构来避免数据冗余。-- 订单主表 CREATE TABLE order ( id varchar(32) NOT NULL COMMENT 订单号可使用时间戳随机数生成, user_id int(11) NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 状态0-待支付1-已支付/待接单2-制作中3-配送中4-已完成5-已取消, address varchar(255) NOT NULL COMMENT 送餐地址, contact_phone varchar(20) NOT NULL COMMENT 联系电话, create_time datetime DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL COMMENT 支付时间, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单明细表 CREATE TABLE order_item ( id int(11) NOT NULL AUTO_INCREMENT, order_id varchar(32) NOT NULL COMMENT 关联订单号, food_id int(11) NOT NULL COMMENT 菜品ID, food_name varchar(100) NOT NULL COMMENT 下单时的菜品名称快照避免菜品信息变更影响历史订单, food_price decimal(10,2) NOT NULL COMMENT 下单时的菜品单价, quantity int(11) NOT NULL COMMENT 购买数量, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;实操心得在order_item表中冗余存储food_name和food_price是关键设计。菜品信息如名称、价格未来可能会变。如果只存food_id查询历史订单时就需要联表查询food表一旦菜品被删除或信息变更历史订单显示就会出错或不准确。这种“快照”设计保证了订单数据的永恒性和准确性是电商类系统的通用做法。3. 开发环境搭建与项目骨架构建3.1 使用IDEA创建JavaWeb项目的正确姿势工欲善其事必先利其器。我们选择IntelliJ IDEA Ultimate版社区版对JavaWeb支持稍弱作为开发工具。创建一个标准的JavaWeb项目我推荐以下步骤它能帮你建立一个结构清晰、易于管理的项目骨架。新建项目打开IDEA选择New Project。左侧选择Java Enterprise。确保已配置好JDK如JDK 8或11。在Application Server处点击New...选择你本地安装的Tomcat路径例如Apache Tomcat 9.0.x。勾选Web Application并确保版本是4.0对应Servlet 3.1规范。这里先不要勾选任何框架如Spring我们从头构建。配置项目名和路径输入项目名称例如CampusFoodOrdering选择项目存储位置。关键目录结构生成IDEA会自动生成一个基础结构。我们需要重点关注和调整以下目录src/main/java存放所有Java源代码包括Servlet、工具类、模型类等。src/main/resources存放配置文件如数据库连接配置db.properties、日志配置文件等。web或src/main/webapp这是Web应用的根目录。WEB-INF安全目录客户端无法直接访问。web.xml核心部署描述文件配置Servlet、Filter、Listener等。lib存放项目依赖的第三方JAR包如MySQL驱动mysql-connector-java-8.0.x.jar、数据库连接池如Druid或HikariCP的JAR包、JSTL标签库JAR包等。其他存放所有前端可见资源如JSP页面、CSS、JavaScript、图片等。通常我们会再创建子目录来分类例如css/,js/,images/,user/用户相关页面,admin/管理后台页面。引入必备JAR包将下载好的JAR包MySQL驱动、数据库连接池、JSTL复制到WEB-INF/lib目录下。在IDEA中这些库会被自动添加到项目的构建路径中。3.2 数据库连接池为什么不用DriverManager在早期的JDBC编程中我们习惯在每次需要操作数据库时使用DriverManager.getConnection()来获取一个连接用完后关闭。这在访问量极低的场景下没问题但对于一个Web应用哪怕是校园订餐系统这种方式的性能开销是巨大的。每次建立物理连接TCP三次握手、数据库权限验证等和关闭连接都非常耗时。数据库连接池Connection Pool就是为了解决这个问题。它预先创建并维护一定数量的数据库连接对象放在一个“池子”里。当应用需要连接时就从池中取出一个空闲的连接使用用完后不是真正关闭而是归还给池子。这避免了频繁创建和销毁连接的开销。我们选择阿里开源的Druid连接池它不仅高效稳定还提供了强大的监控功能。配置步骤如下在src/main/resources下创建db.properties文件# 数据库连接配置 jdbc.driverClassNamecom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/campus_food?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password # Druid连接池配置 druid.initialSize5 druid.minIdle5 druid.maxActive20 druid.maxWait60000 druid.timeBetweenEvictionRunsMillis60000 druid.minEvictableIdleTimeMillis300000 druid.validationQuerySELECT 1 druid.testWhileIdletrue druid.testOnBorrowfalse druid.testOnReturnfalseinitialSize池启动时创建的初始连接数。maxActive池中同时活动的最大连接数。根据预估的并发用户数设置校园系统20通常足够。maxWait当池中无可用连接时获取连接的最大等待时间毫秒超时则抛异常。创建一个工具类DBUtil.java在应用启动时通过ServletContextListener初始化Druid数据源并提供获取连接和关闭资源的方法。public class DBUtil { private static DataSource dataSource; // 初始化数据源通常在监听器中调用 public static void init(Properties props) { try { DruidDataSource druidDataSource new DruidDataSource(); druidDataSource.configFromPropety(props); // 从properties加载配置 // 或者手动设置druidDataSource.setUrl(...); druidDataSource.setUsername(...); ... dataSource druidDataSource; } catch (Exception e) { throw new RuntimeException(初始化数据库连接池失败, e); } } // 获取连接 public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } // 关闭资源Connection, Statement, ResultSet public static void close(Connection conn, Statement stmt, ResultSet rs) { // ... 标准的关闭逻辑注意处理null和异常 } }避坑指南务必在web.xml中配置一个ServletContextListener在Web应用启动时调用DBUtil.init()来初始化连接池。同时要确保在应用关闭时销毁连接池释放资源。否则可能会造成连接泄漏最终导致数据库连接数耗尽。Druid提供了DruidDataSource的close()方法用于销毁。4. 核心业务逻辑与Servlet实现详解4.1 用户登录与会话管理Cookie与Session的抉择用户登录是系统的门户。核心流程是用户在登录页提交用户名和密码后端验证通过后创建该用户的会话Session并将用户标识如user_id存入会话中。后续的请求通过检查会话中是否存在用户标识来判断用户是否已登录以及是谁。这里涉及一个关键问题用户状态如何维持主要有Cookie和Session两种机制。Cookie将数据存储在客户端浏览器。每次请求浏览器会自动携带该站点的Cookie。优点是减轻服务器压力。缺点是数据存储在客户端不安全敏感信息不能存且大小有限约4KB。Session将数据存储在服务器端内存、Redis等并为每个会话生成一个唯一的JSESSIONID通过Cookie或URL重写将这个ID传递给客户端。客户端下次请求时携带此ID服务器就能找到对应的会话数据。优点是数据安全存储量大。对于登录状态我们绝对使用Session。因为登录状态是敏感信息必须由服务器控制。流程如下登录Servlet (LoginServlet)WebServlet(/login) // 使用注解配置替代web.xml public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); // 1. 参数校验略 // 2. 查询数据库验证用户名和密码密码需比对密文 User user userService.login(username, password); if (user ! null) { // 3. 登录成功将用户对象存入Session HttpSession session request.getSession(); session.setAttribute(currentUser, user); // 4. 重定向到首页或目标页面 response.sendRedirect(request.getContextPath() /index.jsp); } else { // 登录失败设置错误信息并转发回登录页 request.setAttribute(errorMsg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } }登录状态检查对于需要登录才能访问的页面如购物车、订单页可以编写一个过滤器Filter统一处理。WebFilter(/*) // 过滤所有请求 public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); // 不创建新session String uri request.getRequestURI(); // 排除不需要过滤的路径登录、注册、静态资源等 if (uri.contains(/login) || uri.contains(/register) || uri.contains(.css) || uri.contains(.js) || uri.contains(.jpg)) { chain.doFilter(request, response); return; } // 检查session中是否有用户 if (session null || session.getAttribute(currentUser) null) { // 未登录重定向到登录页 response.sendRedirect(request.getContextPath() /login.jsp); return; } // 已登录放行 chain.doFilter(request, response); } }重要技巧session.setAttribute(“currentUser”, user)中存储的user对象建议只存储必要信息如id, username, userType而不是整个包含密码的User对象。同时用户密码在数据库存储时必须加密通常使用BCrypt或MD5加盐绝对不要明文存储。4.2 购物车设计与实现Session与数据库的权衡购物车是电商系统的核心组件之一。它的设计主要有两种思路基于Session和基于数据库。基于Session将购物车数据如商品ID、数量直接存储在用户的HttpSession中。优点是性能极高内存操作实现简单用户无需登录即可添加商品访客购物车。缺点是数据易失用户关闭浏览器或Session过期购物车数据就丢失且无法在多设备间同步。基于数据库在数据库中创建一张cart表关联user_id。优点是数据持久化用户在任何设备登录都能看到自己的购物车生命周期长。缺点是增加了数据库压力且需要用户先登录。对于校园订餐系统我们采用一种混合策略这也是很多中型系统的常见做法用户未登录时购物车数据存储在Session中。结构可以是一个MapfoodId, quantity。用户登录时触发一个“购物车合并”操作。检查Session中是否有临时购物车如果有则将其中的商品与数据库中该用户的持久化购物车进行合并相同商品数量相加不同商品新增然后清空Session中的购物车。之后的所有操作都针对数据库购物车。这里给出Session购物车的简化实现// 在添加购物车的Servlet中 HttpSession session request.getSession(); MapInteger, Integer cart (MapInteger, Integer) session.getAttribute(cart); if (cart null) { cart new HashMap(); session.setAttribute(cart, cart); } int foodId Integer.parseInt(request.getParameter(foodId)); int quantity Integer.parseInt(request.getParameter(quantity)); // 合并数量 cart.put(foodId, cart.getOrDefault(foodId, 0) quantity);在JSP页面中可以通过EL表达式${sessionScope.cart}来获取并展示购物车内容。踩坑记录直接使用Map存储购物车在集群部署时会出问题。因为Session默认是存储在单个服务器内存中的。如果用户第一次请求打到服务器A购物车存在A的Session里下次请求通过负载均衡打到服务器BB的Session里没有购物车数据。解决方案是使用Session共享技术如将Session存储到Redis中。对于学习项目我们暂不考虑集群但这是迈向实战必须知道的一点。4.3 订单生成与并发控制防止超卖的基础思想用户提交购物车生成订单这是整个系统最核心的交易环节。这里隐藏着一个经典的并发问题超卖。假设某热门菜品“红烧肉”最后一份库存为1两个用户A和B同时读取库存都为1都成功下单导致库存减为-1这就发生了超卖。在JavaWeb层面我们可以在应用层做简单的乐观锁控制。这需要改造数据库表和下单逻辑。为food表增加一个版本号字段或使用库存本身作为版本控制ALTER TABLE food ADD COLUMN version INT DEFAULT 0; -- 或者利用库存本身在减库存时条件加上where stock old_stock在下单Servlet中使用事务和乐观锁Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查并预扣库存使用版本号 String checkSql SELECT stock, version FROM food WHERE id ? FOR UPDATE; // 悲观锁写法更直接 // 或者乐观锁写法UPDATE food SET stock stock - ?, version version 1 WHERE id ? AND stock ? AND version ? // 这里为了简单可靠直接使用FOR UPDATE进行行锁悲观锁 PreparedStatement pstmt conn.prepareStatement(checkSql); pstmt.setInt(1, foodId); ResultSet rs pstmt.executeQuery(); if (rs.next()) { int stock rs.getInt(stock); if (stock quantity) { throw new RuntimeException(库存不足); } // 2. 扣减库存 String updateSql UPDATE food SET stock stock - ? WHERE id ?; // ...执行更新 // 3. 生成订单主表记录 // 4. 生成订单明细记录 // 5. 清空用户购物车 conn.commit(); // 提交事务 } } catch (Exception e) { if (conn ! null) conn.rollback(); // 回滚事务 throw e; } finally { DBUtil.close(conn, ...); }SELECT ... FOR UPDATE会在事务中给这行数据加上排他锁其他事务必须等待该事务结束才能读取或修改这行数据从而避免了并发修改。这是一种悲观锁的实现在并发不极高的场景下简单有效。性能考量FOR UPDATE锁行在高并发抢购场景下可能成为瓶颈导致大量请求排队。更高级的方案是使用Redis预减库存、消息队列异步下单等。但对于校园订餐系统悲观锁已足够可靠。关键在于所有库存变更操作必须在同一个数据库事务中完成这是保证数据一致性的生命线。5. 前端页面交互与JSP核心技巧5.1 告别ScriptletEL表达式与JSTL标签库的优雅使用早期的JSP页面中充斥着大量的Java代码块% ... %这被称为Scriptlet。它使得页面逻辑混乱难以维护也违背了MVC中视图层应尽量简洁的原则。现代JavaWeb开发强烈建议使用EL表达式Expression Language和JSTLJSP Standard Tag Library来替代Scriptlet。EL表达式用于在JSP中方便地访问域对象pageScope, requestScope, sessionScope, applicationScope中的数据。语法非常简单${表达式}。例如在Servlet中request.setAttribute(“foodList”, foodList)在JSP中可以直接遍历c:forEach items“${foodList}” var“food”...。它支持属性访问、集合操作、算术和关系运算非常强大。JSTL标签库提供了一系列常用的标签如循环、判断、格式化等。最常用的是核心标签库 (core)。引入JSTL将jstl-1.2.jar放入WEB-INF/lib。在JSP页面头部引入% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %常用标签示例条件判断c:if test${sessionScope.currentUser.userType 1} a hrefadmin/food_list.jsp进入后台/a /c:if c:choose c:when test${order.status 0}待支付/c:when c:when test${order.status 1}待接单/c:when c:otherwise已完成/c:otherwise /c:choose循环遍历table c:forEach items${requestScope.foodList} varfood varStatusstatus tr td${status.index 1}/td tdimg src${food.imageUrl} width50/td td${food.name}/td tdfmt:formatNumber value${food.price} pattern#,##0.00//td td input typenumber value1 min1 idqty_${food.id} button onclickaddToCart(${food.id})加入购物车/button /td /tr /c:forEach /table格式化fmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm:ss/可以很好地格式化日期。5.2 利用JSPAjax实现动态购物车与无刷新体验为了提升用户体验我们不应该在每次添加购物车或更新数量时都刷新整个页面。这时就需要用到AjaxAsynchronous JavaScript and XML技术。其核心是通过JavaScript的XMLHttpRequest对象或更现代的fetchAPI在后台与服务器交换数据并局部更新页面。场景点击“加入购物车”按钮无刷新更新页面右上角购物车图标上的数量。前端JSP页面中的JavaScriptfunction addToCart(foodId) { let quantity document.getElementById(qty_ foodId).value; // 使用fetch API发送POST请求 fetch(${pageContext.request.contextPath}/cart/add, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded, }, body: foodId foodId quantity quantity }) .then(response response.json()) // 期望服务器返回JSON .then(data { if (data.success) { // 更新页面上的购物车数量显示 document.getElementById(cartCount).innerText data.totalCount; alert(添加成功); } else { alert(添加失败 data.message); } }) .catch(error console.error(Error:, error)); }后端CartAddServletWebServlet(/cart/add) public class CartAddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { // ... 处理添加购物车逻辑 MapString, Object result new HashMap(); result.put(success, true); result.put(message, 添加成功); result.put(totalCount, cartService.getTotalCount(userId)); // 计算购物车总件数 // 设置响应内容类型为JSON response.setContentType(application/json;charsetUTF-8); // 将result Map转换为JSON字符串并输出 PrintWriter out response.getWriter(); // 可以使用Gson或Jackson库这里简单拼接 out.print({\success\:true, \totalCount\: totalCount }); out.flush(); } }实操心得处理Ajax请求的Servlet通常不再进行页面跳转forward或redirect而是直接向响应流中写入数据JSON或XML。务必设置正确的Content-Type如application/json。同时要处理好字符编码防止中文乱码。可以在Servlet开头统一设置request.setCharacterEncoding(“UTF-8”);和response.setCharacterEncoding(“UTF-8”);。6. 项目部署、优化与常见问题排查6.1 从IDEA到Tomcat项目打包与部署实战开发完成后我们需要将项目部署到独立的Tomcat服务器上模拟真实环境。项目打包JavaWeb项目标准的打包格式是WARWeb Application Archive文件。在IDEA中可以通过Build - Build Artifacts...来生成WAR包。更规范的做法是使用Maven或Gradle进行构建管理。如果项目是Maven项目只需在项目根目录运行mvn clean package就会在target目录下生成项目名.war文件。部署到Tomcat将生成的CampusFoodOrdering.war文件复制到Tomcat的webapps目录下。启动Tomcat运行bin/startup.bat(Windows) 或bin/startup.sh(Linux/Mac)。Tomcat会自动解压WAR包。访问http://localhost:8080/CampusFoodOrdering即可。数据库初始化确保部署服务器的MySQL中已经创建了对应的数据库campus_food并执行了建表SQL脚本。连接池的配置文件db.properties中的JDBC URL、用户名和密码需要根据部署环境进行修改。6.2 性能与安全优化要点一个能跑起来的系统是第一步一个跑得稳、跑得安全的系统才是目标。性能方面数据库优化索引为经常用于查询条件WHERE、排序ORDER BY和连接JOIN的字段创建索引。例如order表的user_id和create_timeorder_item表的order_id。SQL语句避免使用SELECT *只查询需要的列。注意JOIN的效率大数据表关联需谨慎。连接池监控利用Druid的监控功能定期查看连接池活跃数、等待数防止连接泄漏。前端优化静态资源缓存在Tomcat或Nginx中配置CSS、JS、图片的缓存策略减少重复请求。图片处理菜品图片不要直接存储大图应使用缩略图在前端展示。可以使用GraphicsMagick或ImageMagick在服务器端生成。JSP预编译在生产环境中可以预编译JSP文件避免首次访问时的编译开销。安全方面SQL注入防护必须使用PreparedStatement绝对不要用字符串拼接SQL。这是底线。// 错误存在SQL注入风险 String sql “SELECT * FROM user WHERE username” username “ AND password” password “”; // 正确 String sql “SELECT * FROM user WHERE username? AND password?”; PreparedStatement pstmt conn.prepareStatement(sql); pstmt.setString(1, username); pstmt.setString(2, encryptedPassword);XSS跨站脚本攻击防护对用户输入并显示在页面的内容进行转义。JSTL的c:out value“${userInput}”/默认有转义功能。或者使用OWASP Java Encoder库。密码安全使用强哈希算法如BCrypt加盐存储密码。MD5已不安全不推荐。文件上传如果允许上传菜品图片必须严格限制文件类型检查后缀和MIME Type、文件大小并重命名文件避免原始文件名冲突和脚本文件上传存储路径不要放在Web应用可直接访问的目录下。6.3 开发与部署中的常见问题速查表在开发和部署这个系统的过程中你几乎一定会遇到下面这些问题。这里我整理了一份速查表附上原因和解决方案。问题现象可能原因排查与解决思路访问JSP页面报404错误1. URL路径错误。2. 文件未放在webapp或其子目录下。3. Tomcat未正确部署或启动。1. 检查浏览器地址栏路径是否与文件实际路径一致。2. 确认JSP文件在webapp目录下。3. 查看Tomcat日志logs/catalina.out确认应用上下文路径。页面中文乱码1. JSP页面编码、请求编码、响应编码、数据库编码不一致。2. 未设置request.setCharacterEncoding(“UTF-8”)。1. 确保所有JSP页面% page pageEncoding“UTF-8”%。2. 编写一个编码过滤器Filter统一设置request和response的编码为UTF-8。3. 确保MySQL数据库、表、连接字符串characterEncodingUTF-8都使用UTF-8。ClassNotFoundException: com.mysql.cj.jdbc.Driver1. MySQL驱动JAR包未放入WEB-INF/lib。2. 驱动版本与MySQL服务器版本不匹配。1. 检查WEB-INF/lib下是否有mysql-connector-java-8.0.x.jar。2. 对于Tomcat确保JAR包在应用的lib目录而不是Tomcat的lib目录。3. 尝试更换驱动版本。连接数据库失败提示“Access denied”或“Unknown database”1.db.properties中的用户名、密码错误。2. 数据库名错误或数据库未创建。3. MySQL用户权限不足。1. 仔细核对配置文件的用户名、密码、数据库名。2. 使用MySQL客户端如Navicat、命令行尝试用相同信息连接。3. 检查MySQL用户是否有远程连接权限如果服务器和数据库不在同一台机器。修改了Java代码或JSP但刷新页面没变化1. IDEA没有自动编译部署到Tomcat。2. Tomcat开启了缓存。1. 在IDEA中检查Build - Build Project是否执行。2. 检查Tomcat的server.xml中Context的reloadable是否设为true开发环境。3. 最直接的方法重启Tomcat。提交表单后Servlet获取到的参数为null1. 表单method不是POSTServlet只处理了doPost。2. 表单字段的name属性与Servlet中getParameter的键名不匹配。3. 未设置请求编码导致中文参数乱码进而为null。1. 检查表单的method”post”并在Servlet中重写doPost方法。2. 核对表单input name”username”和request.getParameter(“username”)。3. 在Servlet最开头添加request.setCharacterEncoding(“UTF-8”);。页面提示“No suitable driver found for jdbc:mysql://...”JDBC连接URL格式错误或驱动未正确加载。1. 检查URL格式特别是MySQL 8.0需要时区参数serverTimezoneAsia/Shanghai。2. 确保驱动类名正确com.mysql.cj.jdbc.Driver(8.0) 或com.mysql.jdbc.Driver(5.x)。3. 尝试在代码中显式加载驱动类Class.forName(“com.mysql.cj.jdbc.Driver”);新版驱动通常不需要。这个基于JavaWeb的校园订餐系统项目就像一辆手动挡的教练车。它没有自动挡Spring Boot的便捷但能让你透彻理解每一个换挡请求处理、每一次离合会话管理和油门数据库交互的配合。当你亲手把它从零搭建起来并解决了上述所有或大部分问题后你对JavaWeb开发的理解会深入骨髓。这时再去看那些现代化的框架你会恍然大悟明白它们到底在帮你简化什么封装什么。这份从底层构建系统的经验是任何现成框架都无法替代的财富。项目源码的价值不仅在于那几千行代码更在于你思考和解决上述每一个问题的过程。本文还有配套的精品资源点击获取