news 2026/9/24 21:48:20

原生Servlet+JDBC点餐系统:从MVC分层到事务管理的实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
原生Servlet+JDBC点餐系统:从MVC分层到事务管理的实战指南

简介:这是一份基于MVC开发模式的原生Servlet+JDBC点餐系统完整项目包,面向Java Web学习者、毕业设计及课程设计人群,用于掌握Servlet核心处理流程、数据库交互与项目分层思想。包内共139个文件,涵盖81张界面素材图片、20个JSP页面、6个Java源文件及对应class文件、7个依赖JAR包,同时还包含SQL建表脚本、需求文档和项目配置文件,便于直接导入IDE运行与二次开发。压缩包仅3.76MB,结构紧凑,适合快速上手。目前已有61人学习下载。通过该项目可系统梳理Servlet生命周期、doGet/doPost请求分发、JDBC增删改查、HttpSession会话管理、用户认证与权限控制等关键知识点,并结合前端页面理解MVC各层协作方式,是巩固Java Web基础、积累实战经验的实用资源。

1. 点餐系统为什么还要用原生Servlet+JDBC:一个MVC项目能学到什么

在Java Web里,基于MVC开发模式用原生Servlet+JDBC写一个点餐系统,是最能看清框架底层的练手项目。现在Spring Boot遍地都是,但真让你跑这样一套原生项目,很多人会反问“这年头还写这个?”——直到笔试遇到请求生命周期、事务边界,或者接手老系统时,才发现底层没打牢。点餐系统恰好把菜单查询、下单扣库存、订单事务这些最核心的Java Web能力完整过一遍,业务实体清晰,交互链路完整。这个方向适合正在学Java Web、缺一个完整项目经验的初学者,也适合想搞清楚框架底层逻辑的开发者。把这套项目跑通,再回头看任何MVC框架,都能对应上它帮你省掉了哪些重复劳动。

2. MVC三层在点餐系统里的落地:包结构、请求流转与职责边界

2.1 包结构先分对:entity、dao、service、servlet各管什么

很多项目的分层乱,根子在于一开始没想清楚每一层的产出物是什么。我一般会先按这个结构建工程:

com.restaurant ├── entity // Model:Dish、Order、OrderItem,字段对应数据库表 ├── dao // 数据访问:DishDao、OrderDao、OrderItemDao ├── service // 业务:DishService、OrderService ├── servlet // 控制层:DishServlet、OrderServlet、UserServlet ├── filter // CharacterEncodingFilter 编码过滤器 ├── util // DBUtil:连接获取与关闭 └── webapp ├── pages // JSP:menu.jsp、order.jsp、orderList.jsp └── static // css、js

这是MVC在Java Web项目里最常见的映射方式。entity里只放字段和getter/setter,不掺任何数据库操作——有人习惯把SQL写在实体类里,短期看省事,等订单表和菜品表一关联,实体类就变成谁都不敢动的臭泥潭。dao层的方法严格按数据操作命名:findById、findAll、insert、updateStatus,它不回答“这个订单能不能取消”这类问题,能不能取消是service层的职责。service层才是业务规则的所在地:下单时要校验菜品是否存在、库存够不够,写订单表和订单明细要放在同一个事务里。servlet层只做三件事——从request取参数、调service、决定返回JSP还是JSON。

判断分层是否合理的办法很简单:如果数据库表结构改了,你希望只动entity和dao;如果需求变了,你希望只动service;如果页面交互变了,你希望只动servlet和View。任何一层的变化穿透到其他层,说明边界划错了。servlet里不直接写SQL这条,建议当成硬性规范,不然最终一定有人图省事把JDBC写进servlet,让Controller变成数据访问层。这个分层的核心依据是“变化的方向不同”:SQL变化来自表结构调整,业务变化来自产品需求,请求处理变化来自HTTP协议和前端约定。三个方向绑在一个类里,改一处就要重新测整个链路。

2.2 请求流转:从点击“下单”到数据库,一次完整路径

