知其然,知其所以然
Spring 全家桶 2026-10-19

文件上传:multipart 解析与大小限制

实测 Spring MVC 文件上传:getOriginalFilename() 原样返回 ../../etc/passwd;默认单个文件 1MB(正好 1MB 能过,多 1 字节 413)、整个请求 10MB(超了客户端发送中被断开);413 响应体为空且不经过控制器的 @ExceptionHandler;部分个数默认上限 50(51 个文本字段就 413),由 server.tomcat.max-part-count 控制;关掉 multipart 后上传接口全部 400。

Spring MVC 文件上传 multipart MultipartFile Tomcat 真实验证 动画演示
Spring 全家桶 2026-10-19

异步请求:Callable 与 DeferredResult

实测 Spring MVC 异步请求:返回 Callable 时 Tomcat 线程立刻释放,Callable 跑在 task 线程,完成后以 ASYNC 重新派发,拦截器 preHandle 被调用两次;DeferredResult 可以由任意线程完成;超时的实际等待时间对不上设定值(设 100 ms 的有时要等将近 1 秒),超时的 Callable 会被中断;Tomcat 只有 2 个线程时,10 个同步慢请求把别的请求挡住 1161 ms,Callable 只挡 1 ms。

Spring MVC 异步请求 Callable DeferredResult Tomcat 真实验证 动画演示
Spring 全家桶 2026-10-19

SSE 与流式响应

实测 Spring MVC 的 SSE 与流式响应:SseEmitter 的线格式(id、event、data、retry、注释心跳)和逐事件到达;StreamingResponseBody 里的 flush() 被 nonFlushing 包装而无效,小块数据要到请求结束才一起到,块足够大才逐块到达;客户端断开后服务端要等下一次写失败才发现(多发 2 个事件),超时不精确;开启 gzip 压缩后事件仍逐块到达。

Spring MVC SSE SseEmitter StreamingResponseBody 流式响应 真实验证 动画演示
Spring 全家桶 2026-10-19

CORS:预检请求与配置的三个位置

实测 Spring MVC 的 CORS:没有配置时服务端照常返回 200,拦截的是浏览器;@CrossOrigin 不在白名单的来源得到 403 Invalid CORS request;预检默认 Max-Age 1800,实际请求只带 Origin、Expose、Credentials 头;全局 addCorsMappings 与 allowedOriginPatterns 回显来源;异常的 500 在有匹配配置时仍带 CORS 头,没有匹配配置的路径(含 404)不带;CorsFilter 能覆盖 404;allowedOrigins("*") 加 allowCredentials(true) 启动不报错、请求时 500。

Spring MVC CORS CrossOrigin 预检请求 CorsFilter 真实验证 动画演示
Spring 全家桶 2026-10-19

静态资源与缓存头:ResourceHandler 与版本化

实测 Spring Boot 的静态资源:默认只有 Last-Modified 和 Accept-Ranges,没有 Cache-Control 和 ETag;If-Modified-Since 得 304,Range 得 206/416;cache.period 与 cachecontrol.* 对所有资源同一个值;内容哈希版本化让 /app.js 变成 /app-<md5>.js 并加 ETag,但开了长缓存之后没有哈希的原路径也被缓存一年;固定版本是路径前缀,ETag 都是 W/"v7";add-mappings=false 关掉静态资源与欢迎页。

Spring MVC 静态资源 缓存 ResourceHandler 版本化 真实验证 动画演示
Spring 全家桶 2026-10-19

RestClient、RestTemplate 与 HTTP Interface:连接池与超时

实测 Spring Boot 4 的 HTTP 客户端:只有 starter-restclient 时 Boot 用 JDK HttpClient,new RestTemplate() 用 HttpURLConnection;三者顺序 20 次都只建 1 个连接、并发 20 个线程各建 1 个连接,没有默认上限;默认没有读超时和连接超时(连不可达地址挂 20 秒以上),旧属性名 spring.http.client.read-timeout 静默无效,要用 spring.http.clients.*;4xx/5xx 默认抛异常,HTTP Interface 返回 ResponseEntity 也一样抛;加上 Apache HttpClient 后默认每个路由只有 5 个连接。

Spring RestClient RestTemplate HTTP Interface 连接池 超时 真实验证 动画演示
Spring 全家桶 2026-10-19

请求日志与 MDC:每个请求带 trace id

