Servlet+JDBC手写MVC点餐系统:课设部署与代码拆解 简介这是一套基于 MVC 模式开发的点餐系统服务端项目使用 Servlet 与 JDBC 完成请求和数据处理适合准备毕业设计、课程设计或系统学习 Java Web 的开发者。项目覆盖注册登录、菜品分类、检索、购物车、下单及订单管理等流程体现控制器、模型、视图三层协作并包含 Servlet 生命周期、请求分发、JDBC 操作、数据持久化、会话管理、异常处理等关键知识点。压缩包共 139 个文件约 3.76MB以 JSP 页面、Java 源码、class 文件和依赖 jar 包为主另有界面截图、SQL 脚本和需求文档便于对照代码与页面学习。当前已有 61 人学习下载后可直接导入环境运行也可结合文档梳理从请求到数据库再到响应的完整链路源码结构规范、注释清晰适合在此基础上进行二次开发是一套课程设计、毕业设计和练手都适用的完整源码。1. 基于MVC的原生ServletJDBC点餐系统课设代码包能不能直接用毕业设计选了点餐系统下载了一份压缩包文件名写着“基于MVC开发模式开发原生Servletjdbc服务器项目-点餐系统”。解压一看CenterController.class、UserService、FoodService、FoodTypeService这些类名都在还带一份需求文档.doc。现在Spring MVC虽然流行但这份纯ServletJDBC手写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中的位置职责推断对应功能CenterControllerController请求分发入口处理核心路由登录、跳转、统一调度UserServiceService用户业务逻辑注册、登录、用户信息FoodServiceService菜品业务逻辑菜品列表、按分类查菜品FoodTypeServiceService菜品分类业务逻辑分类列表、分类管理DCServiceService缩写类可能管餐桌或配送桌台管理、点餐开桌DBUtilDAO/工具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?useUnicodetruecharacterEncodingutf8useSSLfalse; 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里useUnicodetrue和characterEncodingutf8两个参数写在一起是保证中文出入库不乱码的常规做法少一个都可能出问题。连接用户名密码如果你本机改过记得同步改这里。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. 让点餐系统在本地跑起来EclipseTomcatMySQL的部署与数据库初始化3.1 环境版本选型哪套组合最省事课设项目往往不挑新版本能稳定跑起来比什么都重要。这份资源是原生ServletJDBC项目我推荐的组合是JDK 1.8、Tomcat 8.5、MySQL 5.7、Eclipse IDE for Enterprise Java Developers。如果手头只有MySQL 8.x也没关系把驱动换掉就行。组件推荐版本说明JDK1.8Servlet容器兼容性最好写课设足够Tomcat8.5 或 9.0支持Servlet 3.1/4.0配置简单MySQL5.7 或 8.05.7最稳8.0记得换驱动类Eclipse2020之后版本自带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) ) ENGINEInnoDB DEFAULT CHARSETutf8; CREATE TABLE t_foodtype ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8; 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) ) ENGINEInnoDB DEFAULT CHARSETutf8; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8; 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 ) ENGINEInnoDB DEFAULT CHARSETutf8;建表语言包里带DEFAULT CHARSETutf8能让表和字段默认用utf8存储配合JDBC URL里的characterEncodingutf8双保险防中文乱码。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、转发JSPSQL全部收在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); ListFood foodList foodService.findByType(typeId); request.setAttribute(foodList, foodList); request.getRequestDispatcher(/food_list.jsp).forward(request, response); } }public ListFood findByType(String typeId) { ListFood 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是最常见做法因为购物车是临时性数据没人希望关掉浏览器后再打开购物车还在。用MapInteger, Integer就能搞定key是foodIdvalue是数量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)); MapInteger, Integer cart (MapInteger, 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, MapInteger, 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.EntryInteger, 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.EntryInteger, 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. ServletJDBC避坑排查驱动加载、中文乱码、连接释放、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没带characterEncodingutf8或者MySQL表结构本身就是latin1又或者JSP页面没声明UTF-8。解决先改JDBC URL在数据库地址后面加?useUnicodetruecharacterEncodingutf8再确认建表语句带DEFAULT CHARSETutf8已建的表用ALTER TABLE t_food CONVERT TO CHARACTER SET utf8修最后检查JSP顶部有没有% page contentTypetext/html; charsetutf-8 %。三层全改成UTF-8基本再也不会遇到问号。5.3 系统跑一会就变慢Tomcat假死现象刚开始操作一切正常点了十几分钟页面后请求卡住不响应Tomcat控制台也没异常重启后恢复。原因连接泄漏。某个DAO方法里连接用完没关数据库连接池中的连接被耗光后续请求全部排队等待表现就是假死。课设里DBUtil.close虽然写了但如果你在某个catch分支里提前returnclose就被跳过了。解决全部DAO方法强制用try-catch-finally把DBUtil.close放进finally里保证异常路径也会关闭资源。验证方法在DBUtil的close里打日志或计数跑完一轮功能后看关闭次数和打开次数是否对得上。5.4 部署到Tomcat后访问404Servlet注解不生效现象本地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.getConnectionDruidDataSource ds new DruidDataSource(); ds.setDriverClassName(com.mysql.jdbc.Driver); ds.setUrl(jdbc:mysql://localhost:3306/orderfood?useUnicodetruecharacterEncodingutf8); 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看连接是不是都从这一个出口拿、也从这一个出口关。只要这一关把住了这个项目兜底能跑把这条链路盘顺了剩下的就是照着页面点点出一条业务闭环这个闭环就是你答辩时的底气。希望我的这点经验帮到你。本文还有配套的精品资源点击获取