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


相关推荐
Kiyra1 小时前
Spring Boot Starter 自定义开发:封装中间件配置
spring boot·redis·后端·缓存·中间件·性能优化·rocketmq
码界奇点2 小时前
基于Spring Boot和微信小程序的小程序商城系统设计与实现
spring boot·微信小程序·小程序·毕业设计·源代码管理
+VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue英语学习系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
伯明翰java2 小时前
【无标题】springboot项目yml中使用中文注释报错的解决方法
java·spring boot·后端
码界奇点3 小时前
基于Spring Boot和Vue.js的视频点播管理系统设计与实现
java·vue.js·spring boot·后端·spring·毕业设计·源代码管理
廋到被风吹走3 小时前
【Spring】Spring Boot详细介绍
java·spring boot·spring
czlczl200209253 小时前
基于 Spring Boot 权限管理 RBAC 模型
前端·javascript·spring boot
计算机毕设指导63 小时前
基于微信小程序的智慧社区娱乐服务管理系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·娱乐
赵得C3 小时前
Spring Boot+MyBatis:用 PageHelper 实现 Oracle 12c 的 OFFSET 分页
spring boot·oracle·mybatis