以点餐系统的下单为例,完整请求生命周期可以拆成七步。第一步,浏览器表单POST到/order/create。第二步,web.xml里的Servlet映射或@WebServlet("/order/create")注解把URL交给OrderServlet。第三步,doPost方法先执行request.setCharacterEncoding("UTF-8"),再getParameter("dishId")拿到参数。第四步,调用OrderService#createOrder,service里校验菜品是否存在、计算价格。第五步,OrderDao创建订单主表记录,OrderItemDao写入明细。第六步,两个insert成功后connection.commit(),失败rollback()。第七步,OrderServlet把结果转发到订单结果页或输出JSON。

用web.xml管理映射的方式很多老项目还在用,值得看一次:

<servlet> <servlet-name>orderServlet</servlet-name> <servlet-class>com.restaurant.servlet.OrderServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>orderServlet</servlet-name> <url-pattern>/order/create</url-pattern> </servlet-mapping>

这段配置要说明两个点。servlet-class必须是包含包名的完整类名,写成OrderServlet会直接ClassNotFound。servlet-name只是内部标识,和类名可以不一致,但全项目要唯一,否则容器启动时就会报Servlet [xxx] is already mapped。注解方式更省事,但web.xml的好处是不同环境的URL映射可以通过配置文件替换,不用重新编译。

第七步有个容易忽略的坑:如果你在doGet里转发到JSP,转发后JSP会继续走同一次请求,此时request域里的对象能直接取到;如果用了重定向,浏览器发起第二次请求,request域是空的,数据必须用session或重新查一遍。点餐系统的“下单后跳转订单列表”适合用重定向,因为刷新页面时不能把同一个订单再提交一次。

2.3 为什么点餐系统适合用MVC:菜单模块与订单模块互不污染

点餐系统的业务实体多且规则集中:菜品管理侧重查询,订单管理侧重写入和状态流转,用户管理侧重安全校验。三个模块如果写在一个Servlet里,最直接的后果是doGet和doPost堆满if-else分支,每加一个功能都要从头读一遍代码,改菜的接口时手一抖就可能影响下单逻辑。

用上MVC后,菜单查询只走DishServlet→DishService→DishDao一条链,订单走OrderServlet→OrderService→OrderDao另一条链。两边servlet互不引用,即使后面要把菜品模块从数据库读取改成Redis缓存,也只是替换DishService内部实现,OrderServlet一行都不用动。这就是第2.1节说的“变化隔离”:一个模块的改动不越过自己的边界。

从请求流转还能看出另一层价值:分层给了你一个天然的日志埋点位置。servlet入口统一记录“谁在什么时间请求了什么功能”,service层记录“这笔订单算出来多少钱”,dao层记录“实际执行了哪条SQL”。出问题的时候顺着这三层日志看,定位速度比在一个大方法里翻打印语句快得多。这层能力是框架帮不了你的,项目结构自己决定。

2.4 什么时候用MVC反而笨重:不要无脑套分层

MVC不是银弹。点餐系统里那种单表维护页面,比如管理员改一条轮播图配置,只有三个字段,走MVC要建entity、dao、service、Servlet、JSP五个类,成本远大于收益。遇到这种纯CRUD,直接在Servlet里用JDBC更新一条记录更务实。判断标准是业务规则的数量:规则超过两条,MVC帮你隔离复杂度;规则为零,分层就是给自己找事。

我在实际项目里会把它当一个原则:MVC是给“会变化的业务”用的。点餐系统里订单状态流转(待接单、制作中、已完成、已取消)是天然会持续加规则的部分,必须走完整分层;而餐桌编号这种字典数据,怎么改都是update语句,就不要强行套框架。

3. 用原生Servlet实现点餐核心接口:菜单查询、下单与订单展示

3.1 菜单列表查询:从JDBC连接配置到JSON输出

先写DBUtil,所有dao的前提:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/restaurant" + "?useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

