Web 请求链路:分发、异常与异步重调度
Controller没进却返回400,afterCompletion里又没有异常;Callable已完成却还未写回响应。这些现象要按调度边界解释。固定基线 Framework 6.1.12 / Boot 3.3.3 / Servlet 6.0。迁入Web与上下文主题,本页主讲请求生命周期;线程上下文、身份与授权的完整训练见异步与安全独立页,网络重试和 TLS 见 HTTP/TLS。不要把三个边界合成“Controller 调 Service”。
从容器过滤链到 MVC
HTTP 字节由容器解析成 Servlet 请求,进入映射的 Filter 链,再进入 DispatcherServlet。Filter 位于 Servlet 边界,可用于编码、关联 ID、安全代理和请求包装,是否覆盖某次调用还取决于 URL、Servlet 映射与 dispatcher type。HandlerInterceptor 位于 HandlerExecutionChain 内,围绕 MVC 处理器,不是通用网络过滤器,也不能假设覆盖所有非 MVC 路由。
容器解析请求 -> Filter 链 -> DispatcherServlet
-> HandlerMapping:找到 HandlerExecutionChain
-> Interceptor.preHandle
-> HandlerAdapter:参数解析/数据绑定/校验/方法调用
-> 返回值处理器:转换内容或形成视图
-> Interceptor.postHandle(正常同步路径)
-> 异常解析/视图渲染 -> Interceptor.afterCompletion
-> Filter 链返回HandlerMapping 找“谁处理”,HandlerAdapter 决定“怎样调用”。同一 Servlet 可以支持不同 handler 类型,不能把它们都理解为 Controller 接口。RequestMappingHandlerMapping 为注解方法建立映射,RequestMappingHandlerAdapter 负责调用 HandlerMethod;这正是策略与适配器的实际用途,不需要人为再加一套同名设计模式层。
DispatcherServlet.doDispatch 是固定入口。它先处理 multipart、查 handler、选 adapter、检查相关条件,再 preHandle 和 handle;处理完结果后触发完成回调。找不到 handler、preHandle 返回 false、adapter 抛异常、异步开始,各自有不同分支,不能假设每个请求都完整执行上述每一个阶段。
原码:OncePerRequestFilter.java,v6.1.12,连续89–133行。固定来源。
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
if (!((request instanceof HttpServletRequest httpRequest) && (response instanceof HttpServletResponse httpResponse))) {
throw new ServletException("OncePerRequestFilter only supports HTTP requests");
}
String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;
if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else if (hasAlreadyFilteredAttribute) {
if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
return;
}
// Proceed without invoking this filter...
filterChain.doFilter(request, response);
}
else {
// Do invoke this filter...
request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpRequest, httpResponse, filterChain);
}
finally {
// Remove the "already filtered" request attribute for this request.
request.removeAttribute(alreadyFilteredAttributeName);
}
}
}
private boolean skipDispatch(HttpServletRequest request) {
if (isAsyncDispatch(request) && shouldNotFilterAsyncDispatch()) {
return true;
}
if (request.getAttribute(WebUtils.ERROR_REQUEST_URI_ATTRIBUTE) != null && shouldNotFilterErrorDispatch()) {
return true;
}
return false;
}OncePerRequestFilter的“once”有条件:容器映射先决定是否进入,再由skipDispatch处理ASYNC/ERROR,当前线程的alreadyFiltered标记与嵌套ERROR还各有分支。finally移除标记不等于整个逻辑请求永不重入;默认跳过ASYNC/ERROR也不代表所有自定义Filter都这样配置。不得把MockMvc显式addFilters的执行次数外推为部署映射。
参数、校验与返回值实际发生在哪里
RequestMappingHandlerAdapter.invokeHandlerMethod 准备 WebDataBinderFactory、ModelFactory、ServletInvocableHandlerMethod,设置参数解析器、返回值处理器、方法校验器等,再 invokeAndHandle。参数并不是先在网关统一变成业务 DTO,而是按方法签名选择 HandlerMethodArgumentResolver:路径、查询、请求体、认证主体等来自不同来源。
RequestBody 借助 HttpMessageConverter 读取内容,反序列化错误可能在进入 Controller 前就发生。类型转换与 Bean Validation 也不等同于业务约束:金额为正数是输入约束,余额足够和订单未关闭是业务不变量;用户是否有权操作这个订单是授权。DTO 成功绑定只能证明输入能被解析,不能证明能执行业务。
数据绑定应限制可写字段。直接让外部请求绑定持久实体的所有属性,会把内部 ownerId、状态、权限字段暴露为可写入口;即使用校验注解,也未必禁止这些赋值。应使用操作专属 DTO,明确允许变化的字段,在服务层从可信主体和持久状态导出敏感字段。租户 ID 不应只从可伪造请求体取值。
返回值处理也影响拦截器:ResponseBody/ResponseEntity 可能在 adapter 内就由消息转换器写响应,而 postHandle 是 adapter 返回后才执行,因此 postHandle 不适合统一修改已经序列化的 JSON。需要调整响应结构应使用返回值层扩展或 ResponseBodyAdvice,并考虑二进制流、下载、流式输出和错误响应;不能强行给每个响应套一个 JSON 包装。
具体调用见 RequestMappingHandlerAdapter 与 ServletInvocableHandlerMethod。定位“Controller 第一行没执行”的错误,要先看解析器、转换器、校验和 preHandle,而不是只给业务方法打断点。
Interceptor 的短路、顺序与异常
多个拦截器 preHandle 按登记顺序进入;成功执行的拦截器完成回调按相反方向退出。若某个 preHandle 返回 false,只对此前成功进入的拦截器触发对应清理;返回 false 的拦截器自己取得的资源应由它负责。afterCompletion 不是所有注册拦截器无条件收到一次的广播。
doDispatch 捕获处理过程异常后交给 processDispatchResult,HandlerExceptionResolver 可把异常变成响应或视图。ExceptionHandlerExceptionResolver 支持控制器异常处理,另有状态异常及默认 MVC 异常解析等机制。异常被解析后,afterCompletion 收到的 exception 可能为 null,因此不能仅用该参数统计“业务没有异常”。需要结合状态码、业务错误码和观测事件。
Filter 中、进入 DispatcherServlet 之前发生的异常,不会天然进入 ControllerAdvice;需要过滤链或容器错误调度协议处理。响应已提交后再抛异常,也无法可靠改写为一份新的完整 JSON。错误处理要保留原始原因并避免再次写响应导致混杂。安全认证失败与业务异常应在各自边界处理,不把“统一异常处理”当作跨所有层的魔法。
示例推演:F1→F2→MVC,I1.pre=true、I2.pre=true,Controller 抛异常,resolver 写 400 并返回已处理。预期两个 postHandle 不走,I2/I1 完成回调走,Filter finally 仍执行。若 I2.pre=false,则 Controller 不执行,I1 完成回调走,I2 的资源清理由它自己确保。实现见 HandlerExecutionChain。
原码:HandlerExecutionChain.java,v6.1.12,连续143–201行。固定来源。
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}
/**
* Apply postHandle methods of registered interceptors.
*/
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
interceptor.postHandle(request, response, this.handler, mv);
}
}
/**
* Trigger afterCompletion callbacks on the mapped HandlerInterceptors.
* Will just invoke afterCompletion for all interceptors whose preHandle invocation
* has successfully completed and returned true.
*/
void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}
/**
* Apply afterConcurrentHandlerStarted callback on mapped AsyncHandlerInterceptors.
*/
void applyAfterConcurrentHandlingStarted(HttpServletRequest request, HttpServletResponse response) {
for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
if (interceptor instanceof AsyncHandlerInterceptor asyncInterceptor) {
try {
asyncInterceptor.afterConcurrentHandlingStarted(request, response, this.handler);
}
catch (Throwable ex) {
if (logger.isErrorEnabled()) {
logger.error("Interceptor [" + interceptor + "] failed in afterConcurrentHandlingStarted", ex);
}
}
}
}
}preHandle成功且返回true才更新成功下标;false只清理此前成功者,不包含拒绝者。afterCompletion逐项捕获清理异常,避免一个回调遮蔽后续清理;postHandle与异步钩子的遍历条件不同,不能当成无条件对称通知。实验让第二个拦截器拒绝,第一完成、第二不收完成、handler不执行。
原码:DispatcherServlet.java,v6.1.12,连续1148–1188行。固定来源。
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException mavDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}processDispatchResult先处理ModelAndViewDefiningException或调用resolver。resolver返回空视图可表示已处理,随后完成回调仍收到null;resolver不能处理则抛出,另走doDispatch外层失败完成路径。已经发生的业务错误不能只靠afterCompletion的exception参数计数。异步又可能提前返回,不保证立刻触发所有完成回调。
Servlet 异步:一次请求不再只占一条调用栈
同步请求在容器线程上执行到完成。返回 Callable 时,MVC 启动 Servlet async,把任务提交到配置的 AsyncTaskExecutor,初始容器线程退出;任务产生结果后,通过 async dispatch 再次进入 Servlet,由新的调度处理结果。DeferredResult 则由应用在将来提供结果,不意味着 MVC 替应用创建一个执行该业务的线程。
关键状态是“请求仍挂起”与“当前容器线程已释放”分离。异步只释放初始请求线程,任务线程可能仍阻塞 JDBC 或 HTTP,数据库连接仍占用,挂起响应也占内存与容器资源。把阻塞 I/O 装到 Callable 并不等于非阻塞 I/O,更不意味着可以无限接收请求。
在 DispatcherServlet 中,adapter 返回后若 isConcurrentHandlingStarted 为 true,会提前离开,跳过本轮正常 postHandle;finally 调 applyAfterConcurrentHandlingStarted。异步结果重新 dispatch 后才继续对应结果处理与完成。AsyncHandlerInterceptor 可在初始线程释放前清理线程局部状态,但自定义 Filter 的 ASYNC 映射和 OncePerRequestFilter 策略也要核实;“整个请求只经过一次过滤器”不是通用保证。
// 基于 doDispatch 的教学伪代码,展示异步分支
ModelAndView result = adapter.handle(request, response, handler);
if (asyncManager.isConcurrentHandlingStarted()) {
return; // 初始 dispatch 不走普通 postHandle
}
chain.applyPostHandle(request, response, result);
// finally:若已异步,调用 afterConcurrentHandlingStarted原码:异步启动后本次dispatch提前返回
原码连续摘录(仅统一公共缩进,截取方法局部):DispatcherServlet.java,v6.1.12,L1084–L1096。
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);preHandle返回false会直接短路;ha.handle调用处理器后,如果异步处理已开始就return,不执行本轮后面的默认视图及postHandle。异步结果回来后的重新分派才继续另一轮流程。不能用同步请求的拦截器顺序解释所有异步阶段,返回线程也不说明业务任务已经完成。
WebAsyncManager:结果、超时与竞争
WebAsyncManager.startCallableProcessing 从 NOT_STARTED CAS 到 ASYNC_PROCESSING,登记超时、错误、完成回调,调用 startAsyncProcessing,然后提交 Callable。任务捕获结果或异常,最后 setConcurrentResultAndDispatch。DeferredResult 也接入同样的结果调度协议,但生产者由应用控制。
结果提交不是所有线程都能各写一次。setConcurrentResultAndDispatch 在协调区中尝试 ASYNC_PROCESSING→RESULT_SET;失败表示结果已被设置或状态已变化,迟到结果被忽略;若请求已完成也不再 dispatch。WebAsyncManager 给出状态与分支。
这个 CAS 只保证框架接受的结果竞争,不撤销迟到任务的外部副作用。输入:超时 100ms,任务 300ms 后写数据库。100ms 时返回超时并不证明 300ms 的写入被取消。中断也需要下游和任务合作,JDBC/网络取消语义需实测。因此超时响应、任务取消、事务回滚是不同事件,必须给业务一个可查询状态或幂等协议。
重新 dispatch 时 RequestMappingHandlerAdapter 检查 hasConcurrentResult,把结果包装为可处理的 invocable method;若结果是异常,会进入对应异常处理链。Controller 方法本体并不应被业务设计为每次 async dispatch 都重新执行,否则可能重复副作用。日志应同时记录逻辑 requestId、dispatcher type、线程与尝试号,既能关联整个请求,又能分辨多次调度。
原码:WebAsyncManager.java,v6.1.12,连续385–414行。固定来源。
private void setConcurrentResultAndDispatch(@Nullable Object result) {
Assert.state(this.asyncWebRequest != null, "AsyncWebRequest must not be null");
synchronized (WebAsyncManager.this) {
if (!this.state.compareAndSet(State.ASYNC_PROCESSING, State.RESULT_SET)) {
if (logger.isDebugEnabled()) {
logger.debug("Async result already set: " +
"[" + this.state.get() + "], ignored result: " + result +
" for " + formatUri(this.asyncWebRequest));
}
return;
}
this.concurrentResult = result;
if (logger.isDebugEnabled()) {
logger.debug("Async result set to: " + result + " for " + formatUri(this.asyncWebRequest));
}
if (this.asyncWebRequest.isAsyncComplete()) {
if (logger.isDebugEnabled()) {
logger.debug("Async request already completed for " + formatUri(this.asyncWebRequest));
}
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Performing async dispatch for " + formatUri(this.asyncWebRequest));
}
this.asyncWebRequest.dispatch();
}
}CAS失败会忽略第二份结果,协调的是请求结果状态;request已完成则不再dispatch。这个同步区没有回滚业务SQL或杀掉任意生产者。框架对Callable超时可尝试取消Future,但中断是否终止工作仍看任务/驱动协作;DeferredResult的生产任务由应用持有。
请求上下文的安装与恢复
原码:FrameworkServlet.java,v6.1.12,连续996–1033行。固定来源。
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new ServletException("Request processing failed: " + ex, ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}FrameworkServlet保存原LocaleContext和RequestAttributes,安装本次值,finally恢复previous并结束本次request attributes。清理不是永远remove:嵌套调用需要恢复外层。requestCompleted后的旧包装不再提供有效request-scope访问,不能交后台长期保存。Callable还有RequestBindingInterceptor参与工作线程绑定;普通@Async/自建池不自动复用这套Servlet回调,见异步正文。
可运行验证与超时纸面实验
从仓库根运行 JAVA_HOME=$(/usr/libexec/java_home -v17) bash labs/spring-senior/run.sh web。真实MockMvc调用DispatcherServlet和HandlerAdapter,Servlet请求/响应与asyncDispatch由测试边界提供,没有真实监听端口、网络断连或容器定时器。固定池1线程、队列2;结果等待和关闭等待均3秒。
| 输入 | 可检查预期 |
|---|---|
| /sync | 200/ok;Filter进入→pre:REQUEST→handler→post→complete:null→Filter退出 |
| /fail | 精确IllegalStateException(synthetic)由MVC解析,400/bad,无post,完成参数null |
| /async | 初始handler一次、concurrent、Filter退出;任务线程不同;重调度pre:ASYNC→post→complete,200/async-ok |
| /async-fail | 结果作为异常再分派,400/bad,无post;不是重新执行Controller业务 |
| /short | 第二拦截器拒绝418,第一收到完成,拒绝者不收完成,handler未执行 |
| /filter-fail | 指定IllegalArgumentException(filter failure)发生在MVC前,不进入Advice,Filter finally执行 |
本批上述输入已实跑通过;实际源码如下,不复制另一份片段代替实验。
package study.spring;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.*;
import org.springframework.test.web.servlet.*;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
public class WebLab {
static final List<String> events=new ArrayList<>();
static final AtomicLong worker=new AtomicLong();
@RestController public static class Controller {
@GetMapping({"/sync","/short"}) public String sync(){events.add("handler");return "ok";}
@GetMapping("/fail") public String fail(){events.add("handler");throw new IllegalStateException("synthetic");}
@GetMapping("/async") public Callable<String> async(){events.add("handler");return ()->{worker.set(Thread.currentThread().getId());return "async-ok";};}
@GetMapping("/async-fail") public Callable<String> asyncFail(){events.add("handler");return ()->{throw new IllegalStateException("synthetic");};}
@ExceptionHandler(IllegalStateException.class) @ResponseStatus(org.springframework.http.HttpStatus.BAD_REQUEST)
public String error(){return "bad";}
}
static void equal(Object actual,Object expected){if(!Objects.equals(actual,expected))throw new AssertionError(actual+" != "+expected);}
public static void main(String[] args)throws Exception{
var pool=new org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor();
pool.setCorePoolSize(1);pool.setMaxPoolSize(1);pool.setQueueCapacity(2);pool.initialize();
try {
var first=new AsyncHandlerInterceptor(){
public boolean preHandle(jakarta.servlet.http.HttpServletRequest r,jakarta.servlet.http.HttpServletResponse s,Object h){events.add("pre:"+r.getDispatcherType());return true;}
public void postHandle(jakarta.servlet.http.HttpServletRequest r,jakarta.servlet.http.HttpServletResponse s,Object h,ModelAndView m){events.add("post");}
public void afterCompletion(jakarta.servlet.http.HttpServletRequest r,jakarta.servlet.http.HttpServletResponse s,Object h,Exception e){events.add("complete:"+(e==null?"null":e.getClass().getSimpleName()));}
public void afterConcurrentHandlingStarted(jakarta.servlet.http.HttpServletRequest r,jakarta.servlet.http.HttpServletResponse s,Object h){events.add("concurrent");}
};
var second=new HandlerInterceptor(){
public boolean preHandle(jakarta.servlet.http.HttpServletRequest r,jakarta.servlet.http.HttpServletResponse s,Object h)throws Exception {
if(!r.getRequestURI().equals("/short"))return true;
events.add("deny");
s.setStatus(418);return false;
}
public void afterCompletion(jakarta.servlet.http.HttpServletRequest r,jakarta.servlet.http.HttpServletResponse s,Object h,Exception e){if(r.getRequestURI().equals("/short"))events.add("deny-complete");}
};
jakarta.servlet.Filter filter=(request,response,chain)->{
events.add("filter+");
try {
if(((jakarta.servlet.http.HttpServletRequest)request).getRequestURI().equals("/filter-fail"))throw new IllegalArgumentException("filter failure");
chain.doFilter(request,response);
} finally {events.add("filter-");}
};
MockMvc mvc=MockMvcBuilders.standaloneSetup(new Controller()).addInterceptors(first,second)
.setAsyncRequestTimeout(3000).addFilters(filter).build();
mvc.getDispatcherServlet().getWebApplicationContext()
.getBeansOfType(org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.class)
.values().forEach(adapter->adapter.setTaskExecutor(pool));
// MockMvc supplies mock Servlet boundaries; no network container is started.
var sync=mvc.perform(MockMvcRequestBuilders.get("/sync")).andReturn();
equal(sync.getResponse().getStatus(),200);equal(sync.getResponse().getContentAsString(),"ok");
equal(events,List.of("filter+","pre:REQUEST","handler","post","complete:null","filter-"));
events.clear();var fail=mvc.perform(MockMvcRequestBuilders.get("/fail")).andReturn();
equal(fail.getResponse().getStatus(),400);equal(fail.getResponse().getContentAsString(),"bad");
if(!(fail.getResolvedException() instanceof IllegalStateException)||!"synthetic".equals(fail.getResolvedException().getMessage()))throw new AssertionError("resolver");
equal(events,List.of("filter+","pre:REQUEST","handler","complete:null","filter-"));
for(String path:List.of("/async","/async-fail")) {
events.clear();var async=mvc.perform(MockMvcRequestBuilders.get(path)).andReturn();
if(!async.getRequest().isAsyncStarted())throw new AssertionError("not async");
async.getAsyncResult(3000);
equal(events,List.of("filter+","pre:REQUEST","handler","concurrent","filter-"));
var result=mvc.perform(MockMvcRequestBuilders.asyncDispatch(async)).andReturn();
boolean error=path.endsWith("fail");equal(result.getResponse().getStatus(),error?400:200);
equal(result.getResponse().getContentAsString(),error?"bad":"async-ok");
equal(events,error?List.of("filter+","pre:REQUEST","handler","concurrent","filter-","filter+","pre:ASYNC","complete:null","filter-"):
List.of("filter+","pre:REQUEST","handler","concurrent","filter-","filter+","pre:ASYNC","post","complete:null","filter-"));
if(error&&!(result.getResolvedException() instanceof IllegalStateException))throw new AssertionError("async resolver");
}
if(worker.get()==Thread.currentThread().getId()||worker.get()==0)throw new AssertionError("no worker switch");
events.clear();var denied=mvc.perform(MockMvcRequestBuilders.get("/short")).andReturn();
equal(denied.getResponse().getStatus(),418);
equal(events,List.of("filter+","pre:REQUEST","deny","complete:null","filter-"));
events.clear();try {mvc.perform(MockMvcRequestBuilders.get("/filter-fail"));throw new AssertionError("missing filter failure");}
catch(Exception ex){Throwable cause=ex;while(cause.getCause()!=null)cause=cause.getCause();if(!(cause instanceof IllegalArgumentException)||!"filter failure".equals(cause.getMessage()))throw ex;}
equal(events,List.of("filter+","filter-"));
System.out.println("PASS MockMvc sync/error/async redispatch/async-error/short/filter-failure");
} finally {pool.shutdown();if(!pool.getThreadPoolExecutor().awaitTermination(3,java.util.concurrent.TimeUnit.SECONDS))throw new AssertionError("pool leaked");}
}
}纸面故障实验输入:到达率 600 req/s,业务平均阻塞 200ms,异步池 40 线程、队列 80,HTTP deadline 1s。在无其他瓶颈的稳态近似下需要并行工作约 600×0.2=120;40 线程最多约 200 req/s,队列会持续增长,80 队列无法吸收无限超载。改成异步只把等待转移到另一个池,答案必须是限制接入、扩容或减少工作,而不是单纯增加队列。
再输入一个异步超时竞争:任务完成与超时回调几乎同时提交结果。答案是框架状态只接受一个结果,但任务副作用仍需业务防重。应验证响应次数、最终数据库状态、任务是否退出和资源是否归还;只看客户端收到一次超时无法证明服务已停止工作。
高级答案、三轮追问与架构变化
高级口述:“Servlet Filter 先决定入口处理,DispatcherServlet 找 handler 和 adapter,adapter 完成参数、校验、调用和返回值处理,拦截器及异常解析位于 MVC 调度内。异步把一个请求拆为初始调度、任务执行、结果重调度,超时竞争由状态协调,但不能取消所有业务效果。”
第一轮问“ControllerAdvice 为什么没处理 Filter 异常”,答异常发生在 MVC 处理域之外。第二轮问“postHandle 为什么没改到 JSON”,答序列化可能已在 adapter 内完成。第三轮问“异步超时后为何仍扣款”,答响应生命周期与任务事务不同,取消不保证副作用消失。每轮 2 分,能给阶段图与查库验证各加 2 分。
架构变化一:报表请求从 100ms 升为 30s。长连接同步等待会占用请求上下文和异步任务资源;候选是有限异步池加短 deadline,适合少量短峰值;另一个候选是提交后台作业返回作业 ID,查询/推送状态,代价是持久状态、授权、取消与结果存储。若作业必须在进程重启后继续,单纯 DeferredResult 不够,必须持久化任务及恢复责任。
变化二:需要流式下载百万行。一次转换全部 JSON 可能堆积内存,流式降低常驻内存却延长连接持有与断连清理。应分页/游标并设资源上限,避免在不可控客户端读取速度下长期持有数据库事务;可以先生成对象存储文件再下载。验证关注慢客户端、断开、写失败、内存峰值、连接占用及授权有效期,不只测本机快速下载。
变化三:多租户端点统一包装。不要让 MVC 通用拦截器成为唯一授权点;HTTP 入口、服务操作和带租户条件的数据访问需要一致协作,异步重调度与内部调用也需守住边界。详细认证授权过滤链见安全正文,线程快照模型见异步与上下文。可选专家继续研究容器 async state machine,主线先掌握请求状态、线程、资源和业务效果四条时间线。
闭卷验收
本页 8 道闭卷题收进了下方自测面板:先回忆,再展开参考答案与评分点,最后给三档自评。