Spring Web 异常处理全景:ErrorController、ProblemDetail (RFC 7807)

5 阅读40分钟

概述

衔接前文,在《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@ControllerAdviceHandlerExceptionResolver 链 → ErrorController 的四层异常处理体系,层层递进,确保没有任何异常能逃脱处理。
  • Resolver 链的策略模式HandlerExceptionResolver 及其组合实现是策略模式的典范,三种核心内置 Resolver 各司其职,共同构成了一个有序、可扩展的异常解析生态系统。
  • 兜底的 BasicErrorController:作为整个体系的最后一道防线,BasicErrorController 优雅地处理 /error 请求,并根据内容协商智能地在 HTML 错误页和 JSON 错误体之间切换。
  • 错误响应与内容协商的联动:异常最终的响应形式(HTML/JSON/XML)并非固定不变,而是由 Spring MVC 核心的 HttpMessageConverterViewResolver 两大机制,在内容协商策略下共同决定。这正是前文知识在异常处理场景下的精彩复现。
  • 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 链、声明式注解、兜底控制器等核心组件,再探讨错误响应与内容协商的联动,最后通过标准化演进、事故复盘和面试题,完成从理论、源码到工业实践的闭环。

  • 逐模块说明

    1. 异常处理总览:建立四层体系模型,宏观理解异常从抛出到被最终处理的完整流转路径。
    2. HandlerExceptionResolver 链:深入源码,分析在 DispatcherServlet 中,如何以策略模式串联起多个 HandlerExceptionResolver 实现,职责分明地处理各类异常。
    3. @ExceptionHandler 与 @ControllerAdvice:聚焦最常用的声明式异常处理,剖析其参数解析、返回值处理(联动前文消息转换器),以及局部与全局优先级的实现原理。
    4. ErrorController 与 BasicErrorController:揭示最后一道防线的内部工作流程,包括错误属性收集、Spring Boot 自动化配置,以及如何响应 /error 路径。
    5. 错误响应与内容协商:解析 BasicErrorController 如何巧妙地利用 ViewResolverHttpMessageConverter,根据客户端 Accept 头,在 HTML 页面和 JSON 结构体之间智能分流。
    6. 展望 ProblemDetail:作为技术演进方向,介绍 RFC 7807 标准,分析 ProblemDetail 如何取代 ErrorAttributes Map,提供更规范、智能的响应。
    7. 生产事故排查:通过两个典型事故案例,串联前文知识,演示异常处理机制在生产环境下的排查思路与解决路径,体现其实用价值。
    8. 面试高频专题:提炼核心要点,以问答形式巩固关键概念,并应对高层次面试。
  • 关键结论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)”:

  1. Controller 级别的 @ExceptionHandler:

    • 范围:仅作用于定义它的那个 Controller 类内部。
    • 作用:处理特定 Controller 抛出的特定类型异常。这是最细粒度的“点对点”异常处理。
    • 优先级:最高。等同于在局部范围内提供了特化的异常处理逻辑。
  2. 全局级别的 @ControllerAdvice:

    • 范围:可作用于所有 Controller,或通过 basePackagesassignableTypes 等属性筛选出特定范围的 Controller。
    • 作用:提供跨 Controller 的全局异常处理、数据绑定和模型增强。这是最常见的全局异常处理方式。
    • 优先级:次于局部 @ExceptionHandler。当局部无法处理时,由全局 @ControllerAdvice 接管。
  3. HandlerExceptionResolver:

    • 范围:作用于 DispatcherServlet 这个核心前端控制器处理的所有请求。
    • 作用:这是一个策略模式的接口实现链。Spring MVC 内置了三个关键的实现,按固定顺序执行,将未被上述注解捕获的异常或特定 Spring MVC 内部异常转换为 ModelAndView
    • 优先级:在注解处理器之后执行。
  4. ErrorController (兜底机制):

    • 范围:整个应用的最高层级,处理所有最终未被处理的异常,通常是请求已经转发到类似 /error 的路径。
    • 作用:作为最后的屏障,确保不会直接将 Servlet 容器的原始错误栈暴露给客户端。BasicErrorController 是其默认实现,根据内容协商返回 HTML 页面或 JSON 数据。
    • 优先级:最低。只有在前面三层都无法解析该异常时,请求才会被转发到这里。

1.2 异常在 DispatcherServlet 中的流转

