如何使用Spring優雅地處理REST異常

1. 概覽java

本文將舉例說明如何使用Spring來實現REST API的異常處理。咱們將同時考慮Spring 3.2和4.x推薦的解決方案,同時也會考慮之前的解決方案。json

在Spring 3.2以前,Spring MVC應用程序中處理異常的兩種主要方式是:HandlerExceptionResolver或註解@ExceptionHandler。這兩種方式都有明顯的缺點。安全

在3.2以後,咱們有了新的註解@ControllerAdvice來解決前兩個解決方案的侷限性。服務器

全部這些都有一個共同點——它們很好地處理了關注點分離。應用程序能夠像往常同樣拋出異常以表示某種類型的故障——這些異常將被單獨處理。架構

2. 解決方案 1 – 控制器做用域的註解 @ExceptionHandlerapp

第一個解決方案是在@Controller做用域有效——咱們將定義一個處理異常的方法,並給這個方法添加@ExceptionHandler註解:ide

public class FooController{ui

//...編碼

@ExceptionHandler({ CustomException1.class, CustomException2.class})url

public void handleException() {

//

}

}

這種方法有一個很大的缺陷 ——添加了@ExceptionHandler註解的方法只針對特定的控制器,而不是全局的整個應用程序。固然,在每一個控制器中都添加@ExceptionHandler 註解的辦法使它沒法很好的適應常規的異常處理機制。

@ExceptionHandler在做用域方面的缺陷一般是經過讓全部控制器都擴展一個控制器基類的方式來解決——然而,對於應用程序來講,這多是一個問題,由於無論出於什麼緣由,總有一些控制器不能從這個基控制器擴展。例如,這些控制器可能不能直接修改,或者一些控制器可能已經從別的基類擴展,而這個基類可能在另外一個jar中或者不能直接修改。

接下來,咱們將討論另外一種解決異常處理問題的方法——一種全局的、不包括對現有組件的任何更改。

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)

public class ResourceNotFoundException extends RuntimeException {

public ResourceNotFoundException() {

super();

}

public ResourceNotFoundException(String message, Throwable cause) {

super(message, cause);

}

public ResourceNotFoundException(String message) {

super(message);

}

public ResourceNotFoundException(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

public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

@Override

protected ModelAndView doResolveException

(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

try {

if (ex instanceof IllegalArgumentException) {

return handleIllegalArgument((IllegalArgumentException) ex, response, handler);

}

...

} catch (Exception handlerException) {

logger.warn("Handling of [" + ex.getClass().getName() + "]

resulted in Exception", handlerException);

}

return null;

}

private ModelAndView handleIllegalArgument

(IllegalArgumentException ex, HttpServletResponse response) throws IOException {

response.sendError(HttpServletResponse.SC_CONFLICT);

String accept = request.getHeader(HttpHeaders.ACCEPT);

...

return new ModelAndView();

}

}

這裏須要注意的一個細節是請求自己是可用的,所以應用程序能夠考慮由客戶端發送的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

public class RestResponseEntityExceptionHandler

extends ResponseEntityExceptionHandler {

@ExceptionHandler(value

= { IllegalArgumentException.class, IllegalStateException.class })

protected ResponseEntity<Object> handleConflict(

RuntimeException ex, WebRequest request) {

String bodyOfResponse = "This should be application specific";

return handleExceptionInternal(ex, bodyOfResponse,

new HttpHeaders(), HttpStatus.CONFLICT, request);

}

}

新的@ControllerAdvice註解容許把之前多個分散的@ExceptionHandler合併到一個單一的、全局的錯誤處理組件中。

實際的機制很是簡單,但也很是靈活:

● 它容許對響應體和HTTP狀態碼進行徹底控制

● 它容許將幾個異常映射到相同的方法,以便一塊兒處理

● 它充分利用了新的REST風格的 ResposeEntity響應這裏要特別注意一個細節,@ExceptionHandler聲明的異常類要與其修飾方法的參數類型相匹配。若是這兩個地方不匹配,編譯器將不會提示——它沒有理由去提示,Spring也不會提示。

然而,當異常在運行時被拋出時,異常解析機制將會失敗:

  1. java.lang.IllegalStateException: No suitable resolver for argument [0] [type=...]

  2. HandlerMethod details: ...

5. 處理Spring Security中拒絕訪問

當一個通過身份認證的用戶試圖訪問他沒有足夠權限訪問的資源時,就會出現拒絕訪問。

5.1. MVC – 自定義錯誤頁

首先,讓咱們看一下MVC風格的解決方案,看看如何定製一個拒絕訪問的錯誤頁面:

使用XML配置:

<http>

<intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>

...

<access-denied-handler error-page="/my-error-page" />

</http>

使用Java配置:

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()

.antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")

...

.and()

.exceptionHandling().accessDeniedPage("/my-error-page");

}

當用戶試圖訪問資源但沒有足夠的權限時,它們將被重定向到「/my-error-page「。

5.2. 自定義AccessDeniedHandler

接下來,讓咱們看看如何編寫自定義AccessDeniedHandler:

@Component

public class CustomAccessDeniedHandler implements AccessDeniedHandler {

@Override

public void handle

(HttpServletRequest request, HttpServletResponse response, AccessDeniedException ex)

throws IOException, ServletException {

response.sendRedirect("/my-error-page");

}

}

如今讓咱們使用XML配置進行配置:

<http>

<intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/>

...

<access-denied-handler ref="customAccessDeniedHandler" />

</http>

或者使用Java配置:

@Autowired

private CustomAccessDeniedHandler accessDeniedHandler;

@Override

protected void configure(HttpSecurity http) throws Exception {

http.authorizeRequests()

.antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN")

...

.and()

.exceptionHandling().accessDeniedHandler(accessDeniedHandler)

}

請注意,在咱們的 CustomAccessDeniedHandler中,咱們能夠經過重定向或顯示一條自定義錯誤信息的方式來定製響應。

5.3. REST和方法級的安全性

最後,讓咱們看看如何處理方法級的安全性註解@PreAuthorize、@PostAuthorize和@Secure引起的拒絕訪問。

固然,咱們將使用以前討論過的全局異常處理機制來處理新的AccessDeniedException :

@ControllerAdvice

public class RestResponseEntityExceptionHandler

extends ResponseEntityExceptionHandler {

@ExceptionHandler({ AccessDeniedException.class })

public ResponseEntity<Object> handleAccessDeniedException(

Exception ex, WebRequest request) {

return new ResponseEntity<Object>(

"Access denied message here", new HttpHeaders(), HttpStatus.FORBIDDEN);

}

...

}

寫在最後:

碼字不易看到最後了,那就點個關注唄,只收藏不點關注的都是在耍流氓! 關注並私信我「架構」,免費送一些Java架構資料,先到先得!

相關文章
相關標籤/搜索