本文將演示如何使用 Spring 和 Spring Boot 中實現 REST API 的異常處理,並瞭解不一樣版本引入了哪些新功能。
在 Spring 3.2 以前,在 Spring MVC 中處理異常的兩種主要方法是:HandlerExceptionResolver 或 @ExceptionHandler 註解。 這兩種方法都有一些明顯的缺點。
從 3.2 開始,咱們就使用 @ControllerAdvice 註解來解決前兩種方案的侷限性, 並在整個應用程序中促進統一的異常處理。
如今,Spring 5 引入了 ResponseStatusException 類: REST API 中快速處理基本異常的方法。
最後,咱們將看到 Spring Boot 帶來了什麼,以及如何配置它以知足咱們的須要。java
第一個解決方案在 Controller 級別工做——咱們將定義一個處理異常的方法,並使用 @ExceptionHandler 註釋它:web
public class FooController{ //... @ExceptionHandler({ CustomException1.class, CustomException2.class }) public void handleException() { // } }
這種方法有一個主要的缺點—— @ExceptionHandler註釋方法只對特定的 Controller,而不是對整個應用程序全局。固然,將它添加到每一個控制器並不適合通常的異常處理機制。
咱們能夠經過讓全部的 Controller 擴展一個 Base Controller 來繞過這個限制 —— 可是,對於應用程序來講,不管出於什麼緣由,這均可能是一個問題。例如,控制器可能已經從另外一個基類擴展而來,這個基類可能在另外一個 jar 中,或者不能直接修改,或者它們自己不能直接修改。
接下來,咱們將研究解決異常處理問題的另外一種方法—一種全局的方法。編程
第二個解決方案是定義一個 HandlerExceptionResolver —— 它將解決應用程序拋出的任何異常。它還容許咱們在 REST API 中實現統一的異常處理機制。
在使用自定義解析器以前,讓咱們回顧一下現有的實現。json
該解析器是在 Spring 3.1 中引入的,默認狀況下在 DispatcherServlet 中啓用。 這其實是前面介紹的 @ExceptionHandler 機制如何工做的核心組件。瀏覽器
這個解析器是在 Spring 3.0 中引入的,在 DispatcherServlet 中默認啓用它。它用於解決相應 HTTP 狀態碼的標準 Spring 異常,即客戶端異常 - 4xx 和服務器異常 - 5xx 狀態碼。下面是它處理的 Spring 異常的完整列表:安全
Exception | HTTP Status Code |
---|---|
BindException | 400 (Bad Request) |
ConversionNotSupportedException | 500 (Internal Server Error) |
HttpMediaTypeNotAcceptableException | 406 (Not Acceptable) |
HttpMediaTypeNotSupportedException | 415 (Unsupported Media Type) |
HttpMessageNotReadableException | 400 (Bad Request) |
HttpMessageNotWritableException | 500 (Internal Server Error) |
HttpRequestMethodNotSupportedException | 405 (Method Not Allowed) |
MethodArgumentNotValidException | 400 (Bad Request) |
MissingServletRequestParameterException | 400 (Bad Request) |
MissingServletRequestPartException | 400 (Bad Request) |
NoSuchRequestHandlingMethodException | 404 (Not Found) |
TypeMismatchException | 400 (Bad Request) |
雖然它確實正確地設置了響應的狀態代碼,但有一個限制是它沒有爲響應的主體設置任何內容。對於 REST API —— 狀態代碼實際上並無足夠的信息提供給客戶端 —— 響應也必須有一個主體,以容許應用程序提供關於故障的附加信息。
這能夠經過配置視圖解析和經過 ModelAndView 呈現異常內容來解決,可是這個解決方案顯然不是最優的。這就是爲何 Spring 3.2 引入了一個更好的選項,咱們將在下面討論。服務器
這個解析器也是在 Spring 3.0 中引入的,並在 DispatcherServlet 中默認啓用。它的主要職責是使用自定義異常上可用的 @ResponseStatus 註解,並將這些異常映射到HTTP狀態碼。
這樣的自定義異常以下所示:微信
@ResponseStatus(value = HttpStatus.NOT_FOUND) public class MyResourceNotFoundException extends RuntimeException { public MyResourceNotFoundException() { super(); } public MyResourceNotFoundException(String message, Throwable cause) { super(message, cause); } public MyResourceNotFoundException(String message) { super(message); } public MyResourceNotFoundException(Throwable cause) { super(cause); } }
與 DefaultHandlerExceptionResolver 同樣,這個解析器在處理響應主體的方式上是有限的——它確實將狀態代碼映射到響應上,可是響應的主體仍然是空的。app
SimpleMappingExceptionResolver 已經出現了很長一段時間了——它來自於舊的 Spring MVC 模型,而且與 REST 服務無關。咱們基本上使用它來映射異常類名來查看名稱。
在 Spring 3.0 中引入了 AnnotationMethodHandlerExceptionResolver 來經過 @ExceptionHandler 註釋處理異常,可是從 Spring 3.2 開始, ExceptionHandlerExceptionResolver 就再也不支持它了。ide
DefaultHandlerExceptionResolver 和 ResponseStatusExceptionResolver 的組合在爲 Spring RESTful 服務提供良好的異常處理機制方面走了很長的路。缺點是 —— 如前所述同樣沒法控制響應的主體。
這種方法是 Spring REST 服務異常處理的一致且易於配置的機制。可是,它確實有侷限性:它與低級別的 HtttpServletResponse 交互,而且它適合使用 ModelAndView 的舊 MVC 模型 —— 因此仍然有改進的空間。
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 註解容許咱們將以前分散的多個 @ExceptionHandlers 合併爲一個全局異常處理組件。
實際的機制很是簡單,並且很是靈活。它給咱們帶來了:
Spring 5 引入了 ResponseStatusException 類。 咱們能夠建立一個提供 HttpStatus 以及可能的緣由:
@GetMapping(value = "/{id}") public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) { try { Foo resourceById = RestPreconditions.checkFound(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrievedEvent(this, response)); return resourceById; } catch (MyResourceNotFoundException exc) { throw new ResponseStatusException( HttpStatus.NOT_FOUND, "Foo Not Found", exc); } }
使用 ResponseStatusException 有什麼好處?
那麼如何權衡呢?
咱們還應該注意到,能夠在一個應用程序中組合不一樣的方法。
例如,咱們能夠全局實現 @ControllerAdvice,但也能夠在本地實現 responsestatusexception。然而,咱們須要當心:若是同一個異常能夠用多種方式處理,咱們可能會注意到一些使人驚訝的行爲。一種可能的約定始終以一種方式處理着一種特定類型的異常。
當經過身份驗證的用戶嘗試訪問他沒有足夠權限訪問的資源時,將發生拒絕訪問。
首先,讓咱們看一下該解決方案的 MVC 風格,看看如何爲 Access Denied 自定義異常頁面:
<http> <intercept-url pattern="/admin/*" access="hasAnyRole('ROLE_ADMIN')"/> ... <access-denied-handler error-page="/my-error-page" /> </http>
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/*").hasAnyRole("ROLE_ADMIN") ... .and() .exceptionHandling().accessDeniedPage("/my-error-page"); }
當用戶試圖在沒有足夠權限的狀況下訪問資源時,他們將被重定向到 「/my-error-page」。
接下來,讓咱們看看如何編寫咱們的自定義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 中,咱們能夠按照本身的意願經過重定向或顯示自定義異常消息來定製響應。
Spring Boot 提供了一個 ErrorController 實現,以合理的方式處理異常。
簡而言之,它爲瀏覽器提供一個後備異常頁面(又稱 Whitelabel 異常頁面),併爲 RESTful 非 HTML 請求提供了一個 JSON 響應:
{ "timestamp": "2019-01-17T16:12:45.977+0000", "status": 500, "error": "Internal Server Error", "message": "Error processing the request!", "path": "/my-endpoint-with-exceptions" }
和往常同樣,Spring Boot 容許使用 properties 配置如下功能:
除了這些屬性以外,咱們還能夠爲 /error 提供本身的視圖,覆蓋Whitelabel頁面。
咱們還能夠經過在上下文中包含 ErrorAttributes bean 來定製但願在響應中顯示的屬性。咱們能夠擴展由 Spring Boot 提供的 DefaultErrorAttributes 類,使事情變得更簡單:
@Component public class MyCustomErrorAttributes extends DefaultErrorAttributes { @Override public Map<String, Object> getErrorAttributes( WebRequest webRequest, boolean includeStackTrace) { Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); errorAttributes.put("locale", webRequest.getLocale() .toString()); errorAttributes.remove("error"); //... return errorAttributes; } }
若是咱們想進一步定義(或覆蓋)應用程序如何處理特定內容類型的異常,則能夠註冊一個 ErrorController bean。
一樣,咱們能夠利用 Spring Boot 提供的默認 BasicErrorController 來幫助咱們。
例如,假設咱們但願自定義應用程序如何處理 XML 中觸發的異常。咱們所要作的就是使用 @RequestMapping 定義一個公共方法,並聲明它產生了 application/xml 類型:
@Component public class MyErrorController extends BasicErrorController { public MyErrorController(ErrorAttributes errorAttributes) { super(errorAttributes, new ErrorProperties()); } @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE) public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request) { // ... } }
本文討論了幾個 Spring 版本中的爲 REST API 實現異常處理機制的方法,從較舊的機制開始,一直到 Spring 3.2,一直延伸到 4.x 和 5.x。
歡迎關注個人微信公衆號: 曲翎風,獲取獨家整理的學習資源和平常乾貨推送。
若是您對個人專題內容感興趣,也能夠關注個人博客: sagowiec.com