当 Controller 方法抛出异常,这个异常会沿着 HandlerAdapterDispatcherServlet 的调用栈向上传播,最终在 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 容器中的 HandlerExceptionResolver Bean。
// 源码片段: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 自动配置的三个内置解析器。
  • 短路机制:一旦找到第一个返回非 nullModelAndView 的解析器,循环便会终止。这意味着解析器的顺序至关重要
  • 异常再抛出:如果所有解析器都“袖手旁观”(返回 null),那么 processHandlerException 方法会重新将该异常抛出。然后,这个异常会被 processDispatchResult 方法的外层逻辑捕获,最终交由 Servlet 容器来处理。

2.3 三大内置实现及执行顺序

默认情况下,DispatcherServlet 会初始化一个内含三个解析器的列表,它们的顺序是:

  1. ExceptionHandlerExceptionResolver:

    • 职责:处理 @ExceptionHandler 注解的方法。它是 AbstractHandlerMethodExceptionResolver 的子类,因此可以像调用普通 Controller 方法一样,通过 HandlerMethod 来执行异常处理方法,并享受到完整的参数解析和返回值处理(包括消息转换器)的便利。
    • 为何排第一:因为它的优先级最高,旨在让用户自定义的 @ControllerAdvice 和方法内的 @ExceptionHandler 能最先捕获异常。
  2. ResponseStatusExceptionResolver:

    • 职责:处理被 @ResponseStatus 注解标记的异常类,或处理 ResponseStatusException 类型的异常。它会读取注解中的 value()reason(),并调用 response.sendError(statusCode, reason)response.setStatus(statusCode) 来设置 HTTP 响应状态。
    • 为何排第二:它处理一种更简单、直接的状态码映射场景。如果异常已经被 @ExceptionHandler 处理,它就不会再有执行机会。
  3. DefaultHandlerExceptionResolver:

    • 职责:处理标准的 Spring MVC 内部异常,并将它们转换为合适的 HTTP 状态码。例如,NoHandlerFoundException -> 404,HttpRequestMethodNotSupportedException -> 405,MissingServletRequestParameterException -> 400 等。
    • 为何排第三:它作为 Spring MVC 内部异常的兜底处理。这些异常通常发生在 Handler 映射或调用的早期阶段,甚至 @ExceptionHandler 都可能还未来得及介入。
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 方法完全一致。
  • 状态码转换ResponseStatusExceptionResolverDefaultHandlerExceptionResolver 的主要工作是改变 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)
  • 请求/响应参数:可以注入 HttpServletRequestHttpServletResponseHttpSession 等。
  • 模型与视图:可以注入 ModelModelMapRedirectAttributes 等,用于向视图传递数据。
  • 其他前文所述参数:包括由 @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 来调用。这意味着方法所需的 HttpServletRequestModelException 等参数,都会通过 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 方法完全相同的机制(HandlerMethodArgumentResolverHandlerMethodReturnValueHandler)。
  • 便利性本质:这张图深刻揭示了为何我们可以在 @ExceptionHandler 方法中自由地使用 @RequestBody@ResponseBodyHttpServletResponse 等参数和返回类型——这一切都归功于 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);
    }
}

验证与分析:

  1. 访问 /local-exception,返回 {"error":"Local Handler","message":"Test for local handler"},状态码为 400。这证明了局部优先于全局。尽管 GlobalExceptionHandler 也能处理 MyBusinessException,但 DemoController 内的局部处理器优先级更高。
  2. 访问 /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 -> status
  • javax.servlet.error.exception -> exception
  • javax.servlet.error.message -> message
  • javax.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 的两个核心方法 errorHtmlerror 是响应分发的两个出口。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 会按照既定顺序,在已配置的 statictemplates 路径下查找特定的错误页面:
    1. templates/error/<status_code>.html (如 templates/error/404.html)
    2. templates/error/<series>.html (如 templates/error/4xx.html)
    3. templates/error/error.html (通用错误页)
    4. 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.");
    }
}

验证步骤与分析:

  1. 浏览器访问 http://localhost:8080/test-error

    • 浏览器默认发送 Accept: text/html,... 请求头。
    • Spring MVC 将请求分发到 BasicErrorController.errorHtml 方法。
    • 由于没有自定义错误页面,你将看到 Spring Boot 的白色标签错误页面(Whitelabel Error Page),其中包含状态码、错误类型和错误消息。响应 Content-Typetext/html
  2. 使用 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-Typeapplication/json
  3. 使用 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+jsonapplication/problem+xml

