从抛出异常到返回 JSON/XML:SpringBoot 异常处理全链路解析

前言

在 Web 开发中,异常处理是不可避免的一环。初学者往往喜欢在 Service 或 Controller 层写大量的 try-catch 代码,最后返回一个 Result 对象。这种做法虽然直观,但会导致业务代码与错误处理逻辑严重耦合,代码极其臃肿。

Spring Boot (基于 Spring MVC) 提供了一套优雅的、解耦的异常处理机制。本文将带你深入底层,探究当一个异常被抛出后,究竟经历了怎样的奇幻漂流,又是如何根据前端需求自动变成 JSON 或 XML 的。


没问题,为了让你的博客内容足够硬核且具有实战参考价值,我将这个异常处理流程进行了大幅度的扩充。

这次我们不再只停留在表面,而是结合"源码级"的执行步骤完整的代码示例,把整个过程拆解得清清楚楚。

你可以直接使用以下内容作为博客的核心章节。


Spring Boot 异常处理全链路深度解析

很多同学只会用 @ControllerAdvice,却不知道当一个异常抛出后,Spring Boot 内部到底发生了什么。下面我们通过一个真实的业务场景,配合源码视角,还原异常的"一生"。

1. 场景准备:案发现场

首先,我们需要构建一个标准的异常抛出场景。

1.1 定义标准响应体 (Result)

这是企业级开发的标配,前后端统一契约。

java 复制代码
@Data
public class Result<T> {
    private Integer code;
    private String msg;
    private T data;

    public static <T> Result<T> fail(Integer code, String msg) {
        Result<T> r = new Result<>();
        r.code = code;
        r.msg = msg;
        return r;
    }
}

1.2 定义自定义异常 (MyException)

java 复制代码
public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(String message) {
        super(message);
    }
}

1.3 编写 Controller (肇事者)

java 复制代码
@RestController
public class OrderController {
    @GetMapping("/order/{id}")
    public Result getOrder(@PathVariable Integer id) {
        if (id < 0) {
            // 【关键点】:这里抛出了异常,Controller 方法立即终止!
            throw new OrderNotFoundException("订单ID不能为负数");
        }
        return new Result(); // 正常逻辑
    }
}

1.4 编写全局异常处理器 (救援队)

java 复制代码
@RestControllerAdvice // 相当于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

    @ExceptionHandler(OrderNotFoundException.class)
    public Result handleOrderException(OrderNotFoundException e) {
        // 捕获异常,并"捏造"一个优雅的 Result 返回
        return Result.fail(404, e.getMessage());
    }
}

2. 深度解析:异常处理的七步"奇幻漂流"

当用户请求 GET /order/-1 时,后台发生了如下精密的操作:

第一步:异常冒泡 (JVM 层面)

Controller 的 getOrder 方法执行到 throw 语句。此时,当前方法栈帧被销毁,Controller 彻底"挂了"。异常对象开始沿着调用栈向上冒泡。

第二步:DispatcherServlet 捕获 (总指挥接管)

异常冒泡到了 Spring MVC 的最外层------DispatcherServlet.doDispatch() 方法。这里有一个巨大的 try-catch 块(源码简化版):

java 复制代码
// DispatcherServlet.java
try {
    // 尝试执行 Controller
    mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
} catch (Exception dispatchException) {
    // 【捕获点】Controller 抛出的 OrderNotFoundException 在这里被捕获!
    // 进入异常处理流程
    processDispatchResult(processedRequest, response, mappedHandler, dispatchException, mv);
}
第三步:寻找解析器 (HandlerExceptionResolver)

processDispatchResult 内部,Spring 会遍历所有注册的异常解析器链 ,问:"谁能处理 OrderNotFoundException?"

Spring Boot 默认配置了 ExceptionHandlerExceptionResolver,它举手说:"我能!我在 GlobalExceptionHandler 类里看到了一个 @ExceptionHandler 注解匹配这个异常。"

第四步:反射调用 (执行救援逻辑)

ExceptionHandlerExceptionResolver 通过 Java 反射机制,调用我们写的 handleOrderException(e) 方法。

  • 输入 :刚才捕获的异常对象 e
  • 执行 :运行我们的代码 return Result.fail(404, ...)
  • 输出 :拿到一个 Result 对象。
第五步:处理返回值 (HandlerMethodReturnValueHandler)

框架拿到 Result 对象后,并不会直接发给前端。它发现异常处理类上标记了 @RestControllerAdvice (含 @ResponseBody)

