原生Servlet+JDBC+MVC点餐系统:分层架构与避坑实战

发布时间:2026/10/8 16:32:54
原生Servlet+JDBC+MVC点餐系统:分层架构与避坑实战
简介这是一份基于MVC模式构建的原生ServletJDBC点餐系统完整项目面向Java Web初学者、课程设计与毕业设计人群适合用来理解从请求处理、业务逻辑到数据库访问的完整开发链路。压缩包共139个文件以jpg界面截图与jsp页面为主配合java源码、class编译文件、jar依赖库、sql建表脚本及需求文档可对照界面快速定位前后端实现整体仅3.76MB结构紧凑。项目覆盖Servlet生命周期、doGet/doPost、HttpSession会话管理、用户登录鉴权、JDBC增删改查与数据持久化等关键知识点还包含数据库表设计、错误处理、前端页面与Servlet的数据交互以及Tomcat部署所需的配置文件和项目描述文件。已有61人学习下载。对于需要完成课设或毕业设计、想在真实场景中练习Servlet与JDBC的开发者这份压缩包既是可直接运行参考的完整工程也是梳理Java Web开发流程的实用样例。1. 为什么还要回头看原生Servletjdbc的点餐系统框架越省事地基越重要看到“基于MVC开发模式开发原生Servletjdbc服务器项目-点餐系统.zip”这个标题我反而松了口气终于有个项目不靠 Spring Boot 兜底。MVC、Servlet、JDBC 三个词放在一起意味着每一条点餐请求都要从 Servlet 接到 Service、再到数据库中间没有任何框架替你隐藏细节。这类项目最现实的价值是解决两类人的问题在校生要把 JavaWeb 实训做出来并且答辩讲得清刚入职的初级工程师要维护老系统、能接住“Servlet 到底干了什么”这种追问。整条点菜到写库的链路都摊在你眼前。这篇不带你抄某个现成 zip而是把这类项目最常见、也最不容易翻车的做法拆开讲目录怎么立、JDBC 怎么连、路由怎么跳、坑在哪里。适合每个愿意把地基打实的开发者。2. 原生ServletMVC分层先立边界再写代码拿到这类项目压缩包先别急着把代码跑起来先看它的包结构。如果包结构是乱的后面每一行代码都在给这个乱摊子加码。所谓 MVC 落地点餐系统绝不是把 Servlet、JSP、JDBC 代码堆在一起而是先回答一个问题一个“用户点菜下单”的动作到底该由哪一层负责哪一件事。2.1 手动分层和Spring MVC的差别把路由和调用链显式化很多人最早写的 servlet demo 是一个 HelloServletdoGet 里往浏览器输出一行字。点餐系统是它的完整版但难度不在写 Servlet而在“让多个 Servlet 各司其职。Spring MVC 用起来舒服是因为它本身就是基于 Servlet 规范的封装——DispatcherServlet 本质上就是一个 Servlet。你现在在 Idea 里基于 Maven 骨架建一个 Spring MVC 项目Controller 里写RequestMapping(/order/checkout)就能收请求而在原生 Servlet 里你要自己用WebServlet把一个类暴露成 URL再手动调 Service。这两种写法的对应关系非常直白Spring MVC 的RequestMapping路径对应原生 Servlet 的urlPatternsSpring MVC 的RequestParam对应request.getParameter()Spring MVC 的return view对应request.getRequestDispatcher(...).forward()。理解了这层对应你再看这个点餐系统会突然明白框架只是把原生写法的样板代码收编了并没有发明新的东西。所以我建议你别用脚手架直接生成本地工程手动加servlet-api、jstl、MySQL 驱动三个依赖把目录建出来一个请求完整走一遍。这比背十遍 Spring MVC 原理都管用。2.2 包结构落位Controller、Service、Dao、Domain 谁也不能越界我一般会按下面这个结构搭点餐系统它符合绝大多数 ServletJDBC 项目的习惯答辩时也容易讲src/main/java/com/example/order ├── controller # Servlet只做参数解析、路由、转发 ├── service # 业务逻辑下单、结账、加菜、库存 │ └── impl ├── dao # JDBC 数据访问 │ ├── impl │ └── BaseDao ├── domain # Meal、Order、OrderItem、User ├── util # DBUtil、JsonUtil ├── filter # EncodingFilter、LoginFilter └── src/main/webapp ├── jsp # 页面 ├── static # css/js/images └── WEB-INF ├── web.xml └── lib # 不放 jar 也行Maven 依赖会打进去这套结构的原则只有一条Controller 里不写 SQLDAO 里不做业务判断。很多项目翻车不是功能写不出来而是“看菜价”逻辑在三个 Servlet 里各出现一次改一处漏一处。包结构是给人看的也是给三个月后的自己看的。2.3 一次点餐请求的完整流转从 form 表单到数据库点餐场景是顾客在菜单页勾了几道菜点了“下单”请求进入订单 Servlet。这里我给你一个能直接改来用的下单入口写法WebServlet(/order/checkout) public class CheckoutServlet extends HttpServlet { private OrderService orderService new OrderService(); Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String tableNo request.getParameter(tableNo); String[] mealIds request.getParameterValues(mealId); String[] counts request.getParameterValues(count); Order order new Order(); order.setTableNo(tableNo); for (int i 0; i mealIds.length; i) { OrderItem item new OrderItem(); item.setMealId(Integer.parseInt(mealIds[i])); item.setCount(Integer.parseInt(counts[i])); order.addItem(item); } boolean ok orderService.checkout(order); if (ok) { request.setAttribute(orderNo, order.getOrderNo()); request.getRequestDispatcher(/jsp/orderSuccess.jsp).forward(request, response); } else { response.sendRedirect(request.getContextPath() /jsp/orderFail.jsp); } } }这段代码的动作很清晰先从 request 里取出 tableNo、mealId、count 三组参数再组装成 Order 和 OrderItem 领域对象最后调用 Service 层根据结果决定页面流转。注意getParameterValues返回的是数组因为前端是用同名 checkbox 多选提交的菜如果你只写getParameter(mealId)浏览器只会给你第一道菜的 ID。转发和重定向的选择也值得说成功页用forward地址栏不变request 里的 orderNo 还能带到 JSP失败跳转用sendRedirect避免用户按 F5 时重复提交订单。这个细节是很多课程设计答辩时的加分点。3. JDBC连接MySQL驱动加载、连接管理与DAO封装MVC 里的 Model 最终要落到数据库。点餐系统的持久层几乎全是 JDBC 连接 MySQL所以这一章是整个项目最容易出幺蛾子的地方。连接串写错一个参数启动不报错、查出来全是乱码的情况我见过太多次。3.1 jdbc连接mysql的五个关键连接参数“jdbc连接mysql”本身不玄真正玄的是连接串里那一串后缀参数。我常用的 db.properties 长这样jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/order_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456逐个说参数。useUnicodetruecharacterEncodingutf8是为解决中文乱码如果你数据库和表都是 utf8这个必须带serverTimezoneAsia/Shanghai对应 MySQL 8.x 的时区校验不加会报Server returns invalid timezoneuseSSLfalse是跳过 SSL 握手本地开发能省出肉眼可见的启动时间allowPublicKeyRetrievaltrue是配合 MySQL 8 的 caching_sha2_password 认证插件用的不加会报Public Key Retrieval is not allowed。这一串参数在连接池和 Flink 这类 JDBC 连接器里同样通用排查思路是一模一样的先看驱动版本再看连接串。3.2 把DBUtil写稳连接、事务、关闭三件事连接管理我是用 ThreadLocal 做的不用 C3P0 也不用 HikariCP原因很简单课程设计项目里手写连接管理能让你把 JDBC 事务边界看清楚。下面这个 DBUtil 是我常用的模板public class DBUtil { private static final Properties PROPS new Properties(); private static final ThreadLocalConnection HOLDER new ThreadLocal(); static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { PROPS.load(in); Class.forName(PROPS.getProperty(jdbc.driver)); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { Connection conn HOLDER.get(); if (conn null) { conn DriverManager.getConnection(PROPS.getProperty(jdbc.url), PROPS.getProperty(jdbc.username), PROPS.getProperty(jdbc.password)); HOLDER.set(conn); } return conn; } public static void closeAll(Connection conn, Statement st, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (st ! null) { try { st.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } HOLDER.remove(); } }核心逻辑不在Class.forName而在ThreadLocal。下单时要写订单主表、明细表、扣库存这三步必须用同一个 Connection 才能组成一个事务如果每个 DAO 方法都自己DriverManager.getConnection()连接不是同一个事务就隔离了。closeAll最后一定调HOLDER.remove()否则线程池环境下连接不释放运行几天必炸。这是血泪经验。3.3 DAO层写法与三个数据访问的常见坑DAO 层我只推荐 PreparedStatement不要用 Statement 拼 SQL。菜单查询的常见写法如下public ListMeal findMealsByCategory(String category) { String sql SELECT id, name, price, pic, status FROM meal WHERE category?; ListMeal list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, category); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Meal m new Meal(); m.setId(rs.getInt(id)); m.setName(rs.getString(name)); m.setPrice(rs.getBigDecimal(price)); m.setStatus(rs.getInt(status) 1); list.add(m); } } } catch (SQLException e) { throw new RuntimeException(查询菜单失败, e); } return list; }这里藏着三个翻车点。第一价格字段必须用getBigDecimal用getDouble会在结账时出现 19.99 变成 19.989999 这种浮点误差看起来没多大对账时就是玄学。第二MySQL 的TINYINT(1)映射成 Boolean 时不同版本的驱动行为不一样有的老驱动返回 Integer你用rs.getBoolean()也会被自动转但取出来做判断就可能 NPE我在代码里统一getInt(...) 1最省事。第三SQL 里的?占位符必须用ps.setXxx填充绝不允许字符串拼进去否则 SQL 注入和引号断句问题会让你排查到怀疑人生。4. Servlet控制器与页面流转路由、参数与JSP的安全输出有了数据库支撑接下来要解决的是“用户请求从哪进、往哪跳”。原生 Servlet 没有 Spring MVC 那种GetMapping花式路由但一个点餐系统的路由数量也就十来个规划清楚完全够用。4.1 URL路由规划一个Servlet入口还是多个Servlet我倾向按业务动作拆 Servlet菜单查询、下单、订单查询、登录各一个类而不是只写一个MainServlet用 if 分支分发。下面是点餐系统常见的路由清单URLServlet主要动作/menu/listMenuListServlet查询菜单按分类展示/order/checkoutCheckoutServlet提交订单、扣库存/order/detailOrderDetailServlet查看单笔订单明细/admin/loginAdminLoginServlet管理员登录这里有一个“特殊路由指定”的小技巧如果两个 URL 想进同一个 Servlet不用写两个类WebServlet支持多路径WebServlet({/menu/list, /menu/menuList}) public class MenuListServlet extends HttpServlet { }这样旧页面跳转时不用改 JSP。但我提醒一句alias 路由只适合临时兼容正式演示前最好统一成一条路径否则日志里全是访问旧地址的记录看着心烦。4.2 参数解析的三条原则与响应输出的两种选择多 Servlet 最怕参数解析风格不统一我只定三条规矩第一所有 doGet/doPost 第一行设置编码request.setCharacterEncoding(UTF-8)第二多选参数一律getParameterValues单值参数先判空再转类型第三响应要么forward到 JSP要么用response.getWriter()写 JSON不要混。Ajax 请求在前端点餐页面很常见比如在菜单页不刷新直接加购物车Servlet 返回 JSON 的写法是response.setContentType(application/json;charsetUTF-8); PrintWriter out response.getWriter(); out.write({\success\:true,\mealId\: mealId ,\count\:1});注意两点setContentType必须先于getWriter()调用否则编码不生效前端会收到乱码手拼 JSON 只适用于字段少、值不复杂的场景字段一多我建议引入 Gson 或 Jackson别拿字符串拼接硬扛嵌套结构。原生 Servlet 项目引入一个轻量 JSON 库不算违背“原生”的初衷。4.3 JSP展示数据用EL和JSTL别再裸写JavaJSP 里最丑的写法是把while(rs.next())写进页面。正确做法是 Controller 把查询结果放进 requestJSP 用 EL 表达式和 JSTL 标签取数据。菜单列表页核心片段% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach items${mealList} varmeal tr td${meal.name}/td td${meal.price}/td td c:if test${meal.status}在售/c:if c:if test${!meal.status}停售/c:if /td /tr /c:forEach /table${meal.name}底层找的是 Meal 的 getName() 方法不是直接访问字段所以 domain 类必须写对 getterc:if是 JSTL 核心标签比% if %可读性好太多。最怕的做法是在同一个 JSP 里既写%request.getAttribute(mealList)%又混 EL 表达式页面逻辑稍微一多就会“飘”改一个变量名要全文搜三遍后悔药都没得吃。5. 点餐系统避坑清单五条高频血泪经验这一章是实操里最容易让人熬夜的部分。每一条我都按“现象、原因、解决”讲清楚你踩到任何一个都能直接照着处理。5.1 中文乱码乱码不是一处的锅现象页面刷新后菜名显示成“”或“汤ã”数据库里肉眼看着正常前端就是不对。原因乱码链条上有四个环节request 入参、response 输出、数据库连接、JSP 页面编码你只修一处压不住。Tomcat 默认对 POST 是 ISO-8859-1不做request.setCharacterEncoding(UTF-8)拿到的中文直接变形。解决两头堵。在公共 Filter 里统一设置 request 和 response 的编码为 UTF-8在 JSP 第一行写% page contentTypetext/html;charsetUTF-8 %保证 JDBC URL 带characterEncodingutf8。三处到位乱码基本绝迹。5.2 MySQL驱动版本不匹配报错像抽风现象项目从同事那拷来一启动报Communications link failure或者报Unknown character set甚至报Public Key Retrieval is not allowed偶尔切换网络环境后上午能跑下午连不上。原因本地 MySQL 是 5.7而依赖里的驱动是 MySQL 8.x 对应版本两者握手协议不一致反过来用 5.x 老驱动连 MySQL 8 也会有caching_sha2_password认证问题。这类问题跟 Flink 的 JDBC 连接器报异常一样第一排查项永远是驱动版本与数据库版本是否匹配。解决MySQL 5.7 用mysql-connector-java 5.1.xMySQL 8 用com.mysql.cj.jdbc.Driver。升级或降级驱动后清一下 Maven 本地缓存再重新打包避免旧 jar 残留在 target 里干扰启动。5.3 连接不释放运行几天必卡死现象项目刚启动正常跑了大概几千次请求后任何页面都打不开日志全是Connection is not available, request timed out。原因DAO 里开了 Connection 但没关尤其Statement、ResultSet这两个资源更容易漏。MySQL 默认最大连接数就一两百连接全部占满后服务直接假死。这也是典型的 JDBC 连接器问题——无论是自己写的 DBUtil 还是连接池资源不关闭机器再大也抗不住。解决用 try-with-resources 包住连接和语句对象或者把关闭动作集中放进finally像我前面写的closeAll那样。写完 DAO 顺手检查每一处getConnection()是不是都有配对关闭。5.4 事务边界画错订单写进去了库存扣错现象点击下单页面提示成功但后台发现菜单库存和订单明细对不上明细多了一条或少了一条。原因OrderDao.addOrder()每次提交都自动提交默认 autocommittrue订单主表写成功了明细表插入失败时主表并不会回滚。这是事务边界只画在 DAO 方法内部、没画到 Service 层的典型症状。解决事务必须从 Service 层统一控制用前面 ThreadLocal 里的同一个连接手动关掉自动提交Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); orderDao.insert(conn, order); orderItemDao.insert(conn, order.getItems()); mealDao.deductStock(conn, order.getItems()); conn.commit(); } catch (Exception e) { conn.rollback(); throw new RuntimeException(下单失败已回滚, e); } finally { conn.setAutoCommit(true); DBUtil.closeAll(conn, null, null); }这段代码的关键在于orderDao、orderItemDao、mealDao三个方法必须接收同一个conn参数而 DBUtil.getConnection() 在同一个线程里会返回 ThreadLocal 里那同一个连接。如果把三个 DAO 方法设计成自己再调一次 getConnection这套事务就白写了。5.5 在Controller里直接写SQL框架救了手写党现象项目能跑但想改一个“菜价超过 50 元不参与活动”的需求你得在三个 Servlet 里各改一处 SQL加个字段要全文搜索列名。原因写 Controller 的人图快把 JDBC 代码直接塞进 doGet/doPost。这在点餐系统这种小型项目里一时看不出毛病但本质是又把 MVC 拉回了“全栈 Servlet”的老路Service 层被架空成了摆设。解决把 Controller 里所有Connection、PreparedStatement变量清掉强制走 Service 再进 DAO。没有 Service 层调用的类通常是代码里最值得重构的部分。这也是我判断一个项目是不是真 MVC 的唯一标准打开 Controller里面有没有 import java.sql。6. 让点餐系统经得起演示和答辩一份验证清单与两个小改造项目做完不等于能演示我每次交付前都会把下面这套动作完整跑一遍。这不是形式主义而是给自己留后悔药。验证清单我列成表照着做就行验证项操作预期结果环境可移植性换一台电脑只装 JDK8Tomcat8MySQL不依赖本地 IDEA 配置也能启动中文编码提交“鱼香肉丝”订单入库、页面、打印小票均无乱码并发事务两个浏览器同时下单最后一件菜品一人成功另一人收到库存不足提示路由与回退下单成功后按 F5不产生重复订单数据库备份导出 SQL 在空库执行建表与初始菜单数据完整两个加分改造我建议你自己动一次手。第一把 DBUtil 换成 HikariCP 连接池改动范围集中在 util 包但要亲眼看一遍它如何替代DriverManager这能解决并发高时连接不够用的问题第二给下单、支付这类核心操作加一行LogFilter日志记录请求路径、耗时和参数答辩时把日志拉出来比你在台上空口讲哪一层做了什么更有说服力。我自己的习惯是交付前把 CheckoutServlet 到 DAO 的调用链默写一遍能写出来才说明这个项目不是能跑就行的黑匣子。也希望帮到你把地基打扎实比背会一百个注解值钱得多。本文还有配套的精品资源点击获取