JavaWeb校园订餐系统实战:从Servlet到MVC的完整项目构建
2026/9/5 20:01:46 网站建设 项目流程

简介:本资源是一套完整的基于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建表语句与Eclipse+Tomcat开发环境适配配置,便于直接导入运行与二次开发。

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大行其道,为什么我们这个项目仍坚持使用最基础的Servlet+JSP?这背后有几点教学和实战的考量。

首先,学习路径的平滑性。对于JavaWeb初学者,直接上Spring Boot和SSM(Spring+SpringMVC+MyBatis)框架,很容易被大量的注解、自动配置和“约定大于配置”的理念搞晕。你可能会成功运行一个项目,但并不清楚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 核心功能模块与数据库设计蓝图

在动手写代码之前,我们必须把系统要做什么、数据怎么存想清楚。我们采用自顶向下的方式,先梳理出核心功能模块,再设计支撑这些功能的数据库表。

核心功能模块主要分为前台用户端和后台管理端:

  1. 用户端
    • 用户模块:注册、登录、个人信息管理。
    • 餐厅/菜品展示模块:按食堂、分类(如盖饭、面食)展示菜品,支持图片、价格、描述查看。
    • 购物车模块:添加菜品、修改数量、删除菜品、清空购物车。
    • 订单模块:从购物车生成订单,选择送餐地址(或取餐点)、支付方式(模拟),查看订单历史及状态。
  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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