从设计哲学上看,这是 Spring 生态从“万物皆可 Map”的灵活性,向“核心统一、扩展有序”的标准化演进。它极大地降低了微服务体系下各服务间错误传递的认知负担和对接成本,是构建高可维护性系统的必然选择。


7. 生产事故排查专题

理论上的完美体系在生产环境中也可能因为配置疏忽或理解偏差而掉入陷阱。以下是两个典型的生产事故案例及其排查过程。

7.1 事故一:全局 @ExceptionHandler 神秘失效

现象: 线上某个微服务,为 RuntimeException 定义的全局 @ControllerAdvice 异常处理器,在大部分场景下工作正常,能返回统一的 JSON 错误体。但在某个特定模块的 Controller 中,当抛出 RuntimeException 时,客户端却收到了一个 HTML 格式的“白标错误页面”,导致移动端 App 因无法解析 JSON 而崩溃。

排查思路

  1. 检查日志:首先查看服务端日志,发现该异常正常打印,没有额外的错误。
  2. 验证异常类型:确认抛出的异常确实是 RuntimeException 或其子类,且没有被任何局部 @ExceptionHandler 捕获。
  3. 检查 Resolver 链:怀疑是 ExceptionHandlerExceptionResolver 没有生效,导致异常被后续的解析器或最终的 BasicErrorController 处理。
  4. 诊断 @ControllerAdvice:在 GlobalExceptionHandler 中增加断点或日志,发现在该特定 Controller 的请求中,handleAll 方法从未被调用。
  5. 审视 @ControllerAdvice 配置:重点检查 @ControllerAdvice 注解的属性。发现其定义如下:
@ControllerAdvice(basePackages = "com.example.service.order")

而那个出问题的 Controller 所在的包是 com.example.service.payment。这意味着 GlobalExceptionHandler 被明确限定只对 order 包下的 Controller 生效,payment 模块的 Controller 完全不在其管辖范围内,导致注解驱动的异常处理失败。

根因@ControllerAdvicebasePackages 属性配置范围过小,未包含发生异常的 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-Typetext/html,响应体是一串嵌套在特殊字符里的 HTML 代码,导致前端 JSON 解析失败,页面白屏。

