从抛出异常到返回 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 就够了。


相关推荐
Dolphin_Home23 分钟前
接口字段入参出参分离技巧:从注解到DTO分层实践
java·spring boot·后端
wtsolutions25 分钟前
WPS另存为JSON,WPS导出JSON, WPS表格转换成JSON : Excel to JSON WPS插件使用指南
json·excel·wps·插件·加载项·wtsolutions
Q_Q51100828526 分钟前
python+django/flask+vue的基于文学创作的社交论坛系统
spring boot·python·django·flask·node.js·php
Q_Q51100828532 分钟前
python+django/flask网红酒店预定系统
spring boot·python·django·flask·node.js·php
凌波粒1 小时前
Springboot基础教程(6)--整合JDBC/Druid数据源/Mybatis
spring boot·后端·mybatis
计算机毕设指导61 小时前
基于Springboot+微信小程序流浪动物救助管理系统【源码文末联系】
java·spring boot·后端·spring·微信小程序·tomcat·maven
昊昊该干饭了1 小时前
Spring Boot 从接口设计到业务编排
java·spring boot·后端
Q_Q5110082851 小时前
python+django/flask+vue的高考志愿咨询系统
spring boot·python·django·flask·node.js·php