從拋出異常到返回JSON/XML:SpringBoot?異常處理全鏈路深度解析
前言
在 Web 開發(fā)中,異常處理是不可避免的一環(huán)。初學者往往喜歡在 Service 或 Controller 層寫大量的 try-catch 代碼,最后返回一個 Result 對象。這種做法雖然直觀,但會導致業(yè)務代碼與錯誤處理邏輯嚴重耦合,代碼極其臃腫。
Spring Boot (基于 Spring MVC) 提供了一套優(yōu)雅的、解耦的異常處理機制。本文將帶你深入底層,探究當一個異常被拋出后,究竟經歷了怎樣的奇幻漂流,又是如何根據前端需求自動變成 JSON 或 XML 的。
沒問題,為了讓你的博客內容足夠硬核且具有實戰(zhàn)參考價值,我將這個異常處理流程進行了大幅度的擴充。
這次我們不再只停留在表面,而是結合“源碼級”的執(zhí)行步驟和完整的代碼示例,把整個過程拆解得清清楚楚。
你可以直接使用以下內容作為博客的核心章節(jié)。
Spring Boot 異常處理全鏈路深度解析
很多同學只會用 @ControllerAdvice,卻不知道當一個異常拋出后,Spring Boot 內部到底發(fā)生了什么。下面我們通過一個真實的業(yè)務場景,配合源碼視角,還原異常的“一生”。
1. 場景準備:案發(fā)現(xiàn)場
首先,我們需要構建一個標準的異常拋出場景。
1.1 定義標準響應體 (Result)
這是企業(yè)級開發(fā)的標配,前后端統(tǒng)一契約。
@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不能為負數(shù)");
}
return new Result(); // 正常邏輯
}
}1.4 編寫全局異常處理器 (救援隊)
@RestControllerAdvice // 相當于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
@ExceptionHandler(OrderNotFoundException.class)
public Result handleOrderException(OrderNotFoundException e) {
// 捕獲異常,并“捏造”一個優(yōu)雅的 Result 返回
return Result.fail(404, e.getMessage());
}
}2. 深度解析:異常處理的七步“奇幻漂流”
當用戶請求 GET /order/-1 時,后臺發(fā)生了如下精密的操作:
第一步:異常冒泡 (JVM 層面)
Controller 的 getOrder 方法執(zhí)行到 throw 語句。此時,當前方法棧幀被銷毀,Controller 徹底“掛了”。異常對象開始沿著調用棧向上冒泡。
第二步:DispatcherServlet 捕獲 (總指揮接管)
異常冒泡到了 Spring MVC 的最外層——DispatcherServlet.doDispatch() 方法。這里有一個巨大的 try-catch 塊(源碼簡化版):
// DispatcherServlet.java
try {
// 嘗試執(zhí)行 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 注解匹配這個異常。”
第四步:反射調用 (執(zhí)行救援邏輯)
ExceptionHandlerExceptionResolver 通過 Java 反射機制,調用我們寫的 handleOrderException(e) 方法。
- 輸入:剛才捕獲的異常對象
e。 - 執(zhí)行:運行我們的代碼
return Result.fail(404, ...)。 - 輸出:拿到一個
Result對象。
第五步:處理返回值 (HandlerMethodReturnValueHandler)
框架拿到 Result 對象后,并不會直接發(fā)給前端。它發(fā)現(xiàn)異常處理類上標記了 @RestControllerAdvice (含 @ResponseBody)。
于是,它將任務移交給 RequestResponseBodyMethodProcessor。
- 這個組件既負責處理
@RequestBody(讀),也負責處理@ResponseBody(寫)。
第六步:內容協(xié)商 (Content Negotiation)
RequestResponseBodyMethodProcessor 開始決定用什么格式返回數(shù)據。
- 查看數(shù)據:返回值是
Result對象。 - 查看需求:檢查 HTTP 請求頭
Accept。- 如果是瀏覽器默認請求,通常包含
*/*。 - 如果是 Postman/Ajax,可能是
application/json。 - 如果是舊系統(tǒng)調用,可能是
application/xml。
- 如果是瀏覽器默認請求,通常包含
- 匹配轉換器:遍歷
HttpMessageConverter列表。- Jackson 說:“我是處理 JSON 的,我可以把
Result對象轉成 JSON 字符串。”
- Jackson 說:“我是處理 JSON 的,我可以把
第七步:序列化與寫入 (Write Response)
Jackson 轉換器開始工作:
- 將
Result對象序列化為 JSON 字符串:{"code":404, "msg":"訂單ID不能為負數(shù)", "data":null}。 - 獲取
HttpServletResponse輸出流。 - 設置
Content-Type: application/json。 - 將字符串寫入流中,發(fā)送給客戶端。
3. 總結圖
[Controller 拋出異常]
?
[JVM 冒泡]
?
[DispatcherServlet 捕獲 (catch)]
?
[尋找異常解析器 (Resolver)]
?
[反射調用 @ExceptionHandler 方法] --> 生成 Result 對象
?
[檢測 @ResponseBody 注解]
?
[內容協(xié)商 (檢查 Accept 頭)]
?
[匹配 HttpMessageConverter (如 Jackson)]
?
[序列化 (Result -> JSON/XML)]
?
[寫入 HttpServletResponse]
?
[前端收到報錯]深度解密:異常處理中的“內容協(xié)商”
很多開發(fā)者認為內容協(xié)商(Content Negotiation)只在正常的 Controller 請求中生效,其實不然。異常處理返回的結果,同樣完美支持內容協(xié)商。
1. 原理分析
無論是 Controller 的正常返回,還是 @ExceptionHandler 的異常返回,只要涉及 “對象轉 HTTP Body”,Spring MVC 底層都交給同一個處理器:RequestResponseBodyMethodProcessor。
它會執(zhí)行標準的“談判”流程:
- 看貨:拿到返回值對象(
Result)。 - 看客戶需求:讀取 HTTP 請求頭中的
Accept字段(例如application/json或application/xml)。 - 找翻譯官:遍歷容器中所有的
HttpMessageConverter。 - 執(zhí)行轉換:找到能同時匹配“對象類型”和“客戶需求”的轉換器,執(zhí)行序列化。
2. 場景演示
假設我們引入了 jackson-dataformat-xml 依賴,Spring Boot 會自動注冊 XML 轉換器。
- 場景 A:前端是 Vue/React (默認)
- 請求頭:
Accept: application/json - 響應:
{ "code": 500, "msg": "系統(tǒng)繁忙", "data": null }- 場景 B:前端是舊系統(tǒng) (指定 XML)
- 請求頭:
Accept: application/xml - 響應:
<Result>
<code>500</code>
<msg>系統(tǒng)繁忙</msg>
<data/>
</Result>結論:我們不需要修改一行 Java 代碼,異常信息就能自動適應前端需要的格式。
Spring MVC vs Spring Boot:內容協(xié)商誰在干活?
在這個過程中,我們需要理清兩者的分工:
- Spring MVC(機制提供者):
- 提供了
DispatcherServlet捕獲異常的機制。 - 提供了
@ControllerAdvice和@ExceptionHandler注解。 - 提供了內容協(xié)商管理器 (
ContentNegotiationManager) 和消息轉換器接口 (HttpMessageConverter)。
- 提供了
- 它是“發(fā)動機”。
- Spring Boot(自動化配置):
- 自動配置了
ErrorMvcAutoConfiguration(提供兜底的 /error 路徑)。 - 自動識別 Classpath 下的 Jackson 包,并注冊了 JSON 和 XML 的轉換器。
- 自動配置了
- 它是“裝配工”,讓你開箱即用。
SpringBoot的默認異常處理方案
Spring Boot 的錯誤處理方案,核心就是一個詞:“自動兜底”。
它的官方學名叫做 “默認全局錯誤處理機制”。即使你一行異常處理代碼都不寫,Spring Boot 也能保證你的應用在報錯時,不會直接把服務器炸了,或者給用戶看一堆亂碼,而是返回一個“雖然丑但結構清晰”的錯誤響應。
這個方案的核心由 1 個 Controller、2 種響應模式 和 1 個頁面 組成。
1. 核心組件:BasicErrorController
這是 Spring Boot 自動配置 (ErrorMvcAutoConfiguration) 幫你創(chuàng)建的一個特殊的 Controller。
- 它的地位:和你的
OrderController、UserController平級,都是處理 HTTP 請求的。 - 它的地盤:默認監(jiān)聽
/error路徑。 - 工作原理:
- 當應用中發(fā)生異常(且沒被 Spring MVC 攔截),或者訪問了不存在的路徑(404)。
- Servlet 容器(Tomcat)會捕捉到錯誤。
- Tomcat 發(fā)現(xiàn)你沒有配置專門的錯誤頁,于是根據 Spring Boot 的約定,把請求轉發(fā) (Forward) 到
/error路徑。 BasicErrorController收到請求,開始干活。
2. 智能響應:看人下菜碟(內容協(xié)商)
BasicErrorController 非常智能,它會根據**“誰在訪問”**(檢查 HTTP 請求頭 Accept),決定返回什么格式的數(shù)據。它內部定義了兩個處理方法:
模式 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. 數(shù)據來源:DefaultErrorAttributes
你可能會問:“返回的 JSON 里那些 timestamp, status, message 字段是從哪來的?”
這是由另一個組件 DefaultErrorAttributes 負責收集的。它會從 Request 中提取所有的錯誤信息,封裝成一個 Map 給 BasicErrorController 使用。
如果你想在這個默認的 JSON 里增加一個字段(比如 version: "v1.0"),或者隱藏異常堆棧,你可以繼承這個類并重寫相關方法。
4. 如何自定義?(給兜底方案換個皮膚)
雖然 Spring Boot 有兜底,但那個“白標頁面”太丑了,JSON 格式可能也不符合你們公司的規(guī)范。你可以通過以下方式定制:
方式一:自定義錯誤頁面(最常用)
你只需要在 src/main/resources/templates/ 或 static/ 下創(chuàng)建一個 error 文件夾,然后放入對應狀態(tài)碼的 HTML 文件:
error/404.html:專門展示 404 錯誤。error/500.html:專門展示 500 錯誤。error/4xx.html:展示所有 4 開頭的錯誤。
Spring Boot 掃到這些文件,就會自動用它們替換掉那個丑陋的白頁。
方式二:完全替換兜底邏輯(高階)
如果你覺得 BasicErrorController 邏輯不夠用,你可以實現(xiàn) ErrorController 接口,重寫 /error 的映射邏輯。但這種情況很少見,因為通常我們用 Spring MVC 的 @ControllerAdvice 就夠了。
到此這篇關于從拋出異常到返回JSON/XML:SpringBoot 異常處理全鏈路深度解析的文章就介紹到這了,更多相關springboot異常處理內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
詳解SpringBoot集成Redis來實現(xiàn)緩存技術方案
本篇文章主要介紹了詳解SpringBoot集成Redis來實現(xiàn)緩存技術方案,具有一定的參考價值,有興趣的可以了解一下2017-06-06
Spring運行環(huán)境Environment的解析
本文主要介紹了Spring運行環(huán)境Environment的解析,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-08-08
使用JPA+querydsl如何實現(xiàn)多條件動態(tài)查詢
這篇文章主要介紹了使用JPA+querydsl如何實現(xiàn)多條件動態(tài)查詢,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03