排查思路

  1. 前端网络审查:首先确认是哪个请求出问题。查看该请求的 Response Header,发现 Content-Type: text/html
  2. 查看请求头:检查前端发出的 Rquest Header 中的 Accept 字段。发现其值为 text/html, application/json, */*; q=0.01。这是一个典型的浏览器或某些框架的默认 Accept 头,其中 text/html 的优先级最高。
  3. 关联后端逻辑:服务器端 BasicErrorController 进行内容协商时,会优先匹配 produces = text/htmlerrorHtml 方法,因此返回了 HTML。
  4. 确认根本原因:HTTP 规范规定,如果 Accept 头中多个类型权重相同,更具体的类型优先;如果都相同则按顺序优先。text/html 排在前面,且有较高的隐式权重,导致服务器错误地判断了客户端的意图。

根因: 客户端(或其使用的 Ajax 库)在发送请求时,没有显式地重写 Accept 请求头为 application/json,而是使用了浏览器的默认 Accept 头。BasicErrorController 根据内容协商机制,合法但错误地为 API 请求选择了 HTML 响应分支。

解决: 有几种方案,按推荐程度排序:

  1. 前端修复(推荐):在前端 Ajax 请求库的全局配置中,强制设置请求头为 Accept: application/json。这是最直接、最正确的做法,表达了客户端明确期望的数据类型。
  2. 后端定制
    • 通过继承并重写 BasicErrorControllererrorHtmlerror 方法,强制 API 路径下的所有错误请求都返回 JSON。可以通过判断请求路径是否包含 /api/ 前缀来实现分流。
    • 采用 WebMvcConfigurer 配置 configureContentNegotiation 策略,为 Accept 头设置更严格的匹配规则,例如将默认内容类型从 text/html 改为 application/json(但可能影响纯 Web 页面部分)。
    • 使用 Spring Boot 3.x 并启用 ProblemDetailProblemDetail 的响应类型是 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 处理吗?
      • 加分回答:不能。@ControllerAdviceHandlerExceptionResolver 都工作在 DispatcherServlet 层面。而 FilterDispatcherServlet 之前执行,其异常不会被 @ControllerAdvice 捕获。
    • 追问:如果在 @ExceptionHandler 中抛出了新异常,会发生什么?
      • 加分回答:该异常会被抛出到调用它的 ExceptionHandlerExceptionResolver,后者如果无法处理,会按顺序传递给下一个 HandlerExceptionResolver,或最终被 ErrorController 处理。

2. @ExceptionHandler 和 @ControllerAdvice 的区别?如何控制多个 @ControllerAdvice 的顺序?

  • 标准回答@ExceptionHandler 可以定义在 Controller 内部或 @ControllerAdvice 类中。前者只作用于本 Controller,后者可作用于全局或指定范围的 Controller。要控制顺序,可以使用 @Order 注解或实现 Ordered 接口。值越小,优先级越高。
  • 追问与加分回答:
    • 追问:如果同一个异常在两个不同优先级的 @ControllerAdvice 中都被定义了,哪个会生效?
      • 回答:优先级高的会生效(@Order 值小的)。
    • 追问basePackagesassignableTypesannotations 之间是什么关系?
      • 回答:它们是“与”(AND) 的关系。只有当 Controller 同时满足所有配置的条件时,该 @ControllerAdvice 才会对其生效。

3. HandlerExceptionResolver 的作用是什么?有哪些内置实现?

  • 标准回答:它是一个策略接口,用于尝试将处理请求时抛出的异常解析为一个 ModelAndView 对象。内置实现主要有:ExceptionHandlerExceptionResolver (处理 @ExceptionHandler)、ResponseStatusExceptionResolver (处理 @ResponseStatusResponseStatusException) 和 DefaultHandlerExceptionResolver (处理 Spring MVC 内部标准异常,如 404)。
  • 追问与加分回答:
    • 追问:如果我想让我的自定义异常自动返回 501 状态码,用哪种方式最简单?
      • 回答:在自定义异常类上使用 @ResponseStatus(HttpStatus.NOT_IMPLEMENTED) 注解,由 ResponseStatusExceptionResolver 处理,或者抛出 ResponseStatusException(HttpStatus.NOT_IMPLEMENTED)
    • 追问NoHandlerFoundException 是如何被处理成 404 的?
      • 加分回答:当 DispatcherServlet 找不到对应的 Handler 时,会抛出 NoHandlerFoundExceptionDefaultHandlerExceptionResolver 专门处理此异常,调用 response.sendError(sc) 将其转换为 404 错误。

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=truespring.web.resources.add-mappings=false 来确保异常能传递到 Spring 的 DispatcherServlet
    • 追问:如何在不继承 BasicErrorController 的情况下修改 JSON 返回的字段?
      • 回答:实现并注册一个自定义的 ErrorAttributes Bean。

5. 如何自定义 Spring Boot 的错误 JSON 响应格式?

  • 标准回答:有三种主要方式:1. 实现 ErrorAttributes 接口覆盖默认实现,可以添加或修改返回的字段;2. 继承 BasicErrorController 并重写 error 方法,返回一个自定义的 ResponseEntity;3. (推荐) 使用 @ControllerAdvice 配合 @ExceptionHandler(Exception.class),在所有异常到达 BasicErrorController 之前将其拦截,并返回符合你业务定义的 ResponseEntity
  • 追问与加分回答:
    • 追问:这三种方式的优缺点分别是什么?
      • 加分回答:方案1最简单,但不改变底层结构;方案2是全局的,但只对到达 /error 的异常生效;方案3最灵活,能彻底屏蔽 BasicErrorController,但需要小心处理,确保不会漏掉任何异常。

6. 什么是 ProblemDetail (RFC 7807)?它相比传统错误响应有什么优势?

  • 标准回答ProblemDetail 是 RFC 7807 标准在 Spring 中的实现,是一种标准化的 HTTP API 错误响应格式。它定义了 typetitlestatusdetailinstance 等字段。相比传统的 { "error": "...", "message": "..." } 这种各自为政的格式,它的优势在于:标准化、自描述、可扩展、易于文档化和工具集成,尤其适合微服务架构下的统一错误处理。
  • 追问与加分回答:
    • 追问ProblemDetail 如何扩展自定义属性?
      • 回答:通过 ProblemDetailsetProperty(String name, Object value) 方法。
    • 追问:在 Spring Boot 3 中,@ExceptionHandler 方法可以直接返回 ProblemDetail 吗?
      • 加分回答:可以。控制器可以直接返回 ProblemDetailResponseEntity<ProblemDetail>

7. 如果 Controller 中抛出异常,@ExceptionHandler 没捕获,会怎么处理?

  • 标准回答:该异常会沿着调用栈向上抛出到 DispatcherServletDispatcherServlet 会按顺序遍历它的 HandlerExceptionResolver 链。如果所有 Resolver 都返回 null,则异常会被重新抛出,最终被 Servlet 容器(如 Tomcat)捕获。容器会按照其错误处理机制,将请求转发到 /error 路径,最终由 BasicErrorController 处理。
  • 追问与加分回答:
    • 追问:在什么情况下,一个匹配的 @ExceptionHandler 会不被执行?
      • 加分回答:1. @ControllerAdvice 的过滤条件不满足;2. ExceptionHandlerExceptionResolver 的初始化失败;3. 框架内部更深层的异常(如参数解析、消息转换器异常)在进入 Controller 方法前就被抛出,可能由其他特定的 Resolver 处理。

8. 如何实现全局的业务异常处理并自动返回对应 HTTP 状态码?

  • 标准回答:定义一个继承 RuntimeException 的业务基异常类,并添加 HttpStatus 属性。然后创建一个 @ControllerAdvice,在其中 @ExceptionHandler 该基异常,从异常对象中提取 HttpStatus 和消息,构建 ResponseEntity 返回。
  • 追问与加分回答:
    • 追问:用 ResponseStatusException 能起到相同作用吗?
      • 回答:可以。直接 throw new ResponseStatusException(HttpStatus.NOT_FOUND, "reason") 即可,它会被 ResponseStatusExceptionResolver 处理。
    • 追问:如果想在返回 400 时附加详细的参数校验信息,该怎么做?
      • 回答:需要从抛出的 MethodArgumentNotValidExceptionBindException 中提取 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=trueErrorAttributes 依然存在并用于填充 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. (系统设计题) 设计一个微服务全局异常处理框架...

  • 标准回答

    1. 核心依赖:使用 Spring Boot 3.x 和 RFC 7807 标准。
    2. 统一错误体:定义一个继承自 ErrorResponseException 的业务异常基类 AppException,其内部强制绑定一个 problemType URI、一个业务错误码 errorCode 和一个 map 用于扩展信息。框架层面的 ProblemDetail 将由 @ControllerAdvice 自动构建。
    3. 服务内实现:每个微服务引入一个 common-exception jar,包含 AppException。使用 MDC (Mapped Diagnostic Context) 或 ThreadLocal 在过滤器入口生成并传递 traceId。全局 @ControllerAdvice 捕获 AppException 及其子类,从 MDC 获取 traceId 并通过 problemDetail.setProperty("traceId", traceId) 添加到响应中。
    4. 网关层降级:网关(如 Spring Cloud Gateway)解析下游返回的错误响应。如果 Content-Typeapplication/problem+json,则反序列化为 ProblemDetail,读取扩展属性中的 errorCode。根据预设的规则(例如 errorCode=ORDER_SERVICE_UNAVAILABLE)触发降级逻辑,如返回缓存数据或一个更友好的标准 Problem Detail 响应。
    5. 设计亮点:确保了“错误即文档”(每个 type 对应一个文档页面)。traceId 的全链路跟踪使得排查分布式调用链中的错误点变得非常迅速。网关基于标准格式而非状态码做降级,更加精准和可靠。
  • 追问与加分回答:

    • 追问:如何处理参数校验(Bean Validation)产生的异常,以符合此统一格式?
      • 加分回答:在 @ControllerAdvice 中处理 BindException,遍历它的 FieldError 列表,将字段名和错误信息作为一个 errors 扩展属性放入 ProblemDetail
    • 追问:如果下游服务不可用,网关连 ProblemDetail 都拿不到怎么办?
      • 回答:网关应有超时和断路机制(如 Resilience4j)。当发生 ConnectTimeoutException 或断路器打开时,网关自身也应能生成一个标准的、描述“服务暂时不可用”的 ProblemDetail 返回给客户端。

Spring Web 异常处理速查表

机制覆盖范围响应类型优先级关键注解/类
局部 @ExceptionHandler当前 Controller任意(由方法返回值决定)最高@ExceptionHandler
全局 @ControllerAdvice所有或部分的 Controller任意(由方法返回值决定)次高@ControllerAdvice, @ExceptionHandler
ResponseStatusExceptionResolver全局设置 HTTP Status,无Body次低@ResponseStatus, ResponseStatusException
DefaultHandlerExceptionResolver全局设置 HTTP Status,无BodyNoHandlerFoundException, 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