ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

原生Servlet+JDBC+MVC点餐系统:表设计、事务封装与避坑指南

原生Servlet+JDBC+MVC点餐系统:表设计、事务封装与避坑指南 简介基于MVC开发模式的ServletJDBC点餐系统项目完整源码面向Java Web初学者、毕业设计及课程设计场景完整演示了用户登录、菜品浏览、下单结算的业务闭环。压缩包共139个文件以81张jpg界面素材、20个jsp视图页面、7个jar依赖库、6个java核心类及6个class编译文件为主另含需求文档、SQL建表脚本与Eclipse工程配置整体3.76MB目录结构清晰便于按模块检索。已有61人学习浏览。通过该项目可深入理解Servlet生命周期与doGet/doPost的适用场景掌握MVC三层架构中Model、View、Controller的职责划分并实践JDBC连接、CRUD操作、HttpSession会话跟踪及用户权限控制等关键点。需求文档和数据库脚本辅助梳理表结构与业务逻辑帮助快速完成从需求分析到接口实现的完整演练同时为课程设计答辩展示或二次开发提供可直接运行的基础版本是积累原生Java Web开发经验的实用素材。1. 原生Servlet做的点餐系统MVC课设老骨架为什么现在仍值得抄一遍一个“基于MVC开发模式开发原生Servletjdbc服务器项目-点餐系统.zip”标题背后其实是Java Web课程设计最常见的形态不碰Spring、不碰MyBatis全项目只用Servlet、JSP、JDBC和MySQL把MVC三层架构亲手搭出来。很多同学交完这个项目就扔了但在我看来这类系统反而是理解Spring MVC内部机制最快的一条路——你会在写Servlet路由时突然明白DispatcherServlet在干嘛在调JDBC时想通MyBatis帮你省掉了什么。这篇文章我会按照一套可直接复现的结构把表设计、DAO封装、Servlet基类、过滤器、事务边界和典型踩坑一条条拆开适合拿来改写成自己的课设或练手项目也适合新手用来验证“我到底学没学会框架之前的那层原理”。2. 先把MVC分层立住原生Servlet项目的包结构、职责边界与选型逻辑2.1 为什么是原生Servlet而不是Spring MVC或gin脚手架MVC做点餐系统这样的管理型业务选框架的标准应当是“团队熟练度”和“学习成本”。如果是商业项目我大概率直接上Spring Boot但作为课程设计或框架原理的练手项目我强烈建议用原生Servlet。原因不复杂Spring MVC本质上就是一个大Servlet——DispatcherServlet——再加一堆注解和组件扫描。你用注解写Controller和RequestMapping时框架帮你完成了参数绑定、视图解析、异常处理这些动作。正因为封得够狠很多初学者写了大半年Spring Boot遇到一个请求路径405都不知道去web.xml或Servlet注解里查路由映射。gin脚手架MVC这类词在Go社区很火但对应到Java Web原生Servlet项目反而是“能看到路由表长相”的唯一形态。服务端收到一个HTTP请求Tomcat根据web.xml或WebServlet注解找到对应的Servlet实例调用service方法然后根据请求方法分派到doGet或doPost。这个链条没有被框架藏起来你在项目里写的每一个路径映射都是自己可控的。2.2 三层架构与MVC的对应请求到底怎么流转MVC里的Model、View、Controller对应到传统Java Web项目通常是ControllerServlet→ Service业务逻辑→ DAO数据访问三层。Controller接收HttpServletRequest把参数解析成Java对象Service层处理业务规则比如计算订单总价、扣减库存DAO层用JDBC操作MySQL。视图层是JSP通过request.setAttribute把数据传到页面再用EL表达式或JSTL渲染出来。这里有一个新手常犯的概念偏差把Model理解成“一个数据库表的实体类”。实际上Model包含两层含义一是承载数据的JavaBean也被称为数据模型二是承载业务状态的Service对象业务模型。一个点餐系统的Order对象是数据模型而OrderService里维护“订单状态流转”的逻辑属于业务模型。两者职责不同页面和数据库都只关心前者而Controller同时依赖二者。2.3 包结构怎么做一个可以直接抄的目录设计下面是我给这种原生ServletJDBC项目推荐的包结构解压后主要补代码分包按职责切不按技术类型切。com.canteen ├── controller // Servlet只做参数接收、调用Service、页面跳转 │ ├── LoginServlet.java │ ├── DishServlet.java │ ├── OrderServlet.java │ └── BaseServlet.java // 路由分发基类 ├── service // 业务层事务边界、业务规则 │ ├── UserService.java │ ├── DishService.java │ └── OrderService.java ├── dao // 数据访问层只负责SQL │ ├── UserDao.java │ ├── DishDao.java │ └── OrderDao.java ├── entity // 数据模型对应数据库表 │ ├── User.java │ ├── Dish.java │ ├── Order.java │ └── OrderItem.java ├── util // 工具类 │ ├── JDBCUtils.java │ └── ConnectionHolder.java └── filter // 过滤器编码、登录校验 ├── EncodingFilter.java └── LoginFilter.javacontroller包只做三件事解析参数、调Service、转发或重定向。service包里绝不出现HttpServletRequest这样才有单元测试的可能。dao包也只接收JavaBean或基本类型参数不碰request和session。提示如果项目里出现了Servlet直接拼SQL的代码说明控制层边界破了。后面重构会异常痛苦因为SQL散落在各个doGet里查都查不完。选型上的另一个理由ServletJSP方案在小型局域网或单机部署场景下启动快、内存占用低。点餐系统放在一台普通电脑或教学服务器上用Tomcat即可不需要额外引入Spring容器。对于学校演示这种场景打开浏览器访问localhost:8080就能跑完整个流程复现成本远低于搭一套带注册中心的微服务。3. 数据库与JDBC层落地点餐系统表设计、连接工具与事务边界3.1 点餐系统需要几张表字段设计和关系点餐系统的最小数据集至少包含用户、菜品、订单、订单明细四张核心表。用户表承载登录菜品表是菜单订单表记录一次下单的主信息订单明细表承载该订单下每道菜的数量和单价。这四张表天然形成一对多关系很适合练手JDBC的关联查询。表名字段类型说明useridINT主键自增usernameVARCHAR(50)用户名唯一索引passwordVARCHAR(100)密码建议存MD5或SHA-256dishidINT主键自增nameVARCHAR(50)菜品名priceDECIMAL(10,2)单价imageVARCHAR(255)图片路径可为空orderidINT主键自增user_idINT外键关联user.idtotal_priceDECIMAL(10,2)总金额statusTINYINT0待支付1已支付2已上菜create_timeDATETIME下单时间order_itemidINT主键order_idINT外键关联order.iddish_idINT外键关联dish.idquantityINT数量priceDECIMAL(10,2)下单时的单价快照注意两点。第一order表在MySQL里是保留字建表时一定要加反引号或者改名为t_order不然SQL执行直接报语法错误。第二order_item里的price字段保存的是下单瞬间的价格快照不是关联dish表实时去查。因为菜品价格会改而历史订单里的价格不应该跟着变——这算一个典型的业务建模细节面试也会被问到。3.2 封装JDBC连接工具类参数、代码与说明JDBC连MySQL的标准写法是Class.forName注册驱动再用DriverManager.getConnection获取连接。很多新手项目里每写一个DAO就复制一遍这个流程我建议统一收进一个JDBCUtils工具类。public class JDBCUtils { private static final String URL jdbc:mysql://localhost:3306/canteen?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD root; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败请检查jar包是否导入); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }驱动类名com.mysql.cj.jdbc.Driver对应MySQL 8.x的Connector/J老项目里写的com.mysql.jdbc.Driver在新版驱动里已经移除了不改直接报ClassNotFoundException。URL里带几个关键参数useSSLfalse关掉SSL握手省掉一堆证书警告serverTimezoneAsia/Shanghai是因为新版驱动要求显式指定时区allowPublicKeyRetrievaltrue在MySQL 8的用户认证是caching_sha2_password时必需不加会报“Public Key Retrieval is not allowed”。提示很多人把这段连接参数的调整当玄学实际不是。MySQL 8默认认证插件变化驱动版本也跟着变参数就得配套。这类报错不止点餐系统会遇到所有JDBC连接MySQL的场景都一样。3.3 Service层必须管住事务用ThreadLocal绑一个Connection正常情况下上面这个getConnection每次调用都会新建连接用完再close。看似没问题但一旦涉及多表写操作就露馅了。点餐系统的下单流程是插入一条订单记录、插入多条明细记录、扣减库存。如果订单插入成功明细插入失败数据库里就会产生孤儿订单。DAO层如果各自拿连接、各自提交事务边界天然被拆碎了。解决思路是把Connection和当前线程绑定放进一个ThreadLocal容器里。Service层开启事务时拿到这个连接设置autoCommit为false所有DAO的数据库操作都从这个ThreadLocal里拿同一个连接最后Service层统一commit或rollback。public class ConnectionHolder { private static final ThreadLocalConnection HOLDER new ThreadLocal(); public static Connection get() throws SQLException { Connection conn HOLDER.get(); if (conn null) { conn JDBCUtils.getConnection(); HOLDER.set(conn); } return conn; } public static void remove() { HOLDER.remove(); } }// Service层事务模板 public void createOrder(Order order, ListOrderItem items) throws SQLException { Connection conn ConnectionHolder.get(); try { conn.setAutoCommit(false); orderDao.insert(order); // 插入主单 for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemDao.insert(item); // 插入明细 } dishDao.updateStock(item.getDishId(), item.getQuantity()); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); ConnectionHolder.remove(); conn.close(); } }这段代码的关键是DAO里不能再用JDBCUtils.getConnection()自己拿连接而要统一用ConnectionHolder.get()。否则事务就不共享连接commit和rollback自然也就控制不住。ThreadLocal的作用是让同一个线程在请求处理过程中始终持有同一个数据库连接处理完必须remove否则Tomcat线程池复用线程时下个请求会拿到上一个请求残留的连接这是很多“跑几次就报连接关闭”的隐藏原因。另一个细节是setAutoCommit(true)恢复再close是为了连接归还到连接池以后引入连接池时时状态干净。如果项目只用DriverManager不恢复问题也不大但养成恢复习惯后面切Druid或HikariCP时才不会有莫名的事务残留。4. Servlet控制层与JSP视图层最小实现路由、过滤器和页面拼接4.1 用BaseServlet基类消灭重复的doGet/doPost原生Servlet开发中最大的痛点不是写Servlet而是写一堆重复代码每个Servlet继承HttpServlet重写doGet和doPost两个方法里大概率还是同一种处理逻辑。我习惯的做法是写一个BaseServlet基类重写service方法通过method参数反射调用对应的业务方法。WebServlet(name BaseServlet, urlPatterns /base) public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); String method request.getParameter(method); if (method null || method.isEmpty()) { throw new ServletException(请求缺少method参数); } try { // 通过反射调用当前Servlet中方法名相同的方法 Method targetMethod this.getClass().getMethod(method, HttpServletRequest.class, HttpServletResponse.class); String result (String) targetMethod.invoke(this, request, response); if (result ! null result.startsWith(redirect:)) { response.sendRedirect(request.getContextPath() result.substring(9)); } else if (result ! null) { request.getRequestDispatcher(result).forward(request, response); } } catch (NoSuchMethodException e) { response.sendError(405, 请求的方法不存在); } catch (Exception e) { throw new ServletException(调用方法[ method ]失败, e); } } }子类Servlet的业务方法统一写成返回字符串的形式。返回“redirect:xxx”就走重定向返回“/pages/xxx.jsp”就走转发。这样好处很明显新增一个页面动作时不用再管doGet和doPost的区分子类只写业务方法。WebServlet(/dish) public class DishServlet extends BaseServlet { private DishDao dishDao new DishDao(); // 对应 URL: /dish?methodlist public String list(HttpServletRequest request, HttpServletResponse response) { ListDish dishList dishDao.findAll(); request.setAttribute(dishList, dishList); return /pages/dish_list.jsp; } // 对应 URL: /dish?methoddetailid1 public String detail(HttpServletRequest request, HttpServletResponse response) { int id Integer.parseInt(request.getParameter(id)); Dish dish dishDao.findById(id); request.setAttribute(dish, dish); return /pages/dish_detail.jsp; } }这段代码里反射调用的方法签名固定为(String method, HttpServletRequest, HttpServletResponse)返回String。参数从request.getParameter里取这本身就是路由映射的一种实现方式——很多人在课设里遇到了就止步于此其实Spring MVC的HandlerMapping本质上也在干类似的事情只不过把method参数换成了方法上的注解值。4.2 两个过滤器必写编码过滤器与登录拦截器原生Servlet项目里新建的登录、菜品、订单Servlet很容易出现中文乱码和未登录访问。过滤器能统一搞定这两件事。WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; request.setCharacterEncoding(UTF-8); response.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }编码过滤器必须在最前面执行解决POST请求的请求体编码和响应编码。GET请求的查询字符串编码由Tomcat的URIEncoding参数决定如果仍乱码需要去conf/server.xml里把Connector配置改为URIEncodingUTF-8。登录拦截过滤器的逻辑是白名单路径直接放行其余路径检查session里有没有loginUser对象。WebFilter(/pages/*) public class LoginFilter implements Filter { private static final ListString WHITE_LIST Arrays.asList(/pages/login.jsp, /pages/register.jsp); Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String path request.getRequestURI().substring(request.getContextPath().length()); if (WHITE_LIST.contains(path)) { chain.doFilter(request, response); return; } Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /pages/login.jsp); return; } chain.doFilter(request, response); } }注意拦截路径范围WebFilter(/pages/*)拦JSP页面WebFilter(/dish)拦Servlet入口。如果只拦截Servlet用户直接浏览器输入JSP路径依然能绕过登录。另一种做法是放在/*路径里然后通过白名单放行静态资源和登录页效果一样看个人习惯。4.3 JSP里只用JSTL做渲染把Java代码赶出页面很多课设项目里JSP页面直接%new DishDao().findAll()%。这种写法在演示时没问题但页面越改越乱。正确做法是Servlet查好数据放进request域JSP用EL表达式和JSTL标签渲染。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body h2菜品列表/h2 table border1 tr th编号/th th菜名/th th价格/th th操作/th /tr c:forEach items${dishList} vardish tr td${dish.id}/td td${dish.name}/td td${dish.price}/td tda href${pageContext.request.contextPath}/cart?methodadddishId${dish.id}加入购物车/a/td /tr /c:forEach /table /body /html页面里不再出现一行Java代码。${dish.price}会自动调用Dish的getPrice方法这是JavaBean规范在JSP里的体现。如果页面里出现${dish.xxx}取不到值先检查entity里的字段有没有生成对应的getter和setter这是最常见的问题。JSP和Servlet的分工到这个程度就清晰了Servlet负责准备数据、决定跳转JSP只负责把数据画出来。项目里如果还有页面在做计算、在拼接HTML字符串都该重构到Service层或Controller层。5. 避坑排查原生ServletJDBC项目常见五类翻车现场5.1 Service层事务失效订单表写入了明细却丢了现象下单接口调用后order表多了一条记录order_item表空。或者反过来明细表有数据但主订单不存在。原因DAO层每个方法都自己new了一个Connectionordered做插入用连接A下单明细用连接B事务各管各的。A提交成功、B失败时A已经落库无法回滚。这种事务失效是最隐蔽的因为代码看着每一条SQL都能执行成功。解决把所有DAO方法改成从ConnectionHolder.get()获取连接事务边界统一收口到Service层由Service控制setAutoCommit(false)和commit/rollback。改完之后在下单方法里人为制造一次明细插入异常验证主订单是否跟着回滚。5.2 GET请求中文乱码查“宫保鸡丁”页面全变问号现象从菜品列表点“加入购物车”跳到购物车页面后菜名显示为“”搜索框输入中文查询结果为空。原因POST请求经过编码过滤器能正常但GET请求的查询字符串是Tomcat按ISO-8859-1解析的中文到Tomcat手里已经变了码。即使过滤器里写了setCharacterEncoding(“UTF-8”)对GET的URL参数也不生效。解决在Tomcat的conf/server.xml里给Connector添加参数URIEncodingUTF-8。另外前端表单提交要显式写methodget时对中文参数做encodeURIComponent编码后端再用URLDecoder.decode还原。改完重启Tomcat重点验证带中文参数的GET请求链路。5.3 JDBC连接MySQL 8报错Communications link failure与Public Key Retrieval现象项目部署到新环境后首次访问数据库报Communications link failure连带报Public Key Retrieval is not allowed。原因数据库用的MySQL 8.0默认认证插件是caching_sha2_password而驱动版本和连接参数没有配套。用户名密码正确也会被拒绝因为客户端需要先向服务器请求公钥做加密传输。解决确认JDBC驱动换成mysql-connector-java 8.0.x依赖连接URL加上allowPublicKeyRetrievaltrue和useSSLfalse。如果还不行把数据库用户改成mysql_native_password认证方式但那只是临时绕开推荐直接升级驱动参数。另外serverTimezone参数也要带上否则时间字段会偏移8小时。提示这类报错不是代码逻辑问题基本属于环境配置。排查优先级是驱动版本 → 连接参数 → 后端防火墙端口大多数情况卡在前两步。5.4 Servlet路径映射冲突明明有页面却一直404现象访问/dish?methodlist正常但访问/dish/list直接404。访问/login也进不去Tomcat报重复映射错误。原因原生命名Servlet时同时用了WebServlet注解和web.xml里的 导致一个Servlet被映射到多个路径或者路径带斜杠的写法在注解里写成了/dish/*跟另一个Servlet的精确路径/dish冲突。Tomcat不启动时会直接报错启动后则表现为404、405混着来。解决定一个规则——路径映射只允许出现在WebServlet注解里web.xml里不再配置任何Servlet避免双份映射。业务路径统一用一级路径加method参数比如/dish?methodlist不搞多层路径。确需REST风格路径时用WebServlet(/dish/*)并在Servlet里取request.getPathInfo()做二次路由。5.5 JSP页面绕过Servlet直接连数据库项目“能跑”但废了现象别人接手代码时发现JSP页面里嵌着一堆%new DishDao().findAll()%业务逻辑写进页面一改页面格式就动到数据逻辑。原因开发时为了图快直接在页面里new DAO查数据省得写Servlet再转发。第一次这么做后面就收不住了页面越写越长项目完全退化成JSPJDBC的面向过程写法。解决如果只是少量页面按第2章的包结构把逻辑迁移到ServletServiceDAO。页面里的Java代码块全部删掉换EL表达式和JSTL。对于一个课设项目来说这个重构量通常在一个晚上能完成。做完后再增加新页面强制要求走控制器不再让页面直连数据库。6. 进阶自查怎么验证你的点餐系统不是“能跑但结构烂”项目交出去之前别只试“能登录、能下单”还要按MVC的纪律自查一遍。我习惯用下面几个问题过一遍代码自查项合格标准不合格表现控制器每个Servlet方法不超过30行只做参数解析、调Service、跳转Servlet里出现SQL语句Service层所有多表写操作包在事务里DAO自己提交事务DAO层只接参数、执行SQL、返回实体DAO里写业务判断视图层JSP页面没有Java代码块只用EL和JSTL页面里new DAO连接管理所有连接都在finally里关闭ThreadLocal在请求结束remove连接泄漏导致运行变慢如果自查下来有不合格项按照第4章和第5章给出的代码改掉。这边有一个我常用的验证技巧在JDBCUtils的getConnection里加一个静态计数器每次open加1、每次close减1请求处理完后看计数器是否归零。不归零说明有连接泄漏逐条检查DAO的关闭逻辑。针对多表写事务手动测一个场景在OrderService的明细插入循环里人为抛一个SQLException然后去数据库看主订单是否插入。正确的MVC事务设计下订单表和明细表都应该是空的如果主订单还在说明事务边界失效回到5.1去修。这个项目做完你可以试着把它改造成三层微服务或引入连接池你会发现当初一段段手写的JDBC代码正是你理解各种框架底层的基础。我第一次做完这个项目时对着Spring的DataSourceTransactionManager看了很久突然想起自己手写的ConnectionHolder和事务模板心里才彻底踏实。希望帮到你。本文还有配套的精品资源点击获取
返回列表