简介:这是一套基于 MVC 模式开发的点餐系统服务端项目,使用 Servlet 与 JDBC 完成请求和数据处理,适合准备毕业设计、课程设计或系统学习 Java Web 的开发者。项目覆盖注册登录、菜品分类、检索、购物车、下单及订单管理等流程,体现控制器、模型、视图三层协作,并包含 Servlet 生命周期、请求分发、JDBC 操作、数据持久化、会话管理、异常处理等关键知识点。压缩包共 139 个文件,约 3.76MB,以 JSP 页面、Java 源码、class 文件和依赖 jar 包为主,另有界面截图、SQL 脚本和需求文档,便于对照代码与页面学习。当前已有 61 人学习,下载后可直接导入环境运行,也可结合文档梳理从请求到数据库再到响应的完整链路,源码结构规范、注释清晰,适合在此基础上进行二次开发,是一套课程设计、毕业设计和练手都适用的完整源码。
1. 基于MVC的原生Servlet+JDBC点餐系统:课设代码包能不能直接用
毕业设计选了点餐系统,下载了一份压缩包,文件名写着“基于MVC开发模式开发原生Servlet+jdbc服务器项目-点餐系统”。解压一看,CenterController.class、UserService、FoodService、FoodTypeService这些类名都在,还带一份需求文档.doc。现在Spring MVC虽然流行,但这份纯Servlet+JDBC手写MVC的资源,对课程设计和毕业设计来说反而更值钱:每一层逻辑都露在源码里,答辩时不用背框架,面试官问到底层也能接得住。适合谁用?准备做Java Web课设、想把Servlet生命周期和JDBC连接吃透的新手;也适合想拿一个能跑的基线项目再往上改的熟手。
2. MVC分层在Servlet项目里的实际落法:Controller、Service、DAO的边界与联调
2.1 先认类:这份资源的代码结构长什么样
压缩包里没有Spring、没有MyBatis,目录里能看到Controller、Service、DBUtil和一堆Eclipse的工程配置。按MVC的通用拆法,CenterController是入口,UserService、FoodService、FoodTypeService各管一块业务,DBUtil负责跟数据库打交道。下面这张表是我按照类名和点餐系统的常规模块推断的职责,拿到源码后你可以对照类里的注释再确认。
| 类名 | 在MVC中的位置 | 职责推断 | 对应功能 |
|---|---|---|---|
| CenterController | Controller | 请求分发入口,处理核心路由 | 登录、跳转、统一调度 |
| UserService | Service | 用户业务逻辑 | 注册、登录、用户信息 |
| FoodService | Service | 菜品业务逻辑 | 菜品列表、按分类查菜品 |
| FoodTypeService | Service | 菜品分类业务逻辑 | 分类列表、分类管理 |
| DCService | Service | 缩写类,可能管餐桌或配送 | 桌台管理、点餐开桌 |
| DBUtil | DAO/工具 | JDBC连接与释放 | 所有数据库操作的基础 |
课设项目里Controller通常不止一个,CenterController这类“中央控制器”常见做法是把登录、首页、跳转这类通用请求都收进来,再配合web.xml或@WebServlet做分发。注意一点:这个项目里Service层直接持有DAO操作,没再单独拆DAO层,这在小项目里完全没问题,业务不复杂时多拆一层反而添乱。
2.2 原生JDBC数据访问层:DBUtil的写法与参数
JDBC操作逃不脱六步:加载驱动、建立连接、创建Statement、执行SQL、处理结果集、释放资源。DBUtil就是把这六步的前两步和最后一步抽出来复用。以下是我按常见课设写法还原的一个DBUtil简化版本,和压缩包里的实际代码思路一致:
package com.orderfood.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/orderfood?useUnicode=true&characterEncoding=utf8&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "root"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt != null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这段代码里DRIVER是MySQL 5.x时代的驱动类名,如果你的MySQL是8.x,要换成com.mysql.cj.jdbc.Driver。URL里useUnicode=true和characterEncoding=utf8两个参数写在一起,是保证中文出入库不乱码的常规做法,少一个都可能出问题。连接用户名密码如果你本机改过,记得同步改这里。
2.3 走通一次登录:Servlet接收请求 → Service处理 → Session记录
登录是点餐系统的入口,也是理解MVC协作最好的例子。浏览器把用户名密码POST过来,CenterController接到请求后取出参数,调用UserService去数据库比对,成功就把用户对象扔进Session,然后重定向到首页;失败就带着错误信息跳回登录页。下面是我根据常见课设写法补全的登录Servlet核心逻辑:
package com.orderfood.servlet; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import com.orderfood.entity.User; import com.orderfood.service.UserService; @WebServlet("/login") public class LoginServlet extends HttpServlet { private UserService userService = new UserService(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("utf-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/index"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }这里有几个关键点:request.setCharacterEncoding("utf-8")不写的话,中文用户名大概率乱码;userService.login返回null就说明账号密码不对,不要自己拼SQL去查;登录成功用sendRedirect而不是forward,是为了避免刷新页面弹“表单重复提交”的提示。Session里存的是User对象,后续页面要显示“欢迎xxx”只需要从session中取值,不需要再查一次库。
3. 让点餐系统在本地跑起来:Eclipse+Tomcat+MySQL的部署与数据库初始化
3.1 环境版本选型:哪套组合最省事
课设项目往往不挑新版本,能稳定跑起来比什么都重要。这份资源是原生Servlet+JDBC项目,我推荐的组合是JDK 1.8、Tomcat 8.5、MySQL 5.7、Eclipse IDE for Enterprise Java Developers。如果手头只有MySQL 8.x也没关系,把驱动换掉就行。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | Servlet容器兼容性最好,写课设足够 |
| Tomcat | 8.5 或 9.0 | 支持Servlet 3.1/4.0,配置简单 |
| MySQL | 5.7 或 8.0 | 5.7最稳,8.0记得换驱动类 |
| Eclipse | 2020之后版本 | 自带WTP,导入web项目方便 |
不用纠结Spring Boot那套新东西,这个项目就是要让你看Servlet原生运行在Tomcat上的过程。
3.2 导入项目到Eclipse:关键配置步骤
压缩包解压后里面是Eclipse的Web项目结构,.classpath和org.eclipse.wst.common.component都齐全,意味着可以按已有工程直接导入。我在IDEA里处理过多份类似的课设包,思路是一样的,Eclipse下走这套流程:
- 菜单File → Import → Existing Projects into Workspace,选择解压出的项目根目录。
- 右键项目 → Properties → Targeted Runtimes,勾选你那台机器的Tomcat版本。
- 检查Build Path里JRE版本,缺失或版本不匹配就Remove后重新Add Library。
- 如果项目里有src/main/java这种Maven风格目录,而.classpath没指向它,需要手动把源文件夹加进Build Path。
导入阶段最常见的现象是项目红叉,九成是Tomcat运行时没绑上,或者JDK版本对不上。我的习惯是先把Targeted Runtimes搞定,红叉通常当场消失;还不行的,打开Markers视图看具体报错,别猜。
3.3 建库建表:点餐系统的基础表结构和初始数据
点餐系统的库,最少要覆盖用户、菜品、菜品分类、订单、订单明细五张表。菜品和分类是主从关系,订单和明细是主从关系,用户表独立。以下是我按课设项目常用口径整理的建表SQL,字段名和压缩包需求文档里的口径可能有出入,导入后对照修改即可:
CREATE DATABASE IF NOT EXISTS orderfood DEFAULT CHARACTER SET utf8; USE orderfood; CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(32) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE t_foodtype ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE t_food ( id INT PRIMARY KEY AUTO_INCREMENT, type_id INT NOT NULL, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, description VARCHAR(255) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, desk_id INT, total DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE t_order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, food_id INT NOT NULL, food_name VARCHAR(64), price DECIMAL(10,2), quantity INT NOT NULL DEFAULT 1 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;建表语言包里带DEFAULT CHARSET=utf8,能让表和字段默认用utf8存储,配合JDBC URL里的characterEncoding=utf8,双保险防中文乱码。DECIMAL(10,2)存价格,别用float,涉及到金额的精度问题以后你就懂了。t_order里status字段我习惯用0表示已下单未支付,1表示已支付,2表示已完成,面试被问到订单状态机时也有得说。
3.4 启动验证:按功能清单过一遍
项目启动到Tomcat后,不要只点开首页看一眼就说跑通了。我常用的验收顺序是:
| 功能入口 | 操作 | 预期结果 |
|---|---|---|
| 注册页 | 新用户名注册 | 入库成功,跳登录页 |
| 登录页 | 正确密码登录 | 跳首页,页面显示用户名 |
| 首页列表 | 打开菜品页 | 菜品按分类展示,无乱码 |
| 加购 | 点“加入购物车” | 购物车角标数量变化 |
| 提交订单 | 确认下单 | 生成订单,库存或数量正确 |
| 退出 | 点击退出 | Session失效,再访问需登录 |
走到“退出后需要重新登录”这一条,基本就能确认Servlet、JDBC、Session全链路是通的。这一步做完,再用Navicat或命令行翻一下t_user和t_order,数据都写了,部署环节就算彻底过关了。
4. 点餐业务闭环拆解:菜品列表、会话购物车、订单提交的实现细节
4.1 菜品列表与分类联动:FoodServlet + FoodService
首页点餐最核心的需求是“按分类看菜”。以前课设里最容易写崩的是把SQL散落在Servlet里,这里我们按MVC的思路走:Servlet只接参数、调Service、转发JSP,SQL全部收在Service/DAO层。
@WebServlet("/food") public class FoodServlet extends HttpServlet { private FoodService foodService = new FoodService(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String typeId = request.getParameter("typeId"); List<Food> foodList = foodService.findByType(typeId); request.setAttribute("foodList", foodList); request.getRequestDispatcher("/food_list.jsp").forward(request, response); } }public List<Food> findByType(String typeId) { List<Food> list = new ArrayList<>(); String sql = "SELECT f.* FROM t_food f WHERE f.type_id = ?"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); ps.setInt(1, Integer.parseInt(typeId)); rs = ps.executeQuery(); while (rs.next()) { Food food = new Food(); food.setId(rs.getInt("id")); food.setName(rs.getString("name")); food.setPrice(rs.getBigDecimal("price")); list.add(food); } } catch (Exception e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; }注意Service方法里用了PreparedStatement的setInt占位符,字符串拼接SQL在课设里虽然能跑,但遇到引号转义就翻车,更重要的是有一种SQL注入的隐患,答辩时被老师指出来很尴尬。typeId为空时Integer.parseInt会抛NumberFormatException,稳妥做法是先判空再解析,这里为了简洁没写,实际动手时记得补上。
4.2 加购与购物车:Session里到底存什么
购物车方案可以存数据库,也可以存Session。课设项目里存Session是最常见做法,因为购物车是临时性数据,没人希望关掉浏览器后再打开购物车还在。用Map<Integer, Integer>就能搞定,key是foodId,value是数量:
@WebServlet("/cart/add") public class CartAddServlet extends HttpServlet { @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { HttpSession session = request.getSession(); int foodId = Integer.parseInt(request.getParameter("foodId")); Map<Integer, Integer> cart = (Map<Integer, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); } Integer count = cart.get(foodId); cart.put(foodId, count == null ? 1 : count + 1); session.setAttribute("cart", cart); response.sendRedirect(request.getContextPath() + "/cart.jsp"); } }这段代码的精髓在事务边界外的一句话:购物车对象本质是HttpSession的一个属性,Session只要不失效,数据就一直跟着用户走。加购逻辑里先取旧数量,没有就默认1,有就加一,这个“先get再put”的套路在绝大多数计数场景通用。想改口味的话,可以把Map换成List ,每个CartItem携带foodId、名称、单价、数量,购物车页面展示时少两次查库。
4.3 提交订单:为什么事务一定要包在Service层
生成订单至少涉及两步数据库操作:往t_order插一条主单,往t_order_detail插若干明细,如果还要扣库存,那就是三条SQL。这三条语句要么全成功,要么全失败,原生JDBC里必须手动开启事务:
public boolean submitOrder(int userId, Map<Integer, Integer> cart) { Connection conn = null; PreparedStatement psOrder = null; PreparedStatement psDetail = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); psOrder = conn.prepareStatement( "INSERT INTO t_order(user_id, total) VALUES(?, ?)"); // 计算total后执行,并获取自增主键 double total = 0; for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { total += foodService.getPrice(entry.getKey()) * entry.getValue(); } psOrder.setInt(1, userId); psOrder.setDouble(2, total); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); int orderId = -1; if (keys.next()) { orderId = keys.getInt(1); } // 遍历cart,逐条插入t_order_detail psDetail = conn.prepareStatement( "INSERT INTO t_order_detail(order_id, food_id, quantity, price) VALUES(?, ?, ?, ?)"); for (Map.Entry<Integer, Integer> entry : cart.entrySet()) { psDetail.setInt(1, orderId); psDetail.setInt(2, entry.getKey()); psDetail.setInt(3, entry.getValue()); psDetail.setDouble(4, foodService.getPrice(entry.getKey())); psDetail.addBatch(); } psDetail.executeBatch(); conn.commit(); return true; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { DBUtil.close(null, psDetail, conn); } }这段代码里setAutoCommit(false)是关键开关,一旦关了自动提交,后面所有SQL都不会立即落库,直到你手动commit或rollback。getGeneratedKeys()拿到的是刚才插入主单的自增主键,明细表靠它关联主单。addBatch加批次最后executeBatch一起执行,是JDBC批量插入性能较好的写法。你留意到没有,整个操作从头到尾用的同一个conn,如果每个DAO方法都自己new一个连接,事务就串不起来了,这就是事务必须放在Service层的最根本原因。
5. Servlet+JDBC避坑排查:驱动加载、中文乱码、连接释放、404与重复下单
5.1 启动报 ClassNotFoundException: com.mysql.jdbc.Driver
现象:项目启动后第一次访问数据库页面直接500,控制台打出ClassNotFoundException,指向DBUtil里的Class.forName那一行。
原因:MySQL驱动JAR没有放进WEB-INF/lib目录,或者放到了别的位置。Eclipse的Build Path里加了驱动,但WTP部署时没把它同步给Tomcat,这在导入外部项目时特别常见。
解决:确认mysql-connector-java的jar包物理存在于WEB-INF/lib下,同时右键项目Properties → Deployment Assembly → Add → Java Build Path Entries,把驱动勾上。改完后Clean Tomcat的work目录再重启,别只点刷新。
5.2 数据库里的中文变成???
现象:页面填写菜名或者用户名,提交后再查库,中文全部变成问号。
原因:三层至少有一层编码不对。数据库连接URL没带characterEncoding=utf8,或者MySQL表结构本身就是latin1,又或者JSP页面没声明UTF-8。
解决:先改JDBC URL,在数据库地址后面加?useUnicode=true&characterEncoding=utf8;再确认建表语句带DEFAULT CHARSET=utf8,已建的表用ALTER TABLE t_food CONVERT TO CHARACTER SET utf8修;最后检查JSP顶部有没有<%@ page contentType="text/html; charset=utf-8" %>。三层全改成UTF-8基本再也不会遇到问号。
5.3 系统跑一会就变慢,Tomcat假死
现象:刚开始操作一切正常,点了十几分钟页面后,请求卡住不响应,Tomcat控制台也没异常,重启后恢复。
原因:连接泄漏。某个DAO方法里连接用完没关,数据库连接池中的连接被耗光,后续请求全部排队等待,表现就是假死。课设里DBUtil.close虽然写了,但如果你在某个catch分支里提前return,close就被跳过了。
解决:全部DAO方法强制用try-catch-finally,把DBUtil.close放进finally里,保证异常路径也会关闭资源。验证方法:在DBUtil的close里打日志或计数,跑完一轮功能后看关闭次数和打开次数是否对得上。
5.4 部署到Tomcat后访问404,Servlet注解不生效
现象:本地Eclipse里通过Run on Server能打开首页,把WAR包丢到独立Tomcat后,访问/login、/food这些路径全是404。
原因:最常见的是context path对不上,项目名改了导致URL根路径变了;也有情况是Servlet的@WebServlet注解在旧版Tomcat上没被扫描到,或者web.xml里版本声明过低。
解决:先在浏览器访问http://localhost:8080/项目名/,确认路径里有没有带项目名。再看tomcat的web.xml和项目里的web.xml声明的Servlet版本,至少要写3.0以上注解才生效。最简单粗暴的兜底方案是回到web.xml里显式配置Servlet映射,课设评委管这个叫“兼容性好”。
5.5 刷新页面订单重复提交
现象:点完“提交订单”后手贱按了F5,数据库里多了一模一样的订单。
原因:表单提交是POST,刷新浏览器时会重新提交最后一次请求,而下单接口没有做幂等处理。
解决:常规方案是提交订单成功后立即重定向到订单详情页,用Post/Redirect/Get模式,也就是Response.sendRedirect跳走,把下单的POST请求给“消费”掉。如果需要更强的防重,下单前用Session里存的token做校验,一次请求校验后就删掉,这个思路在答辩现场说出来是加分项。
6. 验证与进阶:把课程设计改成拿得出手的完整作品
先摆一份我在课设答辩前必过的验收清单,每一行都不难,但漏掉任何一行都可能现场被打脸:登录失败时错误提示是否友好这是老师必点的功能;菜品价格精度是否保留了两位小数;购物车里把数量减到0时是否自动移除;订单提交后返回购物车是否已被清空;Session过期后访问内部页是否有拦截跳转。
如果这些都没问题,项目已经有75分了。接下来我只建议做一个改动:把手写DBUtil换成Druid连接池。连接池解决的不只是连接复用,更重要的是给你加了一层“连接最多活多久、空闲多久回收”的保护。改造方式也很直接,引入druid.jar后用DruidDataSource替换DriverManager.getConnection:
DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName("com.mysql.jdbc.Driver"); ds.setUrl("jdbc:mysql://localhost:3306/orderfood?useUnicode=true&characterEncoding=utf8"); ds.setUsername("root"); ds.setPassword("root"); ds.setInitialSize(5); ds.setMaxActive(20);Connection conn = ds.getConnection();之后的代码完全不用动。这几个参数里setInitialSize(5)代表启动时建立5个连接,setMaxActive(20)上线是20个连接。课设这个量级,配10到20个都行,配太大反而浪费内存。做完这一步,你的答辩稿里就多了“连接复用、性能优化、防止连接泄漏”三个可以展开的亮点。
我以前给学弟改课设时,发现大多数人的问题不在代码攥不出来,而在什么时候该在哪个文件下手。后来我养成一个习惯,每次导入这类项目第一件事就是全局搜DBUtil,看连接是不是都从这一个出口拿、也从这一个出口关。只要这一关把住了,这个项目兜底能跑;把这条链路盘顺了,剩下的就是照着页面点,点出一条业务闭环,这个闭环就是你答辩时的底气。希望我的这点经验帮到你。
本文还有配套的精品资源,点击获取