实测 Spring MVC 里的请求日志与 MDC:Filter 放进 MDC 的 traceId 让 controller、service 的日志自动带上;换线程后 @Async、Callable、CompletableFuture、new Thread 的 MDC 都是空,ASYNC 重新派发后的拦截器日志也空;TaskDecorator Bean 只补上 Spring 自己的执行器(@Async、Callable);Filter 不在 finally 里清 MDC 会让下一个请求继承上一个的 id;CommonsRequestLoggingFilter 的 payload 只在 After request 才有且被截断,includeHeaders 会把 Authorization 和 Cookie 明文写进日志。

Spring MVC 日志 MDC traceId CommonsRequestLoggingFilter 真实验证 动画演示
Spring 全家桶 2026-10-19

Tomcat 线程池满了会怎样:max-threads、accept-count 与 max-connections

实测 Spring Boot 内嵌 Tomcat 线程池满了会怎样:默认 threads.max=200、max-connections=8192、accept-count=100;threads.max=2 时请求成对排队、不报错,一个几毫秒的 /fast 被拖到 3770 ms;accept-count 很小时,多余的连接在 1 ms 内被 ConnectionResetError(macOS 本机回环,成功个数每轮波动);connection-timeout 不会踢掉排队等线程的请求。

Spring Tomcat 线程池 max-threads accept-count max-connections 真实验证 动画演示
Spring 全家桶 2026-10-19

Reactor 的惰性:没有订阅就什么都不发生

实测 Reactor 的惰性:fromSupplier / map / doOnNext 在订阅前不执行,但 Mono.just(expr) 的参数在构造时就算了;每次订阅重新执行源,cache() 与 share() 才共用;在 map 或 doOnNext 里调用返回 Mono 的方法并丢掉返回值,等于没有执行;热源按订阅时机决定收到什么(onBackpressureBuffer 缓存、directBestEffort 丢弃);CompletableFuture 包成 Mono 也不会变惰性;没有 error 回调的 subscribe() 会把错误吞到日志里。

Spring Reactor 响应式 惰性 订阅 Mono Flux 真实验证 动画演示
Spring 全家桶 2026-10-19

背压:request(n)、limitRate 与缓冲区溢出

实测 Reactor 背压:Flux.interval 下游跟不上就 OverflowException(处理了 4 个元素);五种溢出策略的取舍(无界缓冲不丢、DROP_OLDEST 收到 116 个并丢弃 4884 个、Drop 只收到 16 个);publishOn 默认预取 256 并按 192 补充、concatMap 每次只 request(1);无界缓冲让 20 万个元素堆在内存,而有背压时生产只领先消费几个;Sinks 缓冲满了 tryEmitNext 返回 FAIL_OVERFLOW,不检查就静默丢失。

Spring Reactor 背压 request limitRate onBackpressureBuffer 真实验证 动画演示
Spring 全家桶 2026-10-19

publishOn 与 subscribeOn:线程在哪里切换

实测 Reactor 的线程切换:subscribeOn 决定订阅与源所在线程且只有最靠近源的生效,publishOn 决定它之后的线程且可以多次切换;Mono.delay 之后在 parallel 线程,fromFuture 之后在完成它的线程;阻塞的源不加 subscribeOn 会卡住调用线程(subscribe() 用了 310 ms),加了之后 2 ms 返回;flatMap 里内层加 subscribeOn 才并行(1235 ms 降到 304 ms);parallel 线程数等于核数,阻塞操作该去 boundedElastic。

Spring Reactor publishOn subscribeOn Schedulers 线程模型 真实验证 动画演示
Spring 全家桶 2026-10-19

WebFlux 线程模型:event loop 上做阻塞调用的后果

实测 WebFlux 的线程模型:Reactor Netty 默认 event loop 线程数等于逻辑核心数(10),连接轮流分配;把线程数设成 2 之后,在 event loop 上 Thread.sleep(无论 Mono.fromCallable 里还是控制器直接 sleep)会让别的请求(包括 /fast)等到 1022 ms,subscribeOn(boundedElastic) 则只有 9 ms;event loop 线程上 block() 会被 Reactor 直接拒绝(500);非阻塞等待让 2 个线程撑住 200 个并发请求。

Spring WebFlux Reactor Netty event loop 阻塞 线程模型 真实验证 动画演示
京ICP备2026067502号-1京公网安备11010502063487号