前言
在 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 开始决定用什么格式返回数据。
- 查看数据:返回值是
result对象。 - 查看需求:检查 http 请求头
accept。- 如果是浏览器默认请求,通常包含
*/*。 - 如果是 postman/ajax,可能是
application/json。 - 如果是旧系统调用,可能是
application/xml。
- 如果是浏览器默认请求,通常包含
- 匹配转换器:遍历
httpmessageconverter列表。- jackson 说:“我是处理 json 的,我可以把
result对象转成 json 字符串。”
- jackson 说:“我是处理 json 的,我可以把
第七步:序列化与写入 (write response)
jackson 转换器开始工作:
- 将
result对象序列化为 json 字符串:{"code":404, "msg":"订单id不能为负数", "data":null}。 - 获取
httpservletresponse输出流。 - 设置
content-type: application/json。 - 将字符串写入流中,发送给客户端。
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。
它会执行标准的“谈判”流程:
- 看货:拿到返回值对象(
result)。 - 看客户需求:读取 http 请求头中的
accept字段(例如application/json或application/xml)。 - 找翻译官:遍历容器中所有的
httpmessageconverter。 - 执行转换:找到能同时匹配“对象类型”和“客户需求”的转换器,执行序列化。
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 个 controller、2 种响应模式 和 1 个页面 组成。
1. 核心组件:basicerrorcontroller
这是 spring boot 自动配置 (errormvcautoconfiguration) 帮你创建的一个特殊的 controller。
- 它的地位:和你的
ordercontroller、usercontroller平级,都是处理 http 请求的。 - 它的地盘:默认监听
/error路径。 - 工作原理:
- 当应用中发生异常(且没被 spring mvc 拦截),或者访问了不存在的路径(404)。
- servlet 容器(tomcat)会捕捉到错误。
- tomcat 发现你没有配置专门的错误页,于是根据 spring boot 的约定,把请求转发 (forward) 到
/error路径。 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异常处理内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论