这段代码是全项目的数据库入口,几个参数在后端开发里最容易出问题。useSSL=false在MySQL 8.x上是必加的,不加它一些版本会在握手时尝试SSL加密,本地自签名证书校验失败直接告警;serverTimezone=Asia/Shanghai解决的是服务器时区与驱动默认时区不一致导致的日期偏差,缺失时查询结果的时间字段可能少8个小时。Class.forName在JDBC 4.0之后可以省略,因为驱动包里有SPI自动注册文件,但显式写出来在Web容器类加载器隔离的场景下更稳。

再写DishDao的菜单查询:

public List<Dish> findAll() throws SQLException { String sql = "SELECT id, name, price, description FROM dish WHERE status = 1"; List<Dish> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Dish d = new Dish(); d.setId(rs.getInt("id")); d.setName(rs.getString("name")); d.setPrice(rs.getBigDecimal("price")); d.setDescription(rs.getString("description")); list.add(d); } } return list; }

这里用try-with-resources关闭三个资源,是JDBC里最低成本的防连接泄漏写法,比在finally里一个个close要稳,因为finally块里一旦第一个close抛异常,后面的close直接不执行。price字段用BigDecimal接收而不是double,金额计算最怕浮点误差,BigDecimal在价格和库存场景是写死的规定。SQL里WHERE status = 1表示只查在售菜品,下架菜品不在菜单里出现,这个过滤条件放SQL里比放Java里更合适,因为数据库可以利用索引,也避免把无谓的数据传到业务层。

DishServlet负责输出JSON:

@WebServlet("/dish/list") public class DishServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { resp.setContentType("application/json;charset=UTF-8"); resp.setCharacterEncoding("UTF-8"); List<Dish> dishes = new DishService().findAllDishes(); String json = new Gson().toJson(dishes); resp.getWriter().write(json); } }

一个很容易被忽略的执行顺序:setCharacterEncoding("UTF-8")必须在getWriter()之前调用,否则编码设置不生效,页面拿到的中文会变成乱码。setContentType里已经带了charset,理论上可以不写第二行,但我习惯都写,因为某些容器对这两行的处理有差异。如果开发环境是VSCode,记得把Maven的编译输出和Tomcat的部署目录对应起来,VSCode本身不自带Servlet容器支持,需要装Tomcat插件并配置好classpath,不然每次改动都要手动拷贝class文件。

3.2 下单接口:服务端定价与入库的关键代码

下单是点餐系统里业务规则最集中的地方。前端传来的参数只有dishId和quantity,价格必须后端根据菜品的当前价格计算。如果让前端把price一起POST上来,改一下请求体就能1块钱下单,这是点餐系统最典型的越权漏洞。

@WebServlet("/order/create") public class OrderServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("UTF-8"); int userId = Integer.parseInt(req.getParameter("userId")); int dishId = Integer.parseInt(req.getParameter("dishId")); int quantity = Integer.parseInt(req.getParameter("quantity")); if (quantity <= 0) { resp.sendError(400, "quantity must be positive"); return; } try { Order order = new OrderService().createOrder(userId, dishId, quantity); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write("{\"orderId\":" + order.getId() + "}"); } catch (BusinessException e) { resp.sendError(400, e.getMessage()); } } }

参数校验放在servlet入口,至少挡掉三种非法输入:quantity为负数、dishId不存在、直接POST伪造的price字段。业务异常和系统异常分开处理也很重要——BusinessException返回400给用户看,SQLException记录日志返回500,不要让用户看到一长串堆栈。Integer.parseInt这一行如果前端传了非法字符串会抛NumberFormatException,这个属于参数格式错误,实际项目里要么用包装类型判空,要么用正则过滤,别让异常越过servlet直接打到容器默认错误页。

OrderService里的核心逻辑在上一章的代码块提过,再补一个重要细节:创建订单时要让订单状态处于“待支付”而不是“已完成”。很多新手把状态机的起点和终点搞混,订单一创建就完成,后面接支付模块时发现状态没法流转。

3.3 订单查询与状态更新:PreparedStatement占位符的硬性要求

