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

 更新时间:2026年02月02日 11:21:25   作者:czlczl20020925  
Spring Boot (基于 Spring MVC) 提供了一套优雅的、解耦的异常处理机制,本文将带你深入底层,探究当一个异常被抛出后,究竟经历了怎样的奇幻漂流,又是如何根据前端需求自动变成 JSON 或 XML 的,感兴趣的朋友跟随小编一起看看吧

前言

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

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

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

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

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

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

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

1. 场景准备:案发现场

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

1.1 定义标准响应体 (Result)

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

@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)

public class OrderNotFoundException extends RuntimeException {
    public OrderNotFoundException(String message) {
        super(message);
    }
}

1.3 编写 Controller (肇事者)

@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 编写全局异常处理器 (救援队)

@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 块(源码简化版):

// 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. 总结图

[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
  • 响应:
{ "code": 500, "msg": "系统繁忙", "data": null }
  • 场景 B:前端是旧系统 (指定 XML)
  • 请求头:Accept: application/xml
  • 响应:
<Result>
    <code>500</code>
    <msg>系统繁忙</msg>
    <data/>
</Result>

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

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

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

  • Spring MVC(机制提供者)
    • 提供了 DispatcherServlet 捕获异常的机制。
    • 提供了 @ControllerAdvice@ExceptionHandler 注解。
    • 提供了内容协商管理器 (ContentNegotiationManager) 和消息转换器接口 (HttpMessageConverter)。
  • 它是“发动机”。
  • 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 对象。
    {
        "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 就够了。

到此这篇关于从抛出异常到返回JSON/XML:SpringBoot 异常处理全链路深度解析的文章就介绍到这了,更多相关springboot异常处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解SpringBoot集成Redis来实现缓存技术方案

    详解SpringBoot集成Redis来实现缓存技术方案

    本篇文章主要介绍了详解SpringBoot集成Redis来实现缓存技术方案,具有一定的参考价值,有兴趣的可以了解一下
    2017-06-06
  • Java如何高效实现Word文档对比

    Java如何高效实现Word文档对比

    在项目协作、文档审核或版本迭代的快节奏工作中,你是否曾为Word文档的细微修改而抓狂,下面我们就来一起探讨如何利用Java进行Word文档的自动化比较吧
    2025-11-11
  • Spring运行环境Environment的解析

    Spring运行环境Environment的解析

    本文主要介绍了Spring运行环境Environment的解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-08-08
  • Java内存模型中的虚拟机栈原理分析

    Java内存模型中的虚拟机栈原理分析

    这篇文章主要介绍了Java内存模型中的虚拟机栈原理分析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • IDEA中设置Run Dashboard方式

    IDEA中设置Run Dashboard方式

    这篇文章主要介绍了IDEA中设置Run Dashboard方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-08-08
  • 使用JPA+querydsl如何实现多条件动态查询

    使用JPA+querydsl如何实现多条件动态查询

    这篇文章主要介绍了使用JPA+querydsl如何实现多条件动态查询,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-03-03
  • 解决spring项目找不到Aspect依赖注解的问题

    解决spring项目找不到Aspect依赖注解的问题

    这篇文章主要介绍了解决spring项目找不到Aspect依赖注解的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java基础之八大排序算法

    Java基础之八大排序算法

    这篇文章主要介绍了Java基础之八大排序算法,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • Java连接WebSocket的实现教程

    Java连接WebSocket的实现教程

    这篇文章主要介绍了Java连接WebSocket的实现教程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • Java链接redis_动力节点Java学院整理

    Java链接redis_动力节点Java学院整理

    这篇文章主要介绍了Java链接redis,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-08-08

最新评论