于是,它将任务移交给 RequestResponseBodyMethodProcessor

  • 这个组件既负责处理 @RequestBody (读),也负责处理 @ResponseBody (写)。
第六步:内容协商 (Content Negotiation)

RequestResponseBodyMethodProcessor 开始决定用什么格式返回数据。

  1. 查看数据 :返回值是 Result 对象。
  2. 查看需求 :检查 HTTP 请求头 Accept
    • 如果是浏览器默认请求,通常包含 */*
    • 如果是 Postman/Ajax,可能是 application/json
    • 如果是旧系统调用,可能是 application/xml
  3. 匹配转换器 :遍历 HttpMessageConverter 列表。
    • Jackson 说:"我是处理 JSON 的,我可以把 Result 对象转成 JSON 字符串。"
第七步:序列化与写入 (Write Response)

Jackson 转换器开始工作:

  1. Result 对象序列化为 JSON 字符串:{"code":404, "msg":"订单ID不能为负数", "data":null}
  2. 获取 HttpServletResponse 输出流。
  3. 设置 Content-Type: application/json
  4. 将字符串写入流中,发送给客户端。

3. 总结图

text 复制代码
[Controller 抛出异常] 
       ⬇
[JVM 冒泡]
       ⬇
[DispatcherServlet 捕获 (catch)]
       ⬇
[寻找异常解析器 (Resolver)]
       ⬇
[反射调用 @ExceptionHandler 方法] --> 生成 Result 对象
       ⬇
[检测 @ResponseBody 注解]
       ⬇
[内容协商 (检查 Accept 头)]
       ⬇
[匹配 HttpMessageConverter (如 Jackson)]
       ⬇
[序列化 (Result -> JSON/XML)]
       ⬇
[写入 HttpServletResponse]
       ⬇
[前端收到报错]

深度解密:异常处理中的"内容协商"

很多开发者认为内容协商(Content Negotiation)只在正常的 Controller 请求中生效,其实不然。异常处理返回的结果,同样完美支持内容协商。

1. 原理分析

无论是 Controller 的正常返回,还是 @ExceptionHandler 的异常返回,只要涉及 "对象转 HTTP Body" ,Spring MVC 底层都交给同一个处理器:RequestResponseBodyMethodProcessor

它会执行标准的"谈判"流程:

  1. 看货 :拿到返回值对象(Result)。
  2. 看客户需求 :读取 HTTP 请求头中的 Accept 字段(例如 application/jsonapplication/xml)。
  3. 找翻译官 :遍历容器中所有的 HttpMessageConverter
  4. 执行转换:找到能同时匹配"对象类型"和"客户需求"的转换器,执行序列化。
2. 场景演示

假设我们引入了 jackson-dataformat-xml 依赖,Spring Boot 会自动注册 XML 转换器。

  • 场景 A:前端是 Vue/React (默认)

    • 请求头:Accept: application/json

    • 响应:

      json 复制代码
      { "code": 500, "msg": "系统繁忙", "data": null }
  • 场景 B:前端是旧系统 (指定 XML)

    • 请求头:Accept: application/xml

    • 响应:

      xml 复制代码
      <Result>
          <code>500</code>
          <msg>系统繁忙</msg>
          <data/>
      </Result>

结论:我们不需要修改一行 Java 代码,异常信息就能自动适应前端需要的格式。


Spring MVC vs Spring Boot:内容协商谁在干活?

在这个过程中,我们需要理清两者的分工:

  1. Spring MVC(机制提供者)

    • 提供了 DispatcherServlet 捕获异常的机制。
    • 提供了 @ControllerAdvice@ExceptionHandler 注解。
    • 提供了内容协商管理器 (ContentNegotiationManager) 和消息转换器接口 (HttpMessageConverter)。
    • 它是"发动机"。
  2. Spring Boot(自动化配置)

    • 自动配置了 ErrorMvcAutoConfiguration(提供兜底的 /error 路径)。
    • 自动识别 Classpath 下的 Jackson 包,并注册了 JSON 和 XML 的转换器。
    • 它是"装配工",让你开箱即用。

SpringBoot的默认异常处理方案

Spring Boot 的错误处理方案,核心就是一个词:"自动兜底"

它的官方学名叫做 "默认全局错误处理机制"。即使你一行异常处理代码都不写,Spring Boot 也能保证你的应用在报错时,不会直接把服务器炸了,或者给用户看一堆乱码,而是返回一个"虽然丑但结构清晰"的错误响应。

这个方案的核心由 1 个 Controller2 种响应模式1 个页面 组成。


1. 核心组件:BasicErrorController

这是 Spring Boot 自动配置 (ErrorMvcAutoConfiguration) 帮你创建的一个特殊的 Controller。

  • 它的地位 :和你的 OrderControllerUserController 平级,都是处理 HTTP 请求的。
  • 它的地盘 :默认监听 /error 路径。
  • 工作原理
    1. 当应用中发生异常(且没被 Spring MVC 拦截),或者访问了不存在的路径(404)。
    2. Servlet 容器(Tomcat)会捕捉到错误。
    3. Tomcat 发现你没有配置专门的错误页,于是根据 Spring Boot 的约定,把请求转发 (Forward)/error 路径。
    4. BasicErrorController 收到请求,开始干活。

2. 智能响应:看人下菜碟(内容协商)

BasicErrorController 非常智能,它会根据**"谁在访问"**(检查 HTTP 请求头 Accept),决定返回什么格式的数据。它内部定义了两个处理方法:

模式 A:浏览器访问 (返回 HTML)
  • 判断依据 :请求头包含 text/html
  • 对应方法errorHtml()
  • 结果
    • 它会去查找有没有定义好的错误页面(比如 error/404.html)。
    • 如果没找到,就返回那个著名的 "Whitelabel Error Page"(白标错误页)。
    • 样子你肯定见过:白底黑字,写着 "This application has no explicit mapping for /error..."
模式 B:客户端访问 (返回 JSON)
  • 判断依据 :请求头不包含 text/html(比如 Postman, Ajax, 安卓 App)。

  • 对应方法error()

  • 结果 :返回一个标准的 JSON 对象。

    json 复制代码
    {
        "timestamp": "2023-12-04T12:00:00.000+00:00",
        "status": 500,
        "error": "Internal Server Error",
        "message": "/ by zero",
        "path": "/api/demo"
    }

3. 数据来源:DefaultErrorAttributes

你可能会问:"返回的 JSON 里那些 timestamp, status, message 字段是从哪来的?"

这是由另一个组件 DefaultErrorAttributes 负责收集的。它会从 Request 中提取所有的错误信息,封装成一个 Map 给 BasicErrorController 使用。

如果你想在这个默认的 JSON 里增加 一个字段(比如 version: "v1.0"),或者隐藏异常堆栈,你可以继承这个类并重写相关方法。


4. 如何自定义?(给兜底方案换个皮肤)

虽然 Spring Boot 有兜底,但那个"白标页面"太丑了,JSON 格式可能也不符合你们公司的规范。你可以通过以下方式定制:

方式一:自定义错误页面(最常用)

你只需要在 src/main/resources/templates/static/ 下创建一个 error 文件夹,然后放入对应状态码的 HTML 文件:

  • error/404.html:专门展示 404 错误。
  • error/500.html:专门展示 500 错误。
  • error/4xx.html:展示所有 4 开头的错误。

Spring Boot 扫到这些文件,就会自动用它们替换掉那个丑陋的白页。

方式二:完全替换兜底逻辑(高阶)

如果你觉得 BasicErrorController 逻辑不够用,你可以实现 ErrorController 接口,重写 /error 的映射逻辑。但这种情况很少见,因为通常我们用 Spring MVC 的 @ControllerAdvice 就够了。


相关推荐
用户908324602733 小时前
Spring AI 1.1.2 + Neo4j:用知识图谱增强 RAG 检索(上篇:图谱构建)
java·spring boot
用户8307196840821 天前
Spring Boot 集成 RabbitMQ :8 个最佳实践,杜绝消息丢失与队列阻塞
spring boot·后端·rabbitmq
Java水解1 天前
Spring Boot 视图层与模板引擎
spring boot·后端
Java水解1 天前
一文搞懂 Spring Boot 默认数据库连接池 HikariCP
spring boot·后端
洋洋技术笔记1 天前
Spring Boot Web MVC配置详解
spring boot·后端
初次攀爬者2 天前
Kafka 基础介绍
spring boot·kafka·消息队列
用户8307196840822 天前
spring ai alibaba + nacos +mcp 实现mcp服务负载均衡调用实战
spring boot·spring·mcp
Java水解2 天前
SpringBoot3全栈开发实战:从入门到精通的完整指南
spring boot·后端
初次攀爬者3 天前
RocketMQ在Spring Boot上的基础使用
java·spring boot·rocketmq
花花无缺3 天前
搞懂@Autowired 与@Resuorce
java·spring boot·后端