点餐系统里“按用户查历史订单”“按订单号查详情”都很常用,查询订单号来自用户在页面的输入。这种外来值拼SQL字符串是绝对红线:

public List<Order> findByUserId(int userId) throws SQLException { String sql = "SELECT id, dish_id, quantity, total_price, status, create_time " + "FROM orders WHERE user_id = ? ORDER BY create_time DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); try (ResultSet rs = ps.executeQuery()) { List<Order> orders = new ArrayList<>(); while (rs.next()) { orders.add(mapRow(rs)); } return orders; } } }

PreparedStatement的?占位符会自动转义特殊字符,从源头堵住SQL注入,这是它比Statement拼字符串最本质的优势。同时预编译语句在数据库端可以被重复执行复用,同一SQL跑多遍时的解析成本可以摊薄。这段代码还有个细节:内层ResultSet单独用一个try-with-resources,外层PreparedStatement的关闭会自动把ResultSet也关掉,这里写两层只有一个目的——可读性,让读者一眼看出ResultSet的生命周期边界。

4. JDBC连接与事务管理:连接池、URL参数与下单扣库存的事务边界

4.1 为什么不能每个请求新建连接:连接开销与并发排队

3.1节的DBUtil用DriverManager.getConnection,教学项目没问题,但点餐系统一旦有真实并发——比如食堂中午12点所有人同时下单——问题立刻暴露。每次getConnection都要经历TCP握手、MySQL认证、分配线程资源,一次连接耗时几十毫秒;并发上去后数据库最大连接数一满,后续请求全部排队,页面表现是“转圈”。

常见做法是引入连接池。Druid和HikariCP都是成熟选择,教学项目用Druid多一些,因为自带监控页面,/druid/index.html能看到活跃连接数、SQL执行耗时、事务提交回滚次数。连接池的核心参数你只需要理解五个:initialSize是启动时建立的连接数,maxActive是连接上限,minIdle是最小空闲数,maxWait是拿不到连接时的最大等待毫秒数,validationQuery是心跳检测SQL。点餐系统初期规模配成3、10、2、3000、SELECT 1就够用了,后面监控数据出来再调。

4.2 MySQL 8.x连接参数:把useSSL、sslmode、时区一次讲清

这部分值得单独开辟一节,因为JDBC URL的每个参数背后都对应一个真实报错。先看一份完整的Druid配置:

driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/restaurant?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4&allowPublicKeyRetrieval=true username=root password=123456 initialSize=3 maxActive=10 maxWait=3000 minIdle=2 validationQuery=SELECT 1

useSSL=false对应MySQL 8.x默认开启SSL,本地自签名证书会导致连接报错,所以开发环境必须关掉;这也就是很多新手遇到的“连接超时”“SSLHandshakeException”的根因。在高版本Connector/J里,控制SSL的官方参数其实是sslMode,可选DISABLED、PREFERRED、REQUIRED、VERIFY_CA、VERIFY_IDENTITY几种,useSSL=falsesslMode=DISABLED描述的是同一件事,如果你看到的报错信息带SSL字样,先检查这两个参数是不是互相冲突。serverTimezone不设置时驱动用服务器默认时区,常见的坑是差8小时,数据入库时看着不对。characterEncoding=utf8mb4是为了存emoji和生僻字,MySQL 8.x的表也建议建表时统一DEFAULT CHARSET=utf8mb4,光改URL不改表结构同样会报Incorrect string value。allowPublicKeyRetrieval=true专门解决MySQL 8.x的caching_sha2_password认证插件在非SSL连接下报Public Key Retrieval is not allowed的问题,使用root账号时很常见。五个参数里哪怕漏一个,项目都可能跑不起来,这类问题大多数时候不是代码bug,是配置参数没对齐。

4.3 下单事务的commit和rollback:边界位置决定成败

JDBC默认autocommit=true,每条SQL各自成一个事务。这就意味着在2.2节的第七步如果忘了手动管理事务,订单主表和订单明细表之间没有原子性。正确写法是把“写订单”“写明细”“扣库存”包进同一个手动事务:

public void createOrderWithTransaction(int userId, int dishId, int quantity) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); DishDao dishDao = new DishDao(conn); OrderDao orderDao = new OrderDao(conn); OrderItemDao itemDao = new OrderItemDao(conn); Dish dish = dishDao.findByIdForUpdate(dishId); if (dish == null || dish.getStock() < quantity) { throw new BusinessException("库存不足"); } orderDao.insert(new Order(userId, dishId, quantity)); itemDao.insertOrderItem(item); dishDao.decreaseStock(dishId, quantity); conn.commit(); } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException re) { re.printStackTrace(); } } throw new SQLException("下单失败,事务已回滚", e); } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }

