简介:本资源是一套完整的基于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 核心功能模块与数据库设计蓝图
在动手写代码之前,我们必须把系统要做什么、数据怎么存想清楚。我们采用自顶向下的方式,先梳理出核心功能模块,再设计支撑这些功能的数据库表。
核心功能模块主要分为前台用户端和后台管理端:
- 用户端:
- 用户模块:注册、登录、个人信息管理。
- 餐厅/菜品展示模块:按食堂、分类(如盖饭、面食)展示菜品,支持图片、价格、描述查看。
- 购物车模块:添加菜品、修改数量、删除菜品、清空购物车。
- 订单模块:从购物车生成订单,选择送餐地址(或取餐点)、支付方式(模拟),查看订单历史及状态。
- 管理端(简易版):
- 菜品管理:对菜品进行增删改查(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_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.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=falseinitialSize:池启动时创建的初始连接数。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中。结构可以是一个
Map<foodId, quantity>。 - 用户登录时:触发一个“购物车合并”操作。检查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层面,我们可以在应用层做简单的乐观锁控制。这需要改造数据库表和下单逻辑。
- 为
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 告别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)。
- 引入JSTL:将
jstl-1.2.jar放入WEB-INF/lib。 - 在JSP页面头部引入:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> - 常用标签示例:
- 条件判断:
<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,在后台与服务器交换数据,并局部更新页面。
场景:点击“加入购物车”按钮,无刷新更新页面右上角购物车图标上的数量。
- 前端(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)); } - 后端(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,通常不再进行页面跳转(
forward或redirect),而是直接向响应流中写入数据(JSON或XML)。务必设置正确的Content-Type(如application/json)。同时,要处理好字符编码,防止中文乱码。可以在Servlet开头统一设置request.setCharacterEncoding(“UTF-8”);和response.setCharacterEncoding(“UTF-8”);。
6. 项目部署、优化与常见问题排查
6.1 从IDEA到Tomcat:项目打包与部署实战
开发完成后,我们需要将项目部署到独立的Tomcat服务器上,模拟真实环境。
- 项目打包:JavaWeb项目标准的打包格式是WAR(Web 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_time,order_item表的order_id。 - SQL语句:避免使用
SELECT *,只查询需要的列。注意JOIN的效率,大数据表关联需谨慎。 - 连接池监控:利用Druid的监控功能,定期查看连接池活跃数、等待数,防止连接泄漏。
- 索引:为经常用于查询条件(WHERE)、排序(ORDER BY)和连接(JOIN)的字段创建索引。例如,
- 前端优化:
- 静态资源缓存:在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数据库、表、连接字符串( characterEncoding=UTF-8)都使用UTF-8。 |
ClassNotFoundException: com.mysql.cj.jdbc.Driver | 1. 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获取到的参数为null | 1. 表单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开发的理解会深入骨髓。这时再去看那些现代化的框架,你会恍然大悟,明白它们到底在帮你简化什么,封装什么。这份从底层构建系统的经验,是任何现成框架都无法替代的财富。项目源码的价值不仅在于那几千行代码,更在于你思考和解决上述每一个问题的过程。
本文还有配套的精品资源,点击获取