菜品表 (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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

订单表 (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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

实操心得:在order_item表中冗余存储food_namefood_price是关键设计。菜品信息(如名称、价格)未来可能会变。如果只存food_id,查询历史订单时就需要联表查询food表,一旦菜品被删除或信息变更,历史订单显示就会出错或不准确。这种“快照”设计保证了订单数据的永恒性和准确性,是电商类系统的通用做法。

3. 开发环境搭建与项目骨架构建

3.1 使用IDEA创建JavaWeb项目的正确姿势

工欲善其事,必先利其器。我们选择IntelliJ IDEA Ultimate版(社区版对JavaWeb支持稍弱)作为开发工具。创建一个标准的JavaWeb项目,我推荐以下步骤,它能帮你建立一个结构清晰、易于管理的项目骨架。

  1. 新建项目:打开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),我们从头构建。
  2. 配置项目名和路径:输入项目名称,例如CampusFoodOrdering,选择项目存储位置。
  3. 关键目录结构生成:IDEA会自动生成一个基础结构。我们需要重点关注和调整以下目录:
    • src/main/java:存放所有Java源代码,包括Servlet、工具类、模型类等。
    • src/main/resources:存放配置文件,如数据库连接配置db.properties、日志配置文件等。
    • websrc/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/(管理后台页面)。
  4. 引入必备JAR包:将下载好的JAR包(MySQL驱动、数据库连接池、JSTL)复制到WEB-INF/lib目录下。在IDEA中,这些库会被自动添加到项目的构建路径中。

3.2 数据库连接池:为什么不用DriverManager?

在早期的JDBC编程中,我们习惯在每次需要操作数据库时,使用DriverManager.getConnection()来获取一个连接,用完后关闭。这在访问量极低的场景下没问题,但对于一个Web应用,哪怕是校园订餐系统,这种方式的性能开销是巨大的。每次建立物理连接(TCP三次握手、数据库权限验证等)和关闭连接都非常耗时。

数据库连接池(Connection Pool)就是为了解决这个问题。它预先创建并维护一定数量的数据库连接对象,放在一个“池子”里。当应用需要连接时,就从池中取出一个空闲的连接使用,用完后不是真正关闭,而是归还给池子。这避免了频繁创建和销毁连接的开销。

我们选择阿里开源的Druid连接池,它不仅高效稳定,还提供了强大的监控功能。配置步骤如下:

  1. src/main/resources下创建db.properties文件:

    # 数据库连接配置 jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/campus_food?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=your_password # Druid连接池配置 druid.initialSize=5 druid.minIdle=5 druid.maxActive=20 druid.maxWait=60000 druid.timeBetweenEvictionRunsMillis=60000 druid.minEvictableIdleTimeMillis=300000 druid.validationQuery=SELECT 1 druid.testWhileIdle=true druid.testOnBorrow=false druid.testOnReturn=false
    • initialSize:池启动时创建的初始连接数。
    • maxActive:池中同时活动的最大连接数。根据预估的并发用户数设置,校园系统20通常足够。
    • maxWait:当池中无可用连接时,获取连接的最大等待时间(毫秒),超时则抛异常。
  2. 创建一个工具类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提供了DruidDataSourceclose()方法用于销毁。

4. 核心业务逻辑与Servlet实现详解

4.1 用户登录与会话管理:Cookie与Session的抉择

用户登录是系统的门户。核心流程是:用户在登录页提交用户名和密码,后端验证通过后,创建该用户的会话(Session),并将用户标识(如user_id)存入会话中。后续的请求,通过检查会话中是否存在用户标识来判断用户是否已登录以及是谁。

这里涉及一个关键问题:用户状态如何维持?主要有Cookie和Session两种机制。

  • Cookie:将数据存储在客户端浏览器。每次请求,浏览器会自动携带该站点的Cookie。优点是减轻服务器压力。缺点是数据存储在客户端,不安全(敏感信息不能存),且大小有限(约4KB)。
  • Session:将数据存储在服务器端(内存、Redis等),并为每个会话生成一个唯一的JSESSIONID,通过Cookie(或URL重写)将这个ID传递给客户端。客户端下次请求时携带此ID,服务器就能找到对应的会话数据。优点是数据安全,存储量大。

对于登录状态,我们绝对使用Session。因为登录状态是敏感信息,必须由服务器控制。流程如下:

  1. 登录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); } } }
  2. 登录状态检查:对于需要登录才能访问的页面(如购物车、订单页),可以编写一个过滤器(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。优点是数据持久化,用户在任何设备登录都能看到自己的购物车;生命周期长。缺点是增加了数据库压力,且需要用户先登录。

对于校园订餐系统,我们采用一种混合策略,这也是很多中型系统的常见做法:

  1. 用户未登录时:购物车数据存储在Session中。结构可以是一个Map<foodId, quantity>
  2. 用户登录时:触发一个“购物车合并”操作。检查Session中是否有临时购物车,如果有,则将其中的商品与数据库中该用户的持久化购物车进行合并(相同商品数量相加,不同商品新增),然后清空Session中的购物车。之后的所有操作都针对数据库购物车。

这里给出Session购物车的简化实现:

// 在添加购物车的Servlet中 HttpSession session = request.getSession(); Map<Integer, Integer> cart = (Map<Integer, 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里;下次请求通过负载均衡打到服务器B,B的Session里没有购物车数据。解决方案是使用Session共享技术,如将Session存储到Redis中。对于学习项目,我们暂不考虑集群,但这是迈向实战必须知道的一点。

4.3 订单生成与并发控制:防止超卖的基础思想

用户提交购物车,生成订单,这是整个系统最核心的交易环节。这里隐藏着一个经典的并发问题:超卖。假设某热门菜品“红烧肉”最后一份库存为1,两个用户A和B同时读取库存都为1,都成功下单,导致库存减为-1,这就发生了超卖。

在JavaWeb层面,我们可以在应用层做简单的乐观锁控制。这需要改造数据库表和下单逻辑。

  1. food表增加一个版本号字段(或使用库存本身作为版本控制):
    ALTER TABLE `food` ADD COLUMN `version` INT DEFAULT 0; -- 或者利用库存本身:在减库存时,条件加上`where stock = old_stock`
  2. 在下单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 告别Scriptlet:EL表达式与JSTL标签库的优雅使用

早期的JSP页面中充斥着大量的Java代码块(<% ... %>),这被称为Scriptlet。它使得页面逻辑混乱,难以维护,也违背了MVC中视图层应尽量简洁的原则。现代JavaWeb开发强烈建议使用EL表达式(Expression Language)JSTL(JSP Standard Tag Library)来替代Scriptlet。

EL表达式:用于在JSP中方便地访问域对象(pageScope, requestScope, sessionScope, applicationScope)中的数据。语法非常简单:${表达式}

  • 例如,在Servlet中request.setAttribute(“foodList”, foodList),在JSP中可以直接遍历:<c:forEach items=“${foodList}” var=“food”>...
  • 它支持属性访问、集合操作、算术和关系运算,非常强大。

JSTL标签库:提供了一系列常用的标签,如循环、判断、格式化等。最常用的是核心标签库 (core)

  1. 引入JSTL:将jstl-1.2.jar放入WEB-INF/lib
  2. 在JSP页面头部引入
    <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
  3. 常用标签示例
    • 条件判断
      <c:if test="${sessionScope.currentUser.userType == 1}"> <a href="admin/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}" var="food" varStatus="status"> <tr> <td>${status.index + 1}</td> <td><img src="${food.imageUrl}" width="50"></td> <td>${food.name}</td> <td>¥<fmt:formatNumber value="${food.price}" pattern="#,##0.00"/></td> <td> <input type="number" value="1" min="1" id="qty_${food.id}"> <button onclick="addToCart(${food.id})">加入购物车</button> </td> </tr> </c:forEach> </table>
    • 格式化<fmt:formatDate value="${order.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/>可以很好地格式化日期。

5.2 利用JSP+Ajax实现动态购物车与无刷新体验

为了提升用户体验,我们不应该在每次添加购物车或更新数量时都刷新整个页面。这时就需要用到Ajax(Asynchronous JavaScript and XML)技术。其核心是通过JavaScript的XMLHttpRequest对象或更现代的fetchAPI,在后台与服务器交换数据,并局部更新页面。

场景:点击“加入购物车”按钮,无刷新更新页面右上角购物车图标上的数量。

  1. 前端(JSP页面中的JavaScript)
    function 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)); }
  2. 后端(CartAddServlet)
    @WebServlet("/cart/add") public class CartAddServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { // ... 处理添加购物车逻辑 Map<String, Object> result = new HashMap<>(); result.put("success", true); result.put("message", "添加成功"); result.put("totalCount", cartService.getTotalCount(userId)); // 计算购物车总件数 // 设置响应内容类型为JSON response.setContentType("application/json;charset=UTF-8"); // 将result Map转换为JSON字符串并输出 PrintWriter out = response.getWriter(); // 可以使用Gson或Jackson库,这里简单拼接 out.print("{\"success\":true, \"totalCount\":" + totalCount + "}"); out.flush(); } }

实操心得:处理Ajax请求的Servlet,通常不再进行页面跳转(forwardredirect),而是直接向响应流中写入数据(JSON或XML)。务必设置正确的Content-Type(如application/json)。同时,要处理好字符编码,防止中文乱码。可以在Servlet开头统一设置request.setCharacterEncoding(“UTF-8”);response.setCharacterEncoding(“UTF-8”);

6. 项目部署、优化与常见问题排查

6.1 从IDEA到Tomcat:项目打包与部署实战

开发完成后,我们需要将项目部署到独立的Tomcat服务器上,模拟真实环境。

  1. 项目打包:JavaWeb项目标准的打包格式是WAR(Web Application Archive)文件。在IDEA中,可以通过Build -> Build Artifacts...来生成WAR包。更规范的做法是使用MavenGradle进行构建管理。如果项目是Maven项目,只需在项目根目录运行mvn clean package,就会在target目录下生成项目名.war文件。
  2. 部署到Tomcat
    • 将生成的CampusFoodOrdering.war文件复制到Tomcat的webapps目录下。
    • 启动Tomcat(运行bin/startup.bat(Windows) 或bin/startup.sh(Linux/Mac))。
    • Tomcat会自动解压WAR包。访问http://localhost:8080/CampusFoodOrdering即可。
  3. 数据库初始化:确保部署服务器的MySQL中已经创建了对应的数据库(campus_food),并执行了建表SQL脚本。连接池的配置文件db.properties中的JDBC URL、用户名和密码需要根据部署环境进行修改。

6.2 性能与安全优化要点

一个能跑起来的系统是第一步,一个跑得稳、跑得安全的系统才是目标。

性能方面

  • 数据库优化
    • 索引:为经常用于查询条件(WHERE)、排序(ORDER BY)和连接(JOIN)的字段创建索引。例如,order表的user_idcreate_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),统一设置requestresponse的编码为UTF-8。
3. 确保MySQL数据库、表、连接字符串(characterEncoding=UTF-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.xmlContextreloadable是否设为true(开发环境)。
3. 最直接的方法:重启Tomcat。
提交表单后,Servlet获取到的参数为null1. 表单method不是POST,Servlet只处理了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+需要时区参数serverTimezone=Asia/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开发的理解会深入骨髓。这时再去看那些现代化的框架,你会恍然大悟,明白它们到底在帮你简化什么,封装什么。这份从底层构建系统的经验,是任何现成框架都无法替代的财富。项目源码的价值不仅在于那几千行代码,更在于你思考和解决上述每一个问题的过程。

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

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

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

立即咨询