三个细节值得记。第一,findByIdForUpdate实际执行的是SELECT ... FOR UPDATE,它在事务内锁定菜品行,阻止另一个并发事务读到同一份库存然后重复扣减,这是“不超卖”的第一道防线。第二,conn.setAutoCommit(true)还原这段经常被漏掉,如果连接来自连接池,还原不彻底的话,下一个请求拿到这条连接时仍然处于事务中,忘记commit会导致数据悄悄丢失。第三,rollbackclose的顺序不能反,先rollback再close,close时连接池才会认为连接是干净的;反过来先把连接还给池子再回滚,等于把脏连接暴露给了其他请求。

注意:写事务代码时,尽量让dao方法接收Connection参数而不是内部自己new连接,否则你无法控制多个DAO在同一个连接上执行——这也是为什么第2.1节里建议在service层统一管理连接的生命周期。

4.4 隔离级别与超卖:要不要上悲观锁

超卖场景在点餐系统里表现为:库存只剩最后一份,A和B同时提交订单,两个事务都读到stock=1,都通过了库存判断,然后都执行扣减,最终库存变成-1。4.3节的SELECT ... FOR UPDATE是悲观锁方案,它直接锁行,简单可靠,代价是并发下吞吐受限。乐观锁方案给菜品表加version字段:

UPDATE dish SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ?

返回受影响行数如果小于1,说明version已被其他人改过,整个下单事务回滚重试。乐观锁适合读多写少、冲突概率低的场景;点餐系统属于典型的中低并发写场景,用悲观锁也不会有什么性能压力。如果哪天业务变成了限时秒杀,那就该把库存挪到Redis里做原子扣减,MySQL只作为最终落库,这就超出本文范围了。

5. 点餐系统避坑指南:从404到中文乱码,5个高频问题排查

5.1 Servlet404或ClassNotFoundException:构建产物与URL映射先查

现象:Tomcat正常启动,浏览器访问/dish/list返回404,日志里没有任何异常。

原因排查有固定顺序。第一步,检查IDEA或Eclipse的编译输出目录下有没有DishServlet.class,很多人改了代码没重新编译,Tomcat跑的其实是旧class。第二步,核对注解@WebServlet("/dish/list")和访问路径完全一致,大小写、开头斜杠、尾斜杠都不能错,访问路径漏掉开头的/会变成相对路径解析,这是Servlet 404里比例最高的原因。第三步,看项目是不是以war包形式部署,Tomcat解压到webapps下的目录和实际项目target目录经常不是同一个,改了代码但没重新打包就会出现在“自己机器上能跑、部署到服务器就是404”的怪象。

解决:IDEA里执行Build->Rebuild Project,清空Tomcat的work目录后重启。这个问题的本质不是代码bug,而是构建产物和运行环境不一致。

5.2 MySQL连接报错:驱动版本、useSSL、serverTimezone三件套

现象:服务启动时日志抛ClassNotFoundException: com.mysql.jdbc.Driver,或持续报Communications link failure

原因:MySQL 8.x版本类名从com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver,旧类名在新版本驱动里已标记废弃;反向的问题也存在,用5.1.x驱动连MySQL 8.x服务器,协议版本不兼容。除此之外,8.x默认SSL握手,本地开发环境没配置证书时,SSLSocket连接会间歇性失败,这类问题表现不固定,最容易被当成网络抖动。

