Spring 文件上传全链路深度解析(二):限额、故障与不落盘直传
浏览器控制台一行 net::ERR_CONNECTION_RESET,Network 面板里那条 POST /api/upload 停在待响应状态,状态码是空的。翻后端日志——INFO 翻到 DEBUG,一行异常都没有。把同一个文件切成 5 MB 分片再传,一切正常。 一个上传请求,从 Nginx 到对象存储,中间有几道限额?分别由谁拦下? 为什么被拒绝之后,客户端看到的是”连接被重置”而不是一个 413? Tomcat 有个 maxSwallowSize(“最多替客户端吞掉多少请求体”)——它为什么能让应用连一句日志都留不下? 为什么写在 Controller 里的异常处理方法,明明写了却没被调用? 上传一定要先落到本地临时文件吗?能不能边收边写进对象存储? 预签名 URL 直传和”应用流式转发”差在哪里,代价是什么? MultipartFile.getOriginalFilename() 到底能不能信? 这篇接着 (一):从报文到临时文件 往下走。第一篇讲报文怎么被解析、MultipartFile 从哪里来;这一篇只关心一件事:这套机制在真实系统里以什么方式失败,以及...
Spring 文件上传全链路深度解析(一):从报文到临时文件
引子:同一项目里的两种表现——3MB 传不上去,200MB 却传上去了 同一个项目里有两个上传接口:POST /upload(Spring MVC + Tomcat)传一个 3MB 的 PDF 直接报错;另一个接口(Spring WebFlux,流式接收)却敢让人传 200MB 的视频。很多人第一反应是”配置没写对”——但更值得先问一句:一个 200MB 的文件传到服务器上,它在某一刻到底躺在哪里? 答案很反直觉:用 Spring Boot 的默认配置,一个上传请求里每一个文件都会先落到磁盘上,哪怕它只有 1 字节;而这个临时文件什么时候被删,取决于请求什么时候走完,不取决于你什么时候把它存到自己的目录里。 这两件事,决定了上传链路上绝大多数”玄学问题”:临时目录被悄悄撑爆、异常抛出后临时文件残留、把文件存进了业务目录、临时文件却可能还占着空间。 Spring MVC 默认用哪个组件把 multipart 请求包装成可用的对象?真的是”classpath 里有 commons-fileupload 就自动切换”吗? 从 Boot 2.x 升到 3.x,“上传坏了”到底坏在哪儿...
@Transactional 为什么会失效(四):失效原因映射与收束
引子:从开头那段没回滚的代码说起 导读开头那段代码: 1234public void createOrder(OrderDTO dto) { stockService.deduct(dto.getSkuId(), dto.getCount()); this.saveOrder(dto); // 同类内部调用} 跑一遍,库存扣了、订单也落了——saveOrder 上那个 @Transactional 像没写一样。前面五个部分已把整条链拆过一遍,这一篇倒过来用:从现象出发,反推断在哪一环。 失效的样子千奇百怪,断点却只有几个:自调用、private、接口注解、被吞的异常、被换的线程——都会落回同一张图。 本篇接第三篇(账本已摊开),覆盖第六~八部分与延伸阅读:失效现象映射、自调用与 @Async、切面顺序、响应式与 AspectJ。口径同导读:Spring Framework 6.1.8 + Spring Boot 3.3.x(Java 17)。 本篇要回答的问题: 十二种失效现象——自调用、private、接口上的注解……分别断...
@Transactional 为什么会失效(三):事务管理器的 ThreadLocal 账本
引子:事务是开了,可它到底开在哪 上一篇的结尾停在 TransactionInterceptor.invokeWithinTransaction 拿到事务属性、选出事务管理器的那一刻。注意,到这里为止还没有一条 SQL 被执行,也还没有连接被占用——真正”开事务”,发生在 PlatformTransactionManager.getTransaction 里面。而在那之前,Spring 必须先回答一个更底层的问题:所谓的”当前事务”,究竟是个什么东西,又存在哪里? 换个说法:@Transactional 让方法”进了事务”,可事务本身既不是对象,也不是变量,它散落在当前线程的几个 ThreadLocal 里——连接、同步回调、只读标记、隔离级别、事务名。这些账目一旦记错线程,事务就会在人眼皮底下消失,不报错、不打日志。这一篇要做的,就是把这份账本翻开。 本篇接第二篇(事务属性已解析、调用已进链),覆盖第五部分:TransactionManager 的 ThreadLocal 账本、七种传播行为、内层属性的静默忽略、提交与回滚的完成逻辑、事务生命周期回调。口径同导读:Spring...
@Transactional 为什么会失效(二):代理的生成与调用进链
引子:代理明明在,方法上却没有事务 上一篇结束时,容器里备好了三样东西:自动代理创建器、事务 Advisor、TransactionInterceptor。但”备好”不等于”用上”,它们得先落到具体 Bean 上,才可能真的拦到一次调用。 于是有了本篇特有的两类困惑:有的 Bean 被包了一层,有的没有,判断标准是什么?更隐蔽的是 AopUtils.isAopProxy(bean) 返回 true,@Transactional 却没生效——代理在,只是这个方法上一条通知都没有。 本篇接第一篇,覆盖第三、四部分:wrapIfNecessary 的三道闸门、pointcut 如何判断”方法上有没有事务属性”、JDK 与 CGLIB 决定注解写在哪才有效、调用怎么进链。口径同导读:Spring Framework 6.1.8 + Spring Boot 3.3.x(Java 17)。 本篇要回答的问题: 代理在哪一步生成?为什么 @PostConstruct 里的 this 不是代理? 事务的”切点表达式”为什么只有两行逻辑?注解写在接口和实现类上,结果为什么不一样? 代理在、...
@Transactional 为什么会失效(一):从注解到 Advisor 的装配
引子:注解挂上去了,容器里到底多出了什么 导读把四个阶段铺在了一张图上,这一篇只走第一步:装配。在任何业务 Bean 被创建之前,容器必须先准备好三件事——由谁来判断”这个 Bean 要不要包代理”、包上之后执行什么、以及怎么判断某个方法要不要被拦。 日常卡住的地方,恰恰都在这三件事上:老文章说要加 @EnableTransactionManagement,可从没加过它,事务照样能用;@Aspect 切面和事务写在同一个项目里,说不清它们是不是各有一套代理创建器;读到”protected 方法上的 @Transactional 不生效”,自己试了一下居然回滚了。这些疑问的答案都不在运行期,而在启动时多出来的那几个 Bean 里。 本篇接系列导读那张四阶段图,覆盖第一、二部分:事务的三个组件分别对应 AOP 的哪个概念、@EnableTransactionManagement 导入的两个类各做什么、AutoProxyRegistrar 与 ProxyTransactionManagementConfiguration 注册了哪些 Bean,以及 Boot 自动配置为什么默认用 C...
@Transactional 为什么会失效?从 AOP 代理链看 Spring 事务机制
这是一个四篇的系列。本篇是导读:先把 AOP 的四个阶段和五类失效点铺在一张图上,再按篇给出入口。后续四篇的节号连续(第一~第八部分),跨篇引用”第七部分”时,翻到对应那一篇就能对上。 引子:注解明明在那,为什么没回滚 一个再普通不过的下午:订单服务要”扣库存 + 落订单”。 123456789101112131415161718@Servicepublic class OrderService { @Autowired private StockService stockService; // 没有 @Transactional public void createOrder(OrderDTO dto) { stockService.deduct(dto.getSkuId(), dto.getCount()); this.saveOrder(dto); // 同类内部调用 } @Transactional public void saveOrder(OrderD...
从 Spring Boot 到 Spring AI:Agent 开发到底多了什么
引子:一个只写过 Controller 的人,第一次听到 Agent 你已经能把 Spring Boot 用得很熟:@RestController 接请求,@Service 装业务,@Transactional 管事务,加一个 starter、写几行 application.yml,一个新功能就能上线。 现在需求变了。要做”一个能自己查订单、查库存、必要时还能给客户发邮件的智能助手”。 然后你打开搜索框,扑面而来的是这些词:Agent、ReAct、MCP、RAG、Function Calling、Tool、Memory、LangChain。每一个词都指向一套你没见过的东西。 你的第一反应是对的:调用大模型,不就是 POST 一个 JSON 到某个 HTTP 接口吗?RestClient 能不能干?能,二十行代码就能跑通。 但这个反应只对了一半。调用模型确实是发 HTTP 请求,可让模型”干活”是一整套新的运行时结构。 那些陌生名词不是营销词,它们各自对应一个真实的工程问题:模型没有记忆怎么办、模型不能执行代码怎么办、工具散落在十个应用里怎么复用、多步任务怎么收敛。 这篇文章沿着这...
一次下载失败排查:被 Filter 吞掉的异步响应流
下载接口的响应头一切正常:状态码 200、Content-Disposition: attachment; filename=big.zip、Content-Type 也对。唯独文件是 0 字节。 更让人摸不着头脑的是它不稳定——偶尔又能下下来一小段,前几 KB 是完整的,后面戛然而止。日志里干干净净,没有任何异常堆栈。 把项目里那个统一的日志 Filter 注释掉,一切立刻恢复正常。 状态码和响应头都写对了,为什么一个字节都没发出去? StreamingResponseBody 拿到的那个 OutputStream,到底是谁的? 为什么它时好时坏,而不是稳定失败? Filter 里判断一下 DispatcherType.ASYNC,能不能救回来? 正确写法是什么?Spring 自己是怎么处理这个问题的? 这篇文章按排查的顺序走一遍:先定位到嫌疑对象,再从源码把时间线还原出来,最后给出几种修法。如果你对 StreamingResponseBody 的工作方式还不太熟,建议先看 Spring 文件下载全方案深度解析的 3.4 节,以及 Spring MVC 中那些“不走寻常...
深入 WebFlux 与 Spring Cloud Gateway:为什么网关没有 Controller,却是一个 WebFlux 应用?
一个真实的困惑 你第一次翻开 Spring Cloud Gateway 的源码,或者试着往里加一个自定义逻辑时,大概率会遇到这样一个瞬间: 我知道 Spring WebFlux 是用来写 @Controller 的响应式 Web 框架,可我翻遍了 Gateway 的代码,一个 Controller 都没找到。那它到底是怎么处理请求的?为什么所有人还说它“基于 WebFlux”? 这个困惑不是你的问题,而是“WebFlux”这个词被用得太窄了。 大多数人第一次接触 WebFlux,都是从 @Controller 开始的,于是很自然地把它当成了 WebFlux 的代名词。但真相是:@Controller 只是 WebFlux 最上面的一层皮。Gateway 剥掉了这层皮,把下面的骨架完整地拿过来用了。 这篇文章就沿着“一个请求进来之后到底发生了什么”这条线,把 WebFlux 的骨架拆开,再把 Gateway 套进去,看看它是怎么复用的。 一、从你熟悉的地方出发:Servlet 栈 在进入 WebFlux 之前,先回到你熟悉的 Spring MVC。 一个 HTTP 请求打进来,...