概述
衔接前文,在《Spring Web 深度解析》系列的第 2 至 5 篇中,我们详细拆解了 Spring MVC 处理正常请求的完整链路,从 HandlerMapping 定位到 HandlerAdapter 执行,再到参数解析、消息转换以及拦截器、过滤器与 Spring Security 的多层协作。这些机制共同构成了一个稳固的“快乐路径”。
然而,一个健壮的 Web 应用不仅要处理好每一个正常请求,更需要在异常发生时,优雅地向客户端反馈清晰、有意义且格式规范的错误信息。当 Controller 方法抛出异常,或框架自身因种种原因(如 404、405)无法继续处理请求时,请求便会从原先的正常处理轨道,转入一个专门设计的“异常处理专线”。
本文将聚焦这条专线,系统性地剖析 Spring MVC 如何通过 HandlerExceptionResolver 链、@ExceptionHandler 注解、ErrorController 等多种机制层层拦截,最终将各种不可预知的异常,转化为可预期、结构化的 HTTP 响应。我们还将前瞻性地介绍 Spring Framework 6 / Boot 3.x 中引入的 ProblemDetail(RFC 7807)标准,揭示 Spring 异常处理体系拥抱国际标准的最新演进。
核心要点
- 四层异常处理模型:Spring MVC 构建了
@ExceptionHandler→@ControllerAdvice→HandlerExceptionResolver链 →ErrorController的四层异常处理体系,层层递进,确保没有任何异常能逃脱处理。 - Resolver 链的策略模式:
HandlerExceptionResolver及其组合实现是策略模式的典范,三种核心内置 Resolver 各司其职,共同构成了一个有序、可扩展的异常解析生态系统。 - 兜底的
BasicErrorController:作为整个体系的最后一道防线,BasicErrorController优雅地处理/error请求,并根据内容协商智能地在 HTML 错误页和 JSON 错误体之间切换。 - 错误响应与内容协商的联动:异常最终的响应形式(HTML/JSON/XML)并非固定不变,而是由 Spring MVC 核心的
HttpMessageConverter和ViewResolver两大机制,在内容协商策略下共同决定。这正是前文知识在异常处理场景下的精彩复现。 - RFC 7807 的标准化演进:
ProblemDetail的出现,标志着 Spring 生态在错误处理上从“自定义 Map”向“国际标准结构”的转型,为微服务间的错误传递提供了统一、可扩展的格式。
文章组织架构图
下图展示了本文的完整知识体系,从异常处理模型开始,逐步深入各个核心机制,最终通过事故与面试完成实践与理论的闭环。
flowchart TB
subgraph A ["1. 异常处理总览:从 Controller 到 ErrorController 的四层体系"]
A1["异常处理模型概述"]
A2["四层结构:<br/>@ExceptionHandler -> @ControllerAdvice -> Resolver链 -> ErrorController"]
A3["异常如何进入processDispatchResult"]
end
subgraph B ["2. HandlerExceptionResolver 链:策略模式驱动的异常解析"]
B1["HandlerExceptionResolver 接口"]
B2["DispatcherServlet中的调用逻辑"]
B3["三大内置实现的顺序与分工"]
B4["自定义解析器"]
end
subgraph C ["3. @ExceptionHandler 与 @ControllerAdvice"]
C1["@ExceptionHandler 方法签名与返回值处理"]
C2["@ControllerAdvice 的全局作用域与过滤"]
C3["ExceptionHandlerExceptionResolver 内部机制"]
C4["局部 vs 全局的优先级"]
end
subgraph D ["4. ErrorController 与 BasicErrorController"]
D1["ErrorController 接口"]
D2["BasicErrorController 的实现"]
D3["ErrorAttributes 如何收集错误信息"]
D4["Spring Boot 自动装配"]
end
subgraph E ["5. 错误响应与内容协商"]
E1["errorHtml: 利用ViewResolver渲染视图"]
E2["error: 利用HttpMessageConverter序列化JSON/XML"]
E3["Accept 头的决定性作用"]
E4["自定义错误响应格式"]
end
subgraph F ["6. 展望:ProblemDetail 与 RFC 7807 标准化错误"]
F1["RFC 7807 标准介绍"]
F2["ProblemDetail 核心类结构"]
F3["ErrorResponseException 体系"]
F4["与传统方式的对比与迁移价值"]
end
subgraph G ["7. 生产事故排查专题"]
G1["事故一:@ExceptionHandler 未生效"]
G2["事故二:API 返回了 HTML 错误页"]
end
subgraph H ["8. 面试高频专题"]
H1["12道面试题 + 追问"]
end
A --> B --> C --> D --> E
E --> F
D --> G
E --> G
G --> H
分层详尽的文字说明
-
总览说明:全文 8 个模块严格遵循“总览-分述-展望-实践”的认知路径。从整体模型出发,层层深入到 Resolver 链、声明式注解、兜底控制器等核心组件,再探讨错误响应与内容协商的联动,最后通过标准化演进、事故复盘和面试题,完成从理论、源码到工业实践的闭环。
-
逐模块说明:
- 异常处理总览:建立四层体系模型,宏观理解异常从抛出到被最终处理的完整流转路径。
- HandlerExceptionResolver 链:深入源码,分析在
DispatcherServlet中,如何以策略模式串联起多个HandlerExceptionResolver实现,职责分明地处理各类异常。 - @ExceptionHandler 与 @ControllerAdvice:聚焦最常用的声明式异常处理,剖析其参数解析、返回值处理(联动前文消息转换器),以及局部与全局优先级的实现原理。
- ErrorController 与 BasicErrorController:揭示最后一道防线的内部工作流程,包括错误属性收集、Spring Boot 自动化配置,以及如何响应
/error路径。 - 错误响应与内容协商:解析
BasicErrorController如何巧妙地利用ViewResolver和HttpMessageConverter,根据客户端Accept头,在 HTML 页面和 JSON 结构体之间智能分流。 - 展望 ProblemDetail:作为技术演进方向,介绍 RFC 7807 标准,分析
ProblemDetail如何取代ErrorAttributesMap,提供更规范、智能的响应。 - 生产事故排查:通过两个典型事故案例,串联前文知识,演示异常处理机制在生产环境下的排查思路与解决路径,体现其实用价值。
- 面试高频专题:提炼核心要点,以问答形式巩固关键概念,并应对高层次面试。
-
关键结论:Spring 的异常处理本质上是一套“由近及远、层层兜底”的责任链。理解其执行顺序和响应渲染机制——尤其是内容协商在异常处理中的应用——是解决“为何错误返回了 HTML 而不是 JSON”、“为何我的 @ExceptionHandler 没生效”这类生产常见问题的关键。
1. 异常处理总览:从 Controller 到 ErrorController 的四层体系
在传统的 Servlet 应用中,异常处理通常通过 web.xml 中的 <error-page> 标签或 Servlet 自带的 sendError() 方法来指定错误页面或状态码。这种方式功能单一,耦合度高,难以满足现代 RESTful 服务对 JSON、XML 等多种响应格式的需求。
Spring MVC 通过设计一个层次分明、高度可插拔的异常处理体系,极大地提升了灵活性和扩展性。它不再依赖 Servlet 容器的静态配置,而是在框架层面将异常处理逻辑内聚,并集成到了请求处理的主链路中。
1.1 四层异常处理模型
这个体系从内到外,依次由四个关键层次构成,它们共同组成了一个异常处理的“多层过滤器(Multi-Layer Filter)”:
-
Controller 级别的
@ExceptionHandler:- 范围:仅作用于定义它的那个 Controller 类内部。
- 作用:处理特定 Controller 抛出的特定类型异常。这是最细粒度的“点对点”异常处理。
- 优先级:最高。等同于在局部范围内提供了特化的异常处理逻辑。
-
全局级别的
@ControllerAdvice:- 范围:可作用于所有 Controller,或通过
basePackages、assignableTypes等属性筛选出特定范围的 Controller。 - 作用:提供跨 Controller 的全局异常处理、数据绑定和模型增强。这是最常见的全局异常处理方式。
- 优先级:次于局部
@ExceptionHandler。当局部无法处理时,由全局@ControllerAdvice接管。
- 范围:可作用于所有 Controller,或通过
-
HandlerExceptionResolver链:- 范围:作用于
DispatcherServlet这个核心前端控制器处理的所有请求。 - 作用:这是一个策略模式的接口实现链。Spring MVC 内置了三个关键的实现,按固定顺序执行,将未被上述注解捕获的异常或特定 Spring MVC 内部异常转换为
ModelAndView。 - 优先级:在注解处理器之后执行。
- 范围:作用于
-
ErrorController(兜底机制):- 范围:整个应用的最高层级,处理所有最终未被处理的异常,通常是请求已经转发到类似
/error的路径。 - 作用:作为最后的屏障,确保不会直接将 Servlet 容器的原始错误栈暴露给客户端。
BasicErrorController是其默认实现,根据内容协商返回 HTML 页面或 JSON 数据。 - 优先级:最低。只有在前面三层都无法解析该异常时,请求才会被转发到这里。
- 范围:整个应用的最高层级,处理所有最终未被处理的异常,通常是请求已经转发到类似
1.2 异常在 DispatcherServlet 中的流转
当 Controller 方法抛出异常,这个异常会沿着 HandlerAdapter、DispatcherServlet 的调用栈向上传播,最终在 DispatcherServlet.doDispatch 方法中被 catch 捕获。随后,它便被转入异常处理流程。
flowchart TD
Start["用户发起HTTP请求"] --> FilterChain["经过 Filter 链"]
FilterChain --> DispatcherServlet["进入 DispatcherServlet"]
DispatcherServlet --> DoDispatch["doDispatch 方法"]
DoDispatch --> HandleAdapter["调用 HandlerAdapter 执行 Controller 方法"]
HandleAdapter -->|"Controller方法抛出异常"| CatchException["被 doDispatch catch 捕获"]
CatchException --> ProcessDispatchResult["调用 processDispatchResult<br/>传入异常信息"]
HandleAdapter -->|"正常返回"| ProcessDispatchResult
ProcessDispatchResult --> Render["检查是否有异常"]
Render -->|"存在异常"| ResolverChain["遍历 HandlerExceptionResolver 链"]
ResolverChain --> TryEEHR["尝试 ExceptionHandlerExceptionResolver"]
TryEEHR -->|"找到 @ExceptionHandler 方法"| HandleMethod["调用 @ExceptionHandler 方法"]
HandleMethod --> ReturnModelAndView["返回 ModelAndView"]
TryEEHR -->|"未匹配"| TryRSER["尝试 ResponseStatusExceptionResolver"]
TryRSER -->|"匹配 @ResponseStatus"| UpdateStatus["更新响应状态码"]
UpdateStatus --> ReturnModelAndView
TryRSER -->|"未匹配"| TryDHER["尝试 DefaultHandlerExceptionResolver"]
TryDHER -->|"内部Spring异常"| ConvertToStatus["转换为对应状态码"]
ConvertToStatus --> ReturnModelAndView
ResolverChain -->|"所有解析器都返回null"| NoResolver["异常仍为 unhandled"]
NoResolver --> ThrowToServlet["异常抛给 Servlet 容器"]
ThrowToServlet --> ErrorPage["容器触发 /error 路径"]
ErrorPage --> ErrorController["由 ErrorController 处理"]
ErrorController --> EndNode["最终响应给客户端"]
ReturnModelAndView --> RenderView["渲染视图或序列化数据"]
RenderView --> EndNode
Render -->|"无异常"| NormalRender["正常渲染 ModelAndView"]
NormalRender --> EndNode
classDef decision fill:#fff4e6,stroke:#ff9800,stroke-width:2px,color:#333;
classDef process fill:#f8f9fa,stroke:#333,stroke-width:1px,color:#333;
class Start,FilterChain,DispatcherServlet,DoDispatch,HandleAdapter,CatchException,ProcessDispatchResult,Render,ResolverChain,TryEEHR,HandleMethod,ReturnModelAndView,TryRSER,UpdateStatus,TryDHER,ConvertToStatus,NoResolver,ThrowToServlet,ErrorPage,ErrorController,EndNode,RenderView,NormalRender process;
class Render process;
图 1 说明
- 核心路径:此图描绘了从 Controller 层抛出异常到生成用户可见响应的全生命周期。它明确指出,所有异常都会被导入
processDispatchResult方法,在这个方法内部,Spring MVC 会启动它的一套异常解析器链。 - 关键分支:图中重点展示了三个主要解析器的分工。
ExceptionHandlerExceptionResolver负责寻找并使用@ExceptionHandler方法,这是一种声明式的、高优先级的处理方式。ResponseStatusExceptionResolver处理更简单的@ResponseStatus注解,用于直接将异常映射到 HTTP 状态码。DefaultHandlerExceptionResolver则负责处理 Spring MVC 内部产生的标准异常。 - 兜底逻辑:当整个解析器链都无法处理该异常时(所有解析器都返回
null),异常会被重新抛出,由 Servlet 容器(如 Tomcat)接管。容器会按照其自身的错误处理机制,最终重定向到/error路径,这也就是ErrorController发挥作用的入口。这也解释了为何有时候我们会看到 Tomcat 默认的错误页面而不是 Spring Boot 的白色标签页。 - 设计意图:整个过程的设计体现了“职责分离”和“层层兜底”的思想。每一层都有其明确的职责,下一个层只处理上一层未处理的异常。这种设计保证了异常处理的灵活性和鲁棒性。
2. HandlerExceptionResolver 链:策略模式驱动的异常解析
HandlerExceptionResolver 接口是 Spring MVC 异常处理体系的基石。它将“如何解析异常”这个行为抽象出来,使得不同的解析策略可以轻松地插入和组合,这是策略模式的经典应用。
2.1 接口定义与职责
HandlerExceptionResolver 接口极其简洁:
// 源码片段:org.springframework.web.servlet.HandlerExceptionResolver
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
@Nullable Object handler,
Exception ex
);
}
它的核心职责是尝试将给定的 Exception 解析(处理)为一个 ModelAndView 对象,该对象可以指向一个具体的错误视图,并携带相关的错误信息。如果该解析器无法处理该异常,它必须返回 null,以便链中的下一个解析器有机会处理它。
2.2 源码分析:在 processDispatchResult 中的调用
在《请求处理全链路》一篇中,我们曾分析过 DispatcherServlet.doDispatch 方法。当一个 HandlerAdapter 完成处理(或抛出异常)后,DispatcherServlet 会调用 processDispatchResult。这正是启用异常解析器链的入口。
// 源码片段:org.springframework.web.servlet.DispatcherServlet.processDispatchResult
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
// 1. 检查是否存在异常
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 2. 核心:遍历所有已注册的 HandlerExceptionResolver
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// 3. 如果有 ModelAndView (可能是正常返回或异常解析得到),则进行渲染
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
// ... 省略日志和资源清理逻辑
}
代码解读:
exception参数是doDispatch中捕获的Exception。- 它首先排除了一种特殊异常
ModelAndViewDefiningException,这种异常本身直接包含了ModelAndView,无需解析。 - 核心在于
processHandlerException方法。此方法内部会遍历所有注册到 Spring 容器中的HandlerExceptionResolverBean。
// 源码片段:org.springframework.web.servlet.DispatcherServlet.processHandlerException
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// 检查响应是否已提交,如果已提交则无法改变
if (response.isCommitted()) {
logger.debug("Response already committed. Ignoring exception", ex);
return null;
}
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
// 遍历解析器链
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break; // 一旦某个解析器返回了非null的ModelAndView,即视为处理成功,跳出循环
}
}
}
// 如果所有解析器都返回null,exMv为null
if (exMv != null) {
// 处理成功,记录日志...
request.setAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE, ex);
return exMv;
}
// 没有解析器处理此异常,将其重新抛出
throw ex;
}
代码解读:
- 方法会遍历
this.handlerExceptionResolvers集合,这个集合默认情况下包含了 Spring MVC 自动配置的三个内置解析器。 - 短路机制:一旦找到第一个返回非
null的ModelAndView的解析器,循环便会终止。这意味着解析器的顺序至关重要。 - 异常再抛出:如果所有解析器都“袖手旁观”(返回
null),那么processHandlerException方法会重新将该异常抛出。然后,这个异常会被processDispatchResult方法的外层逻辑捕获,最终交由 Servlet 容器来处理。
2.3 三大内置实现及执行顺序
默认情况下,DispatcherServlet 会初始化一个内含三个解析器的列表,它们的顺序是:
-
ExceptionHandlerExceptionResolver:- 职责:处理
@ExceptionHandler注解的方法。它是AbstractHandlerMethodExceptionResolver的子类,因此可以像调用普通 Controller 方法一样,通过HandlerMethod来执行异常处理方法,并享受到完整的参数解析和返回值处理(包括消息转换器)的便利。 - 为何排第一:因为它的优先级最高,旨在让用户自定义的
@ControllerAdvice和方法内的@ExceptionHandler能最先捕获异常。
- 职责:处理
-
ResponseStatusExceptionResolver:- 职责:处理被
@ResponseStatus注解标记的异常类,或处理ResponseStatusException类型的异常。它会读取注解中的value()或reason(),并调用response.sendError(statusCode, reason)或response.setStatus(statusCode)来设置 HTTP 响应状态。 - 为何排第二:它处理一种更简单、直接的状态码映射场景。如果异常已经被
@ExceptionHandler处理,它就不会再有执行机会。
- 职责:处理被
-
DefaultHandlerExceptionResolver:- 职责:处理标准的 Spring MVC 内部异常,并将它们转换为合适的 HTTP 状态码。例如,
NoHandlerFoundException-> 404,HttpRequestMethodNotSupportedException-> 405,MissingServletRequestParameterException-> 400 等。 - 为何排第三:它作为 Spring MVC 内部异常的兜底处理。这些异常通常发生在 Handler 映射或调用的早期阶段,甚至
@ExceptionHandler都可能还未来得及介入。
- 职责:处理标准的 Spring MVC 内部异常,并将它们转换为合适的 HTTP 状态码。例如,
sequenceDiagram
participant DS as DispatcherServlet
participant Composite as HandlerExceptionResolver复合器
participant EE as ExceptionHandlerExceptionResolver
participant RS as ResponseStatusExceptionResolver
participant Def as DefaultHandlerExceptionResolver
participant ErrorC as BasicErrorController
DS ->> Composite: resolveException(request, response, handler, ex)
activate Composite
Note over Composite: 按固定顺序遍历所有解析器
Composite ->> EE: resolveException(request, response, handler, ex)
activate EE
EE-->>Composite: 返回 ModelAndView 或 null
deactivate EE
alt 如果 EE 返回了 ModelAndView
Composite-->>DS: 返回 ModelAndView (resolved=true)
deactivate Composite
DS ->> DS: render(mv, request, response)
else EE 返回 null
Composite ->> RS: resolveException(request, response, handler, ex)
activate RS
RS-->>Composite: 返回 ModelAndView 或 null
deactivate RS
alt 如果 RS 返回了 ModelAndView
Composite-->>DS: 返回 ModelAndView (resolved=true)
deactivate Composite
DS ->> DS: render(mv, request, response)
else RS 返回 null
Composite ->> Def: resolveException(request, response, handler, ex)
activate Def
Def-->>Composite: 返回 ModelAndView
deactivate Def
alt 如果 Def 返回了 ModelAndView
Composite-->>DS: 返回 ModelAndView (resolved=true)
deactivate Composite
DS ->> DS: render(mv, request, response)
else 所有内置解析器都返回 null
Composite-->>DS: 返回 null (resolved=false)
deactivate Composite
DS ->> DS: 重新抛出异常 (throw ex)
DS ->> ErrorC: 异常最终由 Servlet 容器触发 /error
end
end
end
图 2 说明:
- 协作流程:此序列图精确地展示了
DispatcherServlet如何与三个核心HandlerExceptionResolver交互。它强调了解析器的顺序性和短路执行特性。 - 职责解析:
ExceptionHandlerExceptionResolver负责最复杂的、基于注解的异常处理逻辑,它的内部实际上会调用一个被@ExceptionHandler注解的方法(HanderMethod),这个方法的参数解析和返回值处理与普通 Controller 方法完全一致。 - 状态码转换:
ResponseStatusExceptionResolver和DefaultHandlerExceptionResolver的主要工作是改变 HTTP 响应的状态码,并可能添加一条错误消息到响应中。它们不负责复杂的响应体渲染。 - 失败路径:当所有解析器都无法处理异常时,异常被重新抛出。这最终会导致 Servlet 容器触发一个指向
/error的转发,从而激活ErrorController。这张图清晰地连接了解析器链和兜底机制。
3. @ExceptionHandler 与 @ControllerAdvice:声明式异常处理
@ExceptionHandler 和 @ControllerAdvice 的组合是 Spring MVC 中最强大、最灵活的异常处理方式。它将异常处理逻辑与业务逻辑解耦,并通过一种声明式的方式,让开发者可以像编写普通请求处理方法一样编写异常处理器。
3.1 @ExceptionHandler 方法签名与返回值处理
一个 @ExceptionHandler 方法可以拥有非常灵活的方法签名。其支持的参数类型与 @RequestMapping 方法几乎完全相同,这得益于 HandlerMethod 的统一参数解析和返回值处理机制。
参数解析:
- 异常参数:可以接收一个或多个与
@ExceptionHandler注解的value相匹配的异常类型。例如@ExceptionHandler(IOException.class) public String handle(IOException ex)。 - 请求/响应参数:可以注入
HttpServletRequest、HttpServletResponse、HttpSession等。 - 模型与视图:可以注入
Model、ModelMap、RedirectAttributes等,用于向视图传递数据。 - 其他前文所述参数:包括由
@RequestParam、@PathVariable、@RequestBody、@RequestHeader等注解解析的参数(尽管在异常处理场景下,部分参数可能已不可用或不完全)。
返回值处理:
返回值处理同样与普通 Controller 方法一致,由 HandlerMethodReturnValueHandler 选择合适的处理器进行处理。
@ResponseBody或@RestController: 如果方法或类上存在此注解,返回值将被HttpMessageConverter序列化为 JSON/XML 写入响应体。这正是我们为 API 提供结构化错误信息的关键。String: 被ViewResolver解析为一个视图名称,渲染 HTML 页面。ResponseEntity<T>: 提供了对响应状态码、响应头和响应体的完全控制。是构建 RESTful API 错误响应最推荐的方式。
3.2 @ControllerAdvice 的全局作用域与过滤
@ControllerAdvice 是一个特殊的 @Component,它能被 Spring 自动扫描并注册。它的核心作用是将其中定义的 @ExceptionHandler、@InitBinder、@ModelAttribute 方法应用到所有或指定的 Controller 上。
其选择性应用的强大能力来源于它的可选属性:
basePackages/value(): 指定一个或多个包路径,只对该包及其子包下的 Controller 生效。assignableTypes/basePackageClasses(): 指定一个或多个具体的 Controller 类,只对指定的 Controller 有效。annotations(): 指定一个或多个注解,只对被这些注解标记的 Controller 生效。
3.3 源码分析:ExceptionHandlerExceptionResolver 内部机制
ExceptionHandlerExceptionResolver 是整个注解驱动异常处理的核心引擎。它通过解析 @ControllerAdvice 和 @ExceptionHandler,在异常发生时找到匹配的 HandlerMethod 并执行。
初始化阶段:
在 Bean 初始化完成后,afterPropertiesSet() 方法被调用,它会扫描所有被 @ControllerAdvice 标注的 Bean,并参与@ExceptionHandler 方法的缓存。
异常解析阶段:
当 resolveException 被调用时,它会调用 doResolveHandlerMethodException 方法。
// 源码简化示意:org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
// 1. 根据当前处理的 handlerMethod 和异常类型,找到最匹配的 @ExceptionHandler 方法
ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
if (exceptionHandlerMethod == null) {
return null; // 没有找到,返回 null
}
// 2. 准备参数解析器和返回值处理器(复用 Spring MVC 基础设施)
exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
// 3. 创建用于参数绑定的 ModelAndViewContainer
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
try {
// ... ServletRequest、ServletResponse 相关处理(如异步请求)
if (logger.isDebugEnabled()) { ... }
// 4. 核心:调用异常处理方法来处理异常!此方法内部会遍历参数解析器来填充方法参数。
exceptionHandlerMethod.invokeAndHandle(request, response, mavContainer, exception);
}
catch (Throwable invocationEx) {
// 处理异常调用自身抛出异常的情况... 返回 null 让下一个解析器处理或直接重新抛出
return null;
}
// 5. 根据 ModelAndViewContainer 的状态构建 ModelAndView 返回
ModelAndView mav = new ModelAndView();
mav.setStatus(mavContainer.getStatus());
mav.setViewName(mavContainer.getViewName());
mav.setModel(mavContainer.getModel());
// ... 处理响应体是否已由@ResponseBody等方式直接写入
return mav;
}
代码解读:
getExceptionHandlerMethod: 这个方法实现了局部优先于全局的查找逻辑。它会先查看当前@Controller方法所在类上是否有匹配的@ExceptionHandler,如果没有,再去所有@ControllerAdvice类中查找。invokeAndHandle: 这是整个机制的精妙之处。它将异常处理方法 (exceptionHandlerMethod) 看作一个普通的ServletInvocableHandlerMethod来调用。这意味着方法所需的HttpServletRequest、Model、Exception等参数,都会通过 Spring 的HandlerMethodArgumentResolver自动注入。同时,方法的返回值(如String,ResponseEntity,@ResponseBody标记的对象)也会交由HandlerMethodReturnValueHandler进行处理。这完美复用了前文所讲的参数解析和返回值处理机制,实现了高度的统一。
sequenceDiagram
participant EE as ExceptionHandlerExceptionResolver
participant Cache as ExceptionHandler缓存
participant ControllerAdvice as @ControllerAdvice Bean
participant SHM as ServletInvocableHandlerMethod
participant ArgsResolvers as HandlerMethodArgumentResolvers
participant ReturnHandlers as HandlerMethodReturnValueHandlers
EE ->> Cache: getExceptionHandlerMethod(handlerMethod, exception)
activate Cache
Note over Cache: 1. 先查当前 Controller 的 @ExceptionHandler<br/>2. 再查所有 @ControllerAdvice 的 @ExceptionHandler
Cache -->> EE: 返回匹配的 ServletInvocableHandlerMethod
deactivate Cache
alt 找到匹配的方法
EE ->> SHM: setHandlerMethodArgumentResolvers(ArgsResolvers)
EE ->> SHM: setHandlerMethodReturnValueHandlers(ReturnHandlers)
EE ->> SHM: invokeAndHandle(req, res, mavContainer, exception)
activate SHM
Note over SHM: 调用前,准备方法参数
SHM ->> ArgsResolvers: 解析参数 (e.g., Exception e, Model m, ...)
activate ArgsResolvers
ArgsResolvers -->> SHM: 返回参数数组
deactivate ArgsResolvers
SHM ->> ControllerAdvice: 反射调用 @ExceptionHandler 方法
activate ControllerAdvice
ControllerAdvice -->> SHM: 返回结果 (e.g., ResponseEntity, String)
deactivate ControllerAdvice
Note over SHM: 调用后,处理返回值
SHM ->> ReturnHandlers: 处理返回值 (e.g., 序列化为JSON, 解析视图名)
activate ReturnHandlers
ReturnHandlers -->> SHM: void (结果已写入 HttpServletResponse 或 Model)
deactivate ReturnHandlers
SHM -->> EE: void (mavContainer 已填充完毕)
deactivate SHM
EE ->> EE: 从 mavContainer 构建 ModelAndView
EE -->> DS: 返回 ModelAndView
else 未找到匹配方法
EE -->> DS: 返回 null
end
图 3 说明:
- 查找流程:清晰地展示了
ExceptionHandlerExceptionResolver如何寻找最匹配的HandlerMethod。它首先查找当前 Controller 内的局部处理器,然后才查找全局的@ControllerAdvice,确保了优先级。 - 统一调度:
ServletInvocableHandlerMethod是连接异常处理方法和 Spring MVC 基础设施的桥梁。无论异常处理逻辑多复杂,它的参数解析和返回值处理都复用了与普通 Controller 方法完全相同的机制(HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler)。 - 便利性本质:这张图深刻揭示了为何我们可以在
@ExceptionHandler方法中自由地使用@RequestBody、@ResponseBody、HttpServletResponse等参数和返回类型——这一切都归功于ExceptionHandlerExceptionResolver将异常处理方法视为一个标准的HandlerMethod来统一调度执行。
3.4 内联示例:验证局部与全局的优先级
下面通过一个可运行的 Demo 来验证异常处理的优先级。
// 1. 自定义业务异常
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "This is a custom Not Found from Annotation") // 这个会被全局ExceptionHandler屏蔽
public class MyBusinessException extends RuntimeException {
public MyBusinessException(String message) {
super(message);
}
}
// 2. 一个全局 @ControllerAdvice
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MyBusinessException.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleGlobalMyBusiness(MyBusinessException ex) {
Map<String, String> body = new HashMap<>();
body.put("error", "Global Handler");
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
@ExceptionHandler(Exception.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleAll(Exception ex) {
Map<String, String> body = new HashMap<>();
body.put("error", "Ultimate Global Fallback");
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 3. 测试 Controller
@RestController
public class DemoController {
@GetMapping("/local-exception")
public String localException() {
throw new MyBusinessException("Test for local handler");
}
@GetMapping("/global-exception")
public String globalExceotion() {
throw new RuntimeException("Test for global handler");
}
// 局部 @ExceptionHandler
@ExceptionHandler(MyBusinessException.class)
@ResponseBody
public ResponseEntity<Map<String, String>> handleLocalMyBusiness(MyBusinessException ex) {
Map<String, String> body = new HashMap<>();
body.put("error", "Local Handler");
body.put("message", ex.getMessage());
return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
}
}
验证与分析:
- 访问
/local-exception,返回{"error":"Local Handler","message":"Test for local handler"},状态码为400。这证明了局部优先于全局。尽管GlobalExceptionHandler也能处理MyBusinessException,但DemoController内的局部处理器优先级更高。 - 访问
/global-exception,返回{"error":"Ultimate Global Fallback","message":"Test for global handler"},状态码为500。这验证了全局@ControllerAdvice作为全局兜底的效力。
4. ErrorController 与 BasicErrorController:兜底的全局错误处理
当所有 HandlerExceptionResolver 都无法处理一个异常时,它就会被重新抛出,最终被 Servlet 容器捕获。容器会将请求转发到一个错误处理路径,这个路径默认就是 /error。Spring Boot 通过 BasicErrorController 接管了这个路径,实现了应用级别的最终错误处置。
4.1 ErrorController 接口
ErrorController 接口极其简单,主要作用是标记一个 Controller 为错误处理器,并允许定制错误处理的映射路径。
// 源码片段:org.springframework.boot.web.servlet.error.ErrorController
@FunctionalInterface
public interface ErrorController {
String getErrorPath();
}
在 Spring Boot 2.x 中,BasicErrorController 实现了这个接口,并通过 @RequestMapping("${server.error.path:${error.path:/error}}") 将自身映射到了 /error 路径。
4.2 BasicErrorController 的实现
BasicErrorController 是一个标准的 Spring MVC @Controller,巧妙地利用了 Spring MVC 的核心特性来实现 MIME 类型驱动的错误响应。
// 源码片段:
// org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
// ... 构造函数,注入了ErrorAttributes和ErrorProperties等
// 处理产生HTML的请求
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
// 如果resolveErrorView找不到视图,则返回Spring Boot默认的“白标错误”视图
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}
// 处理产生JSON/XML等的API请求
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
// ... 其他辅助方法
}
代码解读:
- 内容协商:
errorHtml方法上标注了produces = MediaType.TEXT_HTML_VALUE,这意味着只有请求的Accept头包含text/html时,此方法才会被匹配。而error方法则没有produces限定,它将作为text/html之外的所有请求(如application/json,*/*)的默认处理器。这是内容协商在方法层面的体现。 - HTML 分支:
errorHtml创建一个ModelAndView对象。它首先通过resolveErrorView方法尝试寻找自定义的错误视图(如src/main/resources/templates/error/404.html)。如果找不到,则返回一个名为"error"的默认视图,Spring Boot 会渲染一个内置的“白标错误”HTML 页面。 - JSON 分支:
error方法创建一个ResponseEntity<Map<String, Object>>。它调用getErrorAttributes收集错误信息,并返回一个包含状态码和错误属性 Map 的ResponseEntity。这个ResponseEntity会由HttpMessageConverter(如MappingJackson2HttpMessageConverter)序列化为 JSON。
4.3 ErrorAttributes:错误信息的收集者
ErrorAttributes 接口定义了从哪里以及如何获取错误属性(如 timestamp、error、status、path 等)。DefaultErrorAttributes 是其在 Spring Boot 中的默认实现。
// 源码片段:org.springframework.boot.web.servlet.error.DefaultErrorAttributes
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
// 如果 options 指定要包含消息/错误/异常,则添加或处理这些属性
if (options.isIncluded(Include.MESSAGE)) errorAttributes.put("message", getMessage(webRequest, error));
if (options.isIncluded(Include.ERROR)) errorAttributes.put("error", getError(webRequest));
if (options.isIncluded(Include.EXCEPTION)) errorAttributes.put("exception", error.getClass().getName());
// ... 等等,并处理 message 和 errors 的绑定逻辑
return errorAttributes;
}
private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
addStatus(errorAttributes, webRequest); // 添加 status 和 error 字段
addErrorDetails(errorAttributes, webRequest, includeStackTrace); // 添加 message 等
addPath(errorAttributes, webRequest);
return errorAttributes;
}
这些属性的取值,大多来自于 Request 对象的 javax.servlet.error.* 属性,这些属性是由 Servlet 容器在处理 /error 转发请求时自动设置的。例如:
javax.servlet.error.status_code->statusjavax.servlet.error.exception->exceptionjavax.servlet.error.message->messagejavax.servlet.error.request_uri->path
sequenceDiagram
participant SC as Servlet Container
participant DS as DispatcherServlet
participant Mapping as HandlerMapping
participant BEC as BasicErrorController
participant DEA as DefaultErrorAttributes
participant VR as ViewResolver / HMConv as HttpMessageConverter
Note over SC,DS: 假设Controller中异常未被Resolver链处理
SC ->> DS: 转发请求到 /error 路径
DS ->> Mapping: 查找 /error 的 Handler
Mapping -->> DS: 返回 BasicErrorController
DS ->> BEC: 调用 Handler 方法
alt Accept 包含 text/html
BEC ->> BEC: errorHtml(request, response)
BEC ->> DEA: getErrorAttributes(request, ...)
activate DEA
Note over DEA: 从 request 中提取<br/>javax.servlet.error.* 属性
DEA -->> BEC: 返回包含 timestamp, status 等的 Map
deactivate DEA
BEC ->> BEC: resolveErrorView(status, model)
alt 找到自定义视图 (e.g., 404.html)
BEC ->> VR: 解析并渲染视图
VR -->> BEC: 返回 ModelAndView
else 未找到
BEC ->> BEC: 使用默认 "error" 视图 (白标页)
end
BEC -->> DS: 返回 ModelAndView
else 其他 Accept (e.g., application/json)
BEC ->> BEC: error(request)
BEC ->> DEA: getErrorAttributes(request, ...)
activate DEA
DEA -->> BEC: 返回 Map
deactivate DEA
BEC -->> DS: 返回 ResponseEntity<Map<String, Object>>
DS ->> HMConv: 通过 HttpMessageConverter 将 ResponseEntity 序列化为 JSON
HMConv -->> DS: JSON 字符串
end
DS ->> SC: 将最终响应数据写回客户端
图 4 说明:
- 转发机制:此图起点是 Servlet 容器,强调了
/error请求是通过一次服务器内部的转发到达BasicErrorController的,这解释了为何在ErrorAttributes中能通过javax.servlet.error.*获取到原始错误信息。 - 属性收集:
DefaultErrorAttributes是错误信息的“采集器”。它不产生信息,只是从 Request 属性中提取由 Servlet 容器设置好的异常详情,并打包成一个标准的 Map。 - 响应分发:
BasicErrorController的两个核心方法errorHtml和error是响应分发的两个出口。errorHtml路径与ViewResolver交互,专注于产生可视化的 HTML 页面。而error路径与HttpMessageConverter交互,专注于产生可供程序解析的结构化数据。这个分发逻辑是整个错误处理体系对内容协商的最终实现。
5. 错误响应与内容协商:HTML 与 JSON 的分流
BasicErrorController 的精妙之处在于它完全复用了 Spring MVC 核心的内容协商机制,实现了不同客户端(浏览器 vs. API 调用方)得到不同格式错误响应的分流。
5.1 error 方法:JSON/XML 响应的构建
当 API 客户端(如 Postman)或前端使用 fetch/ajax 发起请求时,其请求头通常会是 Accept: application/json 或 */*。此时,BasicErrorController.error 方法会被选中。
ResponseEntity的使用:该方法返回ResponseEntity<Map<String, Object>>。Spring MVC 会使用HttpMessageConverter(如MappingJackson2HttpMessageConverter)将这个Map序列化为 JSON 字符串,并写入响应体。- 完全控制:
ResponseEntity允许它同时控制响应状态码、响应头(如Content-Type: application/json)和响应体,这是构建 RESTful API 的标准实践。
5.2 errorHtml 方法:HTML 错误页的渲染
当用户通过浏览器访问时,请求头一般是 Accept: text/html,application/xhtml+xml,...(text/html 的权重很高)。此时,BasicErrorController.errorHtml 方法被选中。
ModelAndView的使用:方法返回ModelAndView。它会尝试通过ErrorMvcAutoConfiguration配置的DefaultErrorViewResolver来解析一个特定的错误视图。- 视图解析策略:
DefaultErrorViewResolver会按照既定顺序,在已配置的static和templates路径下查找特定的错误页面:templates/error/<status_code>.html(如templates/error/404.html)templates/error/<series>.html(如templates/error/4xx.html)templates/error/error.html(通用错误页)static/error/目录下的静态错误页 (如static/error/404.html)
- 白标错误页面:如果以上所有视图都不存在,则会返回一个名为
"error"的ModelAndView。Spring Boot 内置了一个默认的“白标错误”(Whitelabel Error Page)视图,确保用户总能得到一个包含基本错误信息的 HTML 页面,避免了容器默认页面的不友好。
5.3 内联示例:观察 Accept 头对响应格式的影响
我们可以编写一个简单的 Controller 方法,故意抛出异常,并用不同的客户端来观察其响应。
@RestController
public class ContentNegotiationErrorController {
@GetMapping("/test-error")
public String throwError() {
throw new RuntimeException("Simulating an error for content negotiation test.");
}
}
验证步骤与分析:
-
浏览器访问
http://localhost:8080/test-error:- 浏览器默认发送
Accept: text/html,...请求头。 - Spring MVC 将请求分发到
BasicErrorController.errorHtml方法。 - 由于没有自定义错误页面,你将看到 Spring Boot 的白色标签错误页面(Whitelabel Error Page),其中包含状态码、错误类型和错误消息。响应
Content-Type为text/html。
- 浏览器默认发送
-
使用 curl 模拟 API 请求:
curl -v -H "Accept: application/json" http://localhost:8080/test-error- 我们显式指定了
Accept: application/json。 - 你将得到一个 JSON 响应,类似于:
{"timestamp":"2024-...","status":500,"error":"Internal Server Error","message":"Simulating an error for content negotiation test.","path":"/test-error"} - 响应
Content-Type为application/json。
- 我们显式指定了
-
使用 curl 不指定 Accept 头:
curl -v http://localhost:8080/test-error- 当不指定
Accept时,curl 和一些 HTTP 客户端可能会发送*/*,或者由服务器端决定。Spring MVC 在这种情况下,由于errorHtml方法有produces = MediaType.TEXT_HTML_VALUE的限制,*/*不能特定匹配text/html,因此请求会落到更通用的error方法上,最终返回 JSON。这表明 JSON 是BasicErrorController的非浏览器客户端的“默认”响应格式。
- 当不指定
这个实验清晰地证明了 BasicErrorController 如何完美利用内容协商,在不增加任何额外配置的情况下,智能地为不同客户端提供合适的错误响应。
6. 展望:ProblemDetail 与 RFC 7807 标准化错误
虽然 BasicErrorController 和 @ExceptionHandler 返回的 Map 结构或 ResponseEntity 提供了极大的灵活性,但在一个由多个微服务构成的分布式系统中,每个服务都自定义一套错误格式,会导致客户端难以统一处理。为此,RFC 7807 标准应运而生,Spring Framework 6 和 Spring Boot 3 也引入了 ProblemDetail 作为其实现。
6.1 RFC 7807 标准介绍
RFC 7807,“Problem Details for HTTP APIs”,定义了一种标准的、机器可读的格式来表达 HTTP API 响应中的错误。它提供了一个最小化的、可扩展的结构,核心字段包括:
type(URI):一个指向描述该问题类型的文档的 URI。客户端可以解析此 URI 来获取更详细的信息。title(String):一个简短、可读的问题摘要,对所有同类型的问题应保持相同。status(Integer):由服务器产生的 HTTP 状态码。detail(String):针对该特定问题事例的、易于人类理解的解释。instance(URI):一个指向引发此问题的具体请求的 URI。
一个标准的 Problem Detail JSON 结构如下:
{
"type": "https://example.com/probs/out-of-credit",
"title": "You do not have enough credit.",
"status": 403,
"detail": "Your current balance is 30, but that costs 50.",
"instance": "/account/12345/msgs/abc",
"balance": 30,
"accounts": ["/account/12345", "/account/67890"]
}
除了核心字段,还可以添加任意自定义扩展字段(如 balance, accounts),从而在标准化和灵活性之间取得平衡。
6.2 ProblemDetail 类与 ErrorResponseException 体系
在 Spring Framework 6.x / Boot 3.x 中,这些概念被具体化为以下关键类:
org.springframework.http.ProblemDetail:一个实现了 RFC 7807 规范的简单 Java Bean。它包含了type,title,status,detail,instance等属性的 getter/setter,以及一个Map<String, Object> properties用于存储扩展属性。org.springframework.web.ErrorResponseException:一个实现了ErrorResponse接口的通用异常类。它内置了一个ProblemDetail实例,并提供了构建器风格的 API 来方便设置错误信息。org.springframework.web.server.ResponseStatusException(演进):同样也实现了ErrorResponse接口,因此它也能直接输出 RFC 7807 格式的响应。
6.3 Spring Boot 3.x 中的工作方式
在 Spring Boot 3.x 中,启用 ProblemDetail 通常只需一个配置项:
spring.mvc.problemdetails.enabled=true
一旦启用,BasicErrorController 的行为就会发生变化。它的 error 方法(处理非 HTML 请求)将不再返回 Map<String, Object>,而是返回 ProblemDetail。更强大的是,任何 @ExceptionHandler 方法也可以直接返回 ProblemDetail 或实现 ErrorResponse 接口的异常对象,Spring MVC 会自动将其序列化为标准格式。
// Spring Boot 3.x / Spring 6.x 示例
@ControllerAdvice
public class GlobalProblemDetailHandler {
@ExceptionHandler(MyBusinessException.class)
public ProblemDetail handleMyBusiness(MyBusinessException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setTitle("User or Resource Not Found");
problemDetail.setType(URI.create("https://your-api.com/errors/not-found"));
problemDetail.setProperty("traceId", MDC.get("traceId")); // 添加自定义属性
problemDetail.setProperty("errorCode", 1001);
return problemDetail;
}
}
6.4 迁移的价值与对比
| 特性 | 传统方式 (Spring Boot 2.x) | ProblemDetail 方式 (Spring Boot 3.x) |
|---|---|---|
| 数据格式 | 自定义 Map 或任意对象,结构由开发者随意定义。 | 严格遵循 RFC 7807 的标准化 ProblemDetail 结构。 |
| 通用字段 | 常见字段如 message, error 等在各服务中名称可能不一致。 | type, title, status, detail, instance 字段统一,语义明确。 |
| 可扩展性 | 直接在 Map 中放入任意 key-value。 | 通过 ProblemDetail.setProperty() 方法添加标准外的自定义属性,保持了核心结构的稳定。 |
| 文档化 | 错误码和含义通常散落在代码或口头约定中。 | type 字段是一个 URI,可以指向一份在线文档,详细解释问题原因和解决方案。利于治理。 |
| 内容协商 | 依赖开发者手动处理 produces 等属性。 | Spring 自动将 ProblemDetail 根据 Accept 头序列化为 application/problem+json 或 application/problem+xml。 |
从设计哲学上看,这是 Spring 生态从“万物皆可 Map”的灵活性,向“核心统一、扩展有序”的标准化演进。它极大地降低了微服务体系下各服务间错误传递的认知负担和对接成本,是构建高可维护性系统的必然选择。
7. 生产事故排查专题
理论上的完美体系在生产环境中也可能因为配置疏忽或理解偏差而掉入陷阱。以下是两个典型的生产事故案例及其排查过程。
7.1 事故一:全局 @ExceptionHandler 神秘失效
现象:
线上某个微服务,为 RuntimeException 定义的全局 @ControllerAdvice 异常处理器,在大部分场景下工作正常,能返回统一的 JSON 错误体。但在某个特定模块的 Controller 中,当抛出 RuntimeException 时,客户端却收到了一个 HTML 格式的“白标错误页面”,导致移动端 App 因无法解析 JSON 而崩溃。
排查思路:
- 检查日志:首先查看服务端日志,发现该异常正常打印,没有额外的错误。
- 验证异常类型:确认抛出的异常确实是
RuntimeException或其子类,且没有被任何局部@ExceptionHandler捕获。 - 检查 Resolver 链:怀疑是
ExceptionHandlerExceptionResolver没有生效,导致异常被后续的解析器或最终的BasicErrorController处理。 - 诊断 @ControllerAdvice:在
GlobalExceptionHandler中增加断点或日志,发现在该特定 Controller 的请求中,handleAll方法从未被调用。 - 审视 @ControllerAdvice 配置:重点检查
@ControllerAdvice注解的属性。发现其定义如下:
@ControllerAdvice(basePackages = "com.example.service.order")
而那个出问题的 Controller 所在的包是 com.example.service.payment。这意味着 GlobalExceptionHandler 被明确限定只对 order 包下的 Controller 生效,payment 模块的 Controller 完全不在其管辖范围内,导致注解驱动的异常处理失败。
根因:
@ControllerAdvice 的 basePackages 属性配置范围过小,未包含发生异常的 Controller 所在的包。当 ExceptionHandlerExceptionResolver 在当前 Controller 和所有可用的 @ControllerAdvice 中都找不到匹配的处理器时,返回 null,异常最终流入 BasicErrorController,后者根据浏览器的 Accept 头返回了 HTML。
解决:
将 basePackages 修改为更大的范围,例如 com.example.service,或创建多个 @ControllerAdvice 分别服务于不同模块,确保所有 Controller 都被一个全局异常处理器覆盖。
最佳实践:
- 设置一个顶层的、无任何过滤属性的
@ControllerAdvice作为全局默认异常处理器,兜底处理Exception.class。 - 使用
@ControllerAdvice(assignableTypes = X.class)或注解过滤时,要确保覆盖到了所有需要的 Controller。 - 利用 IDE 的代码分析功能,检查
@ControllerAdvice的作用范围是否与预期一致。 - 编写集成测试,模拟各个模块异常来验证全局处理器的生效范围。
7.2 事故二:API 接口错误时返回 HTML 而非 JSON
现象:
后端 API 服务为前端 SPA(单页应用)提供数据。一次版本发布后,前端发现在某些 500 错误场景下,收到的响应 Content-Type 是 text/html,响应体是一串嵌套在特殊字符里的 HTML 代码,导致前端 JSON 解析失败,页面白屏。
排查思路:
- 前端网络审查:首先确认是哪个请求出问题。查看该请求的 Response Header,发现
Content-Type: text/html。 - 查看请求头:检查前端发出的 Rquest Header 中的
Accept字段。发现其值为text/html, application/json, */*; q=0.01。这是一个典型的浏览器或某些框架的默认Accept头,其中text/html的优先级最高。 - 关联后端逻辑:服务器端
BasicErrorController进行内容协商时,会优先匹配produces = text/html的errorHtml方法,因此返回了 HTML。 - 确认根本原因:HTTP 规范规定,如果
Accept头中多个类型权重相同,更具体的类型优先;如果都相同则按顺序优先。text/html排在前面,且有较高的隐式权重,导致服务器错误地判断了客户端的意图。
根因:
客户端(或其使用的 Ajax 库)在发送请求时,没有显式地重写 Accept 请求头为 application/json,而是使用了浏览器的默认 Accept 头。BasicErrorController 根据内容协商机制,合法但错误地为 API 请求选择了 HTML 响应分支。
解决: 有几种方案,按推荐程度排序:
- 前端修复(推荐):在前端 Ajax 请求库的全局配置中,强制设置请求头为
Accept: application/json。这是最直接、最正确的做法,表达了客户端明确期望的数据类型。 - 后端定制:
- 通过继承并重写
BasicErrorController的errorHtml或error方法,强制 API 路径下的所有错误请求都返回 JSON。可以通过判断请求路径是否包含/api/前缀来实现分流。 - 采用
WebMvcConfigurer配置configureContentNegotiation策略,为Accept头设置更严格的匹配规则,例如将默认内容类型从text/html改为application/json(但可能影响纯 Web 页面部分)。 - 使用 Spring Boot 3.x 并启用
ProblemDetail。ProblemDetail的响应类型是application/problem+json,它不会和text/html产生歧义。
- 通过继承并重写
最佳实践:
- API 自治原则:API 服务应被视为独立的服务组件,其响应格式不应受客户端请求头中与业务无关的值的影响。推荐永远在代码中通过
@ExceptionHandler返回ResponseEntity,明确控制整个响应(包括状态码、头和体),而不是依赖BasicErrorController的默认容错行为。 - 客户端自我标识:API 客户端必须明确地通过
Accept头宣告自己期望的响应格式,这是 HTTP 协议的基本约定。 - 建立统一的 API 基础架构,在网关或 Spring 拦截器/过滤器中统一处理上游服务的返回,提前捕获
text/html这种非预期的响应,并转换为标准 JSON 错误或抛出特定异常。
8. 面试高频专题
说明:以下内容与正文严格分离,旨在以问答形式加深对核心概念的理解,并应对相关技术面试。
1. Spring MVC 中有哪些处理异常的方式?它们的优先级顺序是什么?
- 标准回答:主要有四种方式,优先级从高到低为:1. Controller 内部的
@ExceptionHandler方法;2.@ControllerAdvice中的@ExceptionHandler方法;3.HandlerExceptionResolver接口的实现类(如ExceptionHandlerExceptionResolver本身以及ResponseStatusExceptionResolver等);4.ErrorController(如BasicErrorController)。 - 追问与加分回答:
- 追问:
HandlerExceptionResolver和@ExceptionHandler哪个先执行?- 回答:
ExceptionHandlerExceptionResolver作为一个HandlerExceptionResolver,负责执行@ExceptionHandler方法。所以它们在执行链上是包含关系。
- 回答:
- 追问:
Filter中的异常能被@ControllerAdvice处理吗?- 加分回答:不能。
@ControllerAdvice和HandlerExceptionResolver都工作在DispatcherServlet层面。而Filter在DispatcherServlet之前执行,其异常不会被@ControllerAdvice捕获。
- 加分回答:不能。
- 追问:如果在
@ExceptionHandler中抛出了新异常,会发生什么?- 加分回答:该异常会被抛出到调用它的
ExceptionHandlerExceptionResolver,后者如果无法处理,会按顺序传递给下一个HandlerExceptionResolver,或最终被ErrorController处理。
- 加分回答:该异常会被抛出到调用它的
- 追问:
2. @ExceptionHandler 和 @ControllerAdvice 的区别?如何控制多个 @ControllerAdvice 的顺序?
- 标准回答:
@ExceptionHandler可以定义在 Controller 内部或@ControllerAdvice类中。前者只作用于本 Controller,后者可作用于全局或指定范围的 Controller。要控制顺序,可以使用@Order注解或实现Ordered接口。值越小,优先级越高。 - 追问与加分回答:
- 追问:如果同一个异常在两个不同优先级的
@ControllerAdvice中都被定义了,哪个会生效?- 回答:优先级高的会生效(
@Order值小的)。
- 回答:优先级高的会生效(
- 追问:
basePackages、assignableTypes、annotations之间是什么关系?- 回答:它们是“与”(AND) 的关系。只有当 Controller 同时满足所有配置的条件时,该
@ControllerAdvice才会对其生效。
- 回答:它们是“与”(AND) 的关系。只有当 Controller 同时满足所有配置的条件时,该
- 追问:如果同一个异常在两个不同优先级的
3. HandlerExceptionResolver 的作用是什么?有哪些内置实现?
- 标准回答:它是一个策略接口,用于尝试将处理请求时抛出的异常解析为一个
ModelAndView对象。内置实现主要有:ExceptionHandlerExceptionResolver(处理@ExceptionHandler)、ResponseStatusExceptionResolver(处理@ResponseStatus和ResponseStatusException) 和DefaultHandlerExceptionResolver(处理 Spring MVC 内部标准异常,如 404)。 - 追问与加分回答:
- 追问:如果我想让我的自定义异常自动返回 501 状态码,用哪种方式最简单?
- 回答:在自定义异常类上使用
@ResponseStatus(HttpStatus.NOT_IMPLEMENTED)注解,由ResponseStatusExceptionResolver处理,或者抛出ResponseStatusException(HttpStatus.NOT_IMPLEMENTED)。
- 回答:在自定义异常类上使用
- 追问:
NoHandlerFoundException是如何被处理成 404 的?- 加分回答:当
DispatcherServlet找不到对应的 Handler 时,会抛出NoHandlerFoundException。DefaultHandlerExceptionResolver专门处理此异常,调用response.sendError(sc)将其转换为 404 错误。
- 加分回答:当
- 追问:如果我想让我的自定义异常自动返回 501 状态码,用哪种方式最简单?
4. BasicErrorController 是如何工作的?它如何决定返回 HTML 还是 JSON?
- 标准回答:
BasicErrorController是 Spring Boot 提供的最终错误处理@Controller,映射到/error路径。它基于 Spring MVC 的内容协商机制,通过errorHtml(处理Accept: text/html) 和error(处理其他) 两个方法来分流。前者用ViewResolver渲染错误视图,后者用HttpMessageConverter将错误属性 Map 序列化为 JSON/XML。 - 追问与加分回答:
- 追问:为什么有时会看到 Tomcat 的默认错误页而不是 Spring Boot 的白标页?
- 加分回答:当发生类似
RequestMapping不匹配(如 404)的错误时,Tomcat 在接入 Spring Boot 前就可能报了错。需要在application.properties中配置spring.mvc.throw-exception-if-no-handler-found=true和spring.web.resources.add-mappings=false来确保异常能传递到 Spring 的DispatcherServlet。
- 加分回答:当发生类似
- 追问:如何在不继承
BasicErrorController的情况下修改 JSON 返回的字段?- 回答:实现并注册一个自定义的
ErrorAttributesBean。
- 回答:实现并注册一个自定义的
- 追问:为什么有时会看到 Tomcat 的默认错误页而不是 Spring Boot 的白标页?
5. 如何自定义 Spring Boot 的错误 JSON 响应格式?
- 标准回答:有三种主要方式:1. 实现
ErrorAttributes接口覆盖默认实现,可以添加或修改返回的字段;2. 继承BasicErrorController并重写error方法,返回一个自定义的ResponseEntity;3. (推荐) 使用@ControllerAdvice配合@ExceptionHandler(Exception.class),在所有异常到达BasicErrorController之前将其拦截,并返回符合你业务定义的ResponseEntity。 - 追问与加分回答:
- 追问:这三种方式的优缺点分别是什么?
- 加分回答:方案1最简单,但不改变底层结构;方案2是全局的,但只对到达
/error的异常生效;方案3最灵活,能彻底屏蔽BasicErrorController,但需要小心处理,确保不会漏掉任何异常。
- 加分回答:方案1最简单,但不改变底层结构;方案2是全局的,但只对到达
- 追问:这三种方式的优缺点分别是什么?
6. 什么是 ProblemDetail (RFC 7807)?它相比传统错误响应有什么优势?
- 标准回答:
ProblemDetail是 RFC 7807 标准在 Spring 中的实现,是一种标准化的 HTTP API 错误响应格式。它定义了type、title、status、detail、instance等字段。相比传统的{ "error": "...", "message": "..." }这种各自为政的格式,它的优势在于:标准化、自描述、可扩展、易于文档化和工具集成,尤其适合微服务架构下的统一错误处理。 - 追问与加分回答:
- 追问:
ProblemDetail如何扩展自定义属性?- 回答:通过
ProblemDetail的setProperty(String name, Object value)方法。
- 回答:通过
- 追问:在 Spring Boot 3 中,
@ExceptionHandler方法可以直接返回ProblemDetail吗?- 加分回答:可以。控制器可以直接返回
ProblemDetail或ResponseEntity<ProblemDetail>。
- 加分回答:可以。控制器可以直接返回
- 追问:
7. 如果 Controller 中抛出异常,@ExceptionHandler 没捕获,会怎么处理?
- 标准回答:该异常会沿着调用栈向上抛出到
DispatcherServlet。DispatcherServlet会按顺序遍历它的HandlerExceptionResolver链。如果所有 Resolver 都返回null,则异常会被重新抛出,最终被 Servlet 容器(如 Tomcat)捕获。容器会按照其错误处理机制,将请求转发到/error路径,最终由BasicErrorController处理。 - 追问与加分回答:
- 追问:在什么情况下,一个匹配的
@ExceptionHandler会不被执行?- 加分回答:1.
@ControllerAdvice的过滤条件不满足;2.ExceptionHandlerExceptionResolver的初始化失败;3. 框架内部更深层的异常(如参数解析、消息转换器异常)在进入 Controller 方法前就被抛出,可能由其他特定的 Resolver 处理。
- 加分回答:1.
- 追问:在什么情况下,一个匹配的
8. 如何实现全局的业务异常处理并自动返回对应 HTTP 状态码?
- 标准回答:定义一个继承
RuntimeException的业务基异常类,并添加HttpStatus属性。然后创建一个@ControllerAdvice,在其中@ExceptionHandler该基异常,从异常对象中提取HttpStatus和消息,构建ResponseEntity返回。 - 追问与加分回答:
- 追问:用
ResponseStatusException能起到相同作用吗?- 回答:可以。直接
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "reason")即可,它会被ResponseStatusExceptionResolver处理。
- 回答:可以。直接
- 追问:如果想在返回 400 时附加详细的参数校验信息,该怎么做?
- 回答:需要从抛出的
MethodArgumentNotValidException或BindException中提取FieldError列表,然后在@ExceptionHandler中构造包含详细字段信息的 JSON 返回。
- 回答:需要从抛出的
- 追问:用
9. ResponseStatusExceptionResolver 和 @ResponseStatus 注解的关系?
- 标准回答:
ResponseStatusExceptionResolver是实现,@ResponseStatus是它的使用方式之一。该 Resolver 会检查异常是否被@ResponseStatus注解,或者异常本身是否是ResponseStatusException类型。如果是,它会读取注解或异常实例中定义的状态码和原因,并调用response.sendError(status, reason)。
10. ErrorAttributes 和 ProblemDetail 可以并存吗?在 Spring Boot 3.x 中发生了什么变化?
- 标准回答:可以并存,但不推荐。在 Spring Boot 3.x 以前,
ErrorAttributes是核心的错误信息模型。在 3.x 中,如果启用了spring.mvc.problemdetails.enabled=true,ErrorAttributes依然存在并用于填充ProblemDetail对象。但整个思维模型已经从“填充属性 Map”转向“构建标准ProblemDetail对象”,ErrorAttributes的角色被边缘化了。
11. 为何有时候错误页面展示的是 Tomcat 默认页面而不是 Spring Boot 的白色标签页?
- 标准回答:默认情况下,Spring Boot 的 404 错误是在
DispatcherServlet内部处理的,但如果找不到任何静态资源,Tomcat 会认为这是个错误并返回它自带的 404 页。要让所有 404 都走到 Spring 的体系内,需配置spring.mvc.throw-exception-if-no-handler-found=true使 Spring 抛出异常,并设置spring.web.resources.add-mappings=false最终禁用 Spring MVC 的默认资源处理器。
12. (系统设计题) 设计一个微服务全局异常处理框架...
-
标准回答:
- 核心依赖:使用 Spring Boot 3.x 和 RFC 7807 标准。
- 统一错误体:定义一个继承自
ErrorResponseException的业务异常基类AppException,其内部强制绑定一个problemTypeURI、一个业务错误码errorCode和一个 map 用于扩展信息。框架层面的ProblemDetail将由@ControllerAdvice自动构建。 - 服务内实现:每个微服务引入一个
common-exceptionjar,包含AppException。使用MDC(Mapped Diagnostic Context) 或ThreadLocal在过滤器入口生成并传递traceId。全局@ControllerAdvice捕获AppException及其子类,从MDC获取traceId并通过problemDetail.setProperty("traceId", traceId)添加到响应中。 - 网关层降级:网关(如 Spring Cloud Gateway)解析下游返回的错误响应。如果
Content-Type是application/problem+json,则反序列化为ProblemDetail,读取扩展属性中的errorCode。根据预设的规则(例如errorCode=ORDER_SERVICE_UNAVAILABLE)触发降级逻辑,如返回缓存数据或一个更友好的标准 Problem Detail 响应。 - 设计亮点:确保了“错误即文档”(每个
type对应一个文档页面)。traceId的全链路跟踪使得排查分布式调用链中的错误点变得非常迅速。网关基于标准格式而非状态码做降级,更加精准和可靠。
-
追问与加分回答:
- 追问:如何处理参数校验(Bean Validation)产生的异常,以符合此统一格式?
- 加分回答:在
@ControllerAdvice中处理BindException,遍历它的FieldError列表,将字段名和错误信息作为一个errors扩展属性放入ProblemDetail。
- 加分回答:在
- 追问:如果下游服务不可用,网关连
ProblemDetail都拿不到怎么办?- 回答:网关应有超时和断路机制(如 Resilience4j)。当发生
ConnectTimeoutException或断路器打开时,网关自身也应能生成一个标准的、描述“服务暂时不可用”的ProblemDetail返回给客户端。
- 回答:网关应有超时和断路机制(如 Resilience4j)。当发生
- 追问:如何处理参数校验(Bean Validation)产生的异常,以符合此统一格式?
Spring Web 异常处理速查表
| 机制 | 覆盖范围 | 响应类型 | 优先级 | 关键注解/类 |
|---|---|---|---|---|
| 局部 @ExceptionHandler | 当前 Controller | 任意(由方法返回值决定) | 最高 | @ExceptionHandler |
| 全局 @ControllerAdvice | 所有或部分的 Controller | 任意(由方法返回值决定) | 次高 | @ControllerAdvice, @ExceptionHandler |
| ResponseStatusExceptionResolver | 全局 | 设置 HTTP Status,无Body | 次低 | @ResponseStatus, ResponseStatusException |
| DefaultHandlerExceptionResolver | 全局 | 设置 HTTP Status,无Body | 低 | NoHandlerFoundException, MethodNotAllowedException 等 |
ResponseEntityExceptionHandler | 全局 (@ControllerAdvice) | ResponseEntity,可含复杂Body | 次高(属于第2层) | @ControllerAdvice 继承该类并重写特定方法 |
| ErrorController/BasicErrorController | 全局(兜底) | HTML 页面或 Map 序列化的 JSON/XML | 最低 | ErrorController, ErrorAttributes |
| ProblemDetail (3.x) | 全局(启用后替代默认错误响应) | application/problem+json/xml | 覆盖整个错误处理链末端 | ProblemDetail, ErrorResponseException |
延伸阅读
- Spring Framework 官方文档: “Web MVC” 章节下的 “Exceptions” 部分。
- Spring Boot 官方文档: “Error Handling” 部分,特别是 “Customizing Error Pages” 和 “Mapping Error Pages outside of Spring MVC”。
- RFC 7807 规范: Problem Details for HTTP APIs。