解决:驱动jar版本与MySQL服务器大版本对齐,JDBC URL带上第4.2节里的useSSL、serverTimezone、allowPublicKeyRetrieval三个参数。这里提一句,JDBC自动注册机制在绝大多数情况下可用,但如果你的Web容器有类加载器隔离(比如Tomcat的部分共享库),显式Class.forName能少踩一个坑。

5.3 中文乱码:request、response、数据库三层一起查

现象:页面菜品名显示è??å??,或者数据库里存进去的就是乱码。

原因:乱码从来不是单一原因,一般要查三层。请求方向没设request.setCharacterEncoding("UTF-8"),或者设了但它出现在getParameter之后,等于没设;响应方向没设setContentType("text/html;charset=UTF-8"),容器用默认ISO-8859-1输出;最隐蔽的是JDBC URL里没有characterEncoding=utf8mb4并且数据库表本身charset不是utf8mb4。

解决:三层统一。Servlet里第一行就设request编码;输出前设response的ContentType;JDBC URL带编码参数;建表语句写死DEFAULT CHARSET=utf8mb4。还有一个容易漏的是IDE的properties文件编码,如果db.properties里写的密码含中文,而IDE默认存成GBK,读出来就是另一串字符,这类问题看文件编码一眼就懂。

5.4 本地正常、部署到服务器就连不上数据库

现象:点餐系统在Windows本机跑没问题,部署到Linux服务器后访问菜单接口报Communications link failure,数据库日志提示连接被拒绝。

原因:从网络的五个点逐个排查。第一,MySQL没有开启远程访问,默认只允许localhost连接;第二,云服务器安全组或iptables只放行了80/443,3306没有放行;第三,JDBC URL里写死了localhost或127.0.0.1,但数据库其实在别的机器上;第四,防火墙过了但MySQL账号的host字段是localhost,需要通过授权语句把host改成允许的网段;第五,服务器时区与MySQL时区不一致,导致时间字段偏移8小时,这个严格说不算连不上,但数据相貌不对。

解决:数据库账号host授权到%或指定内网IP,安全组放行3306,JDBC URL里的地址抽到配置文件里用环境变量注入,避免每次部署都改代码。生产环境最好不要把3306直接暴露到公网,走内网地址或者跳板机更稳妥。

5.5 连接泄漏:没有报错,但连接数悄悄占满

现象:系统连续运行几天后show processlist里全是Sleep状态连接,max_connections被打满,新请求全部超时,重启Tomcat又能撑几天。

原因:代码里某个分支忘记关闭Connection。最典型的两个场景:DAO内部new DBUtil().getConnection()但返回前没close;或者用了finally手工关闭,但ResultSet的close抛了异常导致后面两个资源没关掉。这种问题不会立刻报错,是那种慢慢把系统拖死的慢性病。

解决:全面改用try-with-resources,引入连接池设置removeAbandonedTimeout或HikariCP的connectionTimeout。观察手段也很简单,MySQL执行show status like 'Threads_connected'配合连接池监控面板,看哪个SQL长期占着连接不释放。连接泄漏是最常见的“开发环境永远复现不了、生产环境几天挂一次”的问题,属于典型的血泪型踩坑。

6. 点餐系统的下一步改造:参数化、流式查询与接口验证技巧

点餐系统跑通之后,值得做三件事。

第一,把DBUtil里的URL、账号密码全部搬进db.properties,用Properties.load读取,配合环境变量区分开发和生产两套配置。这样项目在本地、测试、生产之间迁移时,只需要替换配置文件,不需要重新编译源码。第二,如果点餐系统后期加了销售报表,比如统计一周内每道菜的销量,查询结果可能上万行,一次性executeQuery会把所有结果加载进内存。这时可以用MySQL JDBC的流式查询:Statement设置setFetchSize(Integer.MIN_VALUE),让驱动按游标一行行取回,内存占用从O(n)降到O(1)。但要注意流式查询期间连接不能执行第二条SQL,结果集使用完必须立刻关闭。

