如何使用Spring優雅地處理REST異常?
原文連結:https://www.baeldung.com/exception-handling-for-rest-with-spring
作者: Eugen Paraschiv
譯者: helloworldtang
目錄
1. 概覽
2. 使用控制器作用域的註解 @ExceptionHandler
3. 使用 HandlerExceptionResolver
4. 使用新註解 @ControllerAdvice (Spring 3.2及以上版本)
5. 處理Spring Security中的拒絕訪問
6. 總結
1. 概覽
本文將舉例說明如何使用Spring來實現REST API的異常處理。我們將同時考慮Spring 3.2和4.x推薦的解決方案,同時也會考慮以前的解決方案。
在Spring 3.2之前,Spring MVC應用程式中處理異常的兩種主要方式是:HandlerExceptionResolver或註解@ExceptionHandler。這兩種方式都有明顯的缺點。
在3.2之後,我們有了新的註解@ControllerAdvice來解決前兩個解決方案的侷限性。
所有這些都有一個共同點——它們很好地處理了關注點分離。應用程式可以像往常一樣丟擲異常以表示某種型別的故障——這些異常將被單獨處理。
2. 解決方案 1 – 控制器作用域的註解 @ExceptionHandler
第一個解決方案是在@Controller作用域有效——我們將定義一個處理異常的方法,並給這個方法新增@ExceptionHandler註解:
publicclassFooController{
//...
@ExceptionHandler({ CustomException1.class, CustomException2.class})
publicvoid handleException() {
//
}
}
這種方法有一個很大的缺陷 ——添加了@ExceptionHandler註解的方法只針對特定的控制器,而不是全域性的整個應用程式。當然,在每個控制器中都新增@ExceptionHandler 註解的辦法使它無法很好的適應常規的異常處理機制。
@ExceptionHandler在作用域方面的缺陷通常是通過讓所有控制器都擴充套件一個控制器基類的方式來解決
接下來,我們將討論另一種解決異常處理問題的方法——一種全域性的、不包括對現有元件的任何更改。
3. 解決方案 2 – HandlerExceptionResolver
第二個解決方案是定義一個 HandlerExceptionResolver——它將處理應用程式丟擲的任何異常。它還允許我們在REST API中實現統一的異常處理機制。
在使用自定義解析器之前,讓我們回顧一下現有的異常解析器。
3.1. ExceptionHandlerExceptionResolver
這個解析器在Spring 3.1中引入,並且在 DispatcherServlet中是預設啟用的。它實際上是前面介紹的@ExceptionHandler機制的核心組成部分。
3.2. DefaultHandlerExceptionResolver
DefaultHandlerExceptionResolver是在Spring 3.0中引入的,並且在DispatcherServlet中是預設啟用的。它用於將Spring中的標準異常解析為對應的HTTP狀態碼,即客戶端錯誤——4xx和伺服器錯誤——5xx狀態碼。這是Spring異常的完整列表,以及這些異常對應的HTTP狀態碼。
雖然它確實正確地設定了響應的狀態碼,但有一個缺陷是它不會改變響應體。對於REST API來說,狀態碼實際上並沒有足夠的資訊顯示給客戶端——響應也必須有一個響應體,以便伺服器能夠提供更多關於故障的資訊。
這個缺陷可以通過ModelAndView配置檢視解析和渲染錯誤內容來解決,但是這個解決方案很顯然不是最理想的——這就是為什麼在Spring 3.2中提供了更好的選項——我們將在本文的後半部分討論這個問題。
3.3. ResponseStatusExceptionResolver
這個解析器也是在Spring 3.0中引入,並且在DispatcherServlet中是預設啟用的。它的主要職責是根據自定義異常上配置的註解@ResponseStatus,將這些自定義異常對映到設定的HTTP狀態碼。
通過這個方式建立的一個自定義異常可能看起來是這樣的:
@ResponseStatus(value = HttpStatus.NOT_FOUND)
publicclassResourceNotFoundExceptionextendsRuntimeException {
publicResourceNotFoundException() {
super();
}
publicResourceNotFoundException(String message, Throwable cause) {
super(message, cause);
}
publicResourceNotFoundException(String message) {
super(message);
}
publicResourceNotFoundException(Throwable cause) {
super(cause);
}
}
與DefaultHandlerExceptionResolver一樣,這個解析器在處理響應體方面是有缺陷的——它確實重新設定了響應的狀態碼,但是響應體仍然是空的。
3.4. SimpleMappingExceptionResolver和 AnnotationMethodHandlerExceptionResolver
SimpleMappingExceptionResolver 已經存在了相當長一段時間——它來自於較早的Spring MVC模型,與REST服務不太相關。它被用來對映異常類名到檢視名。
在Spring 3.0中引入了AnnotationMethodHandlerExceptionResolver,通過註解@ExceptionHandler來處理異常,但是在Spring 3.2時已經被ExceptionHandlerExceptionResolver 廢棄。
3.5. 自定義HandlerExceptionResolver
在為Spring RESTful 服務提供良好的錯誤處理機制方面,DefaultHandlerExceptionResolver和ResponseStatusExceptionResolver組合還有很長的路要走。缺陷是——正如前面提到的——無法控制響應體。
理想情況下,我們希望能夠輸出JSON或XML,這取決於客戶端請求的格式(通過Accept頭)。
這就足以建立一個新的、自定義的異常解析器:
@Component
publicclassRestResponseStatusExceptionResolverextendsAbstractHandlerExceptionResolver {
@Override
protectedModelAndView doResolveException
(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
try {
if (ex instanceofIllegalArgumentException) {
return handleIllegalArgument((IllegalArgumentException) ex, response, handler);
}
...
} catch (Exception handlerException) {
logger.warn("Handling of [" + ex.getClass().getName() + "]
resulted in Exception", handlerException);
}
returnnull;
}
privateModelAndView handleIllegalArgument
(IllegalArgumentException ex, HttpServletResponse response) throwsIOException {
response.sendError(HttpServletResponse.SC_CONFLICT);
String accept = request.getHeader(HttpHeaders.ACCEPT);
...
returnnewModelAndView();
}
}
這裡需要注意的一個細節是請求本身是可用的,因此應用程式可以考慮由客戶端傳送的Accept頭。例如,如果客戶端要求application/json ,在出現錯誤的情況下,應用程式仍然應該返回用application/json 編碼的響應體。
另一個重要的實現細節是返回一個ModelAndView ——這是響應體,它將允許應用程式設定它所需要的任何東西。
對於Spring REST服務的異常處理來說,這種方法是一種一致且易於配置的機制。但是它有一些限制:它與低層的HtttpServletResponse互動,它適合使用ModelAndView的舊MVC模型——所以仍然有改進的空間。
4. 新的解決方案 3 – 使用新的註解 @ControllerAdvice (Spring 3.2及以上版本)
Spring 3.2使用新的註解@ControllerAdvice為全域性的@ExceptionHandler提供支援。這就形成了一種脫離舊MVC模型的機制,使用ResponseEntity以及註解@ExceptionHandler的型別安全性和靈活性:
@ControllerAdvice
publicclassRestResponseEntityExceptionHandler
extendsResponseEntityExceptionHandler {
@ExceptionHandler(value
= { IllegalArgumentException.class, IllegalStateException.class })
protectedResponseEntity<Object> handleConflict(
RuntimeException ex, WebRequest request) {
String bodyOfResponse = "This should be application specific";
return handleExceptionInternal(ex, bodyOfResponse,
newHttpHeaders(), HttpStatus.CONFLICT, request);
}
}
新的@ControllerAdvice註解允許把以前多個分散的@ExceptionHandler合併到一個單一的、全域性的錯誤處理元件中。
實際的機制非常簡單,但也非常靈活:
它允許對響應體和HTTP狀態碼進行完全控制
它允許將幾個異常對映到相同的方法,以便一起處理
它充分利用了新的REST風格的 ResposeEntity響應
這裡要特別注意一個細節,@ExceptionHandler宣告的異常類要與其修飾方法的引數型別相匹配。如果這兩個地方不匹配,編譯器將不會提示——它沒有理由去提示,Spring也不會提示。
然而,當異常在執行時被丟擲時,異常解析機制將會失敗:
java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]
HandlerMethod details: ...
5. 處理Spring Security中拒絕訪問
當一個經過身份認證的使用者試圖訪問他沒有足夠許可權訪問的資源時,就會出現拒絕訪問。
5.1. MVC – 自定義錯誤頁
首先,讓我們看一下MVC風格的解決方案,看看如何定製一個拒絕訪問的錯誤頁面:
使用XML配置:
<http>
<intercept-urlpattern="/admin/*"access="hasAnyRole('ROLE_ADMIN')"/>
...
<access-denied-handlererror-page="/my-error-page"/>
</http>
使用Java配置:
@Override
protectedvoid configure(HttpSecurity http) throwsException {
http.authorizeRequests()
.antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
...
.and()
.exceptionHandling().accessDeniedPage("/my-error-page");
}
當用戶試圖訪問資源但沒有足夠的許可權時,它們將被重定向到“/my-error-page“。
5.2. 自定義AccessDeniedHandler
接下來,讓我們看看如何編寫自定義AccessDeniedHandler:
@Component
publicclassCustomAccessDeniedHandlerimplementsAccessDeniedHandler {
@Override
publicvoid handle
(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)
throwsIOException, ServletException {
response.sendRedirect("/my-error-page");
}
}
現在讓我們使用XML配置進行配置:
<http>
<intercept-urlpattern="/admin/*"access="hasAnyRole('ROLE_ADMIN')"/>
...
<access-denied-handlerref="customAccessDeniedHandler"/>
</http>
或者使用Java配置:
@Autowired
privateCustomAccessDeniedHandler accessDeniedHandler;
@Override
protectedvoid configure(HttpSecurity http) throwsException {
http.authorizeRequests()
.antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")
...
.and()