第三,养成用curl验证接口的习惯。前端页面还没写的时候,接口能不能用,一条命令就能断言:

curl -X POST "http://localhost:8080/restaurant/order/create" \ -d "userId=1&dishId=3&quantity=1" curl "http://localhost:8080/restaurant/dish/list" | jq '.[0]'

验证的重点不是“有没有返回”,而是三件事:状态码是200还是异常4xx/5xx;JSON字段名和前端约定的是否一致,比如把orderId写成id,前端解析时直接undefined;异常请求比如dishId不存在、quantity=-1时服务端有没有正确返回400而不是抛500。这三个断言全部通过,再打开浏览器看页面。

我做这类Servlet+JDBC项目时有个习惯:每写完一个接口,先用curl把正常路径和异常路径各打一遍,确认状态码和返回结构,再碰JSP页面。这个习惯帮我避开过很多次“前端说后端有问题、后端说前端没对齐”的扯皮。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 21:48:19

轻松掌握 LangGraph 的状态与节点:详细原理与代码实战

1. 为什么先理解状态与节点LangGraph 是 LangChain 生态中用于构建有状态、可循环、可控制流程的 Agent 框架。和普通的大模型单次调用不同&#xff0c;它把一次复杂任务拆成一张「图」&#xff1a;图里有多个节点&#xff0c;节点之间通过边连接&#xff0c;数据则存放在共享的…

作者头像 李华
网站建设 2026/9/24 21:48:18

400 Bad Request深度解析:从HTTP状态码到前后端排查实战

作为一个天天跟接口打交道的程序员&#xff0c;你大概率遇到过这种场景&#xff1a;前端测得好好的&#xff0c;后端本地也调得好好的&#xff0c;一上测试环境&#xff0c;控制台突然蹦出一个大红错——400 Bad Request。更让人抓狂的是&#xff0c;请求没发出去、页面没崩、网…

作者头像 李华
网站建设 2026/9/24 21:47:56

Windows与Ubuntu双系统安装全攻略:从U盘制作到引导修复

装双系统这件事&#xff0c;说难也难&#xff0c;说简单也简单。从最早用光盘引导、手动改menu.lst的年代&#xff0c;到如今UEFIGPT下用一个U盘就能走完整个链路&#xff0c;工具换了好几代&#xff0c;但我帮人装了二十多台机器以后&#xff0c;最大的感受是&#xff1a;大部…

作者头像 李华
网站建设 2026/9/24 21:47:37

Windows开机密码忘了怎么办?三种重置方法详解

说个真实经历。前两年我出差回来&#xff0c;打开笔记本准备赶方案&#xff0c;屏幕上弹出了密码输入框&#xff0c;我愣是好几分钟&#xff0c;硬是想不起来开机密码是什么。平时一直用指纹和PIN码登录&#xff0c;那个真正的账户密码至少一年没敲过&#xff0c;脑子里只剩一个…

作者头像 李华
网站建设 2026/9/24 21:47:01

de4dot脱壳.NET Reactor 4.9:从命令行到手工修复完整指南

简介&#xff1a;这份资源是面向.NET逆向工程师的脱壳工具包&#xff0c;基于de4dot Reactor v4.9 Mod&#xff0c;可用于脱去.NET Reactor 4.9及以下版本的保护壳&#xff0c;适合需要分析混淆程序、恢复程序集结构的逆向场景。压缩包整体约2.8MB&#xff0c;共51个文件&#…

作者头像 李华
网站建设 2026/9/24 21:46:20

palera1n 越狱完整实操:老设备在 iOS 15+ 上快速拿回可写系统

palera1n 越狱完整实操&#xff1a;老设备在 iOS 15 上快速拿回可写系统 【免费下载链接】palera1n Jailbreak for A8 through A11, T2 devices, on iOS/iPadOS/tvOS 15.0, bridgeOS 5.0 and higher. 项目地址: https://gitcode.com/GitHub_Trending/pa/palera1n palera…

作者头像 李华