掌握 Spring 之異常處理

公衆號

前言

此次咱們學習 Spring 的異常處理,做爲一個 Spring 爲基礎框架的 Web 程序,若是不對程序中出現的異常進行適當的處理好比異常信息友好化,記錄異常日誌等等,直接將異常信息返回給客戶端展現給用戶,對用戶體驗有很差的影響。因此本篇文章主要探討經過 Spring 進行統一異常處理的幾種方式實現,以更優雅的方式捕獲程序發生的異常信息並進行適當的處理響應給客戶端。html

本文主要內容涉及以下:java

  • HandlerExceptionResolver 擴展git

  • @ExceptionHandler@ControllerAdvice 使用github

  • ResponseEntityExceptionHandler 擴展web

  • ResponseStatusException 使用spring

  • Spring Boot ErrorController 擴展json

示例項目:瀏覽器

環境支持:springboot

  • JDK 8mvc

  • SpringBoot 2.1.4

  • Maven 3.6.0

正文

Spring 框架的異常處理提供了許多種方式,在 Spring 3.2 以前主要有兩種處理方式:擴展 HandlerExceptionResolver 和 使用註解 @ExceptionHandler,Spring 3.2 以後提供了更豐富的處理方式。

HandlerExceptionResolver 擴展

HandlerExceptionResolver 是一個處理 Web 程序發生異常時的接口,接口方法以下:

@Nullable
ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
複製代碼

從返回值類型 ModelAndView 能夠看出,這個屬於 Spring MVC 框架中的接口,實現此方法就能夠對捕獲的異常進行解析處理,而後根據自身須要返回 ModelAndView 對象,以 JSON 數據或者頁面形式響應客戶端請求。

首先來看下 HandlerExceptionResolver 類層次體系,Spring 提供了 4 個實現類,下面根據這些類作了簡單的描述。

HandlerExceptionResolver 類體系

HandlerExceptionResolver 描述
SimpleMappingExceptionResolver 映射異常類到指定視圖,通常用於展示異常發生時的錯誤頁面
DefaultHandlerExceptionResolver HandlerExceptionResolver 的默認實現,處理 Spring MVC 異常
ResponseStatusExceptionResolver 處理帶有 @ResponseStatus 註解的異常,將註解上對應的值轉換爲 HTTP 狀態碼,通常放於自定義的異常類上
ExceptionHandlerExceptionResolver 處理帶有 @ExceptionHandler註解的異常

當咱們須要實現自定義的 HandlerExceptionResolver時,只要經過繼承它的抽象類 AbstractHandlerExceptionResolver,覆寫 doResolveException方法就能夠了。

下方的示例代碼處理了程序中發生的 IllegalArgumentException 異常時的狀況,並經過 MappingJackson2JsonView 對象返回客戶端一個 JSON 數據對象。若是不是 IllegalArgumentException異常,返回 null 表示讓其餘異常處理器進行處理,這裏因爲異常處理鏈機制,若是不處理異常,就會由 Web 容器將異常返回給客戶端。

@Component
public class RestResponseStatusExceptionResolver extends AbstractHandlerExceptionResolver {

    @Override
    protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        try {
            if (ex instanceof IllegalArgumentException) {
                ModelAndView modelAndView = new ModelAndView();
                Map<String, String> maps = new HashMap<>();
                maps.put("code", "400");
                maps.put("message", ex.getClass().getName());
                maps.put("data", null);
                MappingJackson2JsonView mappingJackson2JsonView = new MappingJackson2JsonView();
                mappingJackson2JsonView.setAttributesMap(maps);
                modelAndView.setView(mappingJackson2JsonView);
                return modelAndView;
            }
        } catch (Exception handlerException) {
            logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);
        }
        return null;
    }

}
複製代碼

咱們使用 Postman 工具模擬請求項目的 API 接口 /exception1 來致使異常的觸發,正常能夠看到以下效果:

image-20190518131151510

@ExceptionHandler

接下來咱們看下 @ExceptionHandler 的用法,這個註解一般定義在某個控制器下的方法裏,代表處理該控制器出現的指定異常, 以下代碼所示:

@RestController
public class RestApiController {
    //...

    @ExceptionHandler({IllegalStateException.class})
    public ModelAndView handleIllegalStateException(IllegalStateException ex) {
        System.out.println("非法狀態異常出現,須要處理 " + ex.getMessage());
        ModelAndView modelAndView = new ModelAndView();
        Map<String, String> maps = new HashMap<>();
        maps.put("data", null);
        maps.put("message", ex.getClass().getName());
        maps.put("code", "400");
        MappingJackson2JsonView mappingJackson2JsonView = new MappingJackson2JsonView();
        mappingJackson2JsonView.setAttributesMap(maps);
        modelAndView.setView(mappingJackson2JsonView);
        return modelAndView;
    }
}
複製代碼

@ExceptionHandler 能夠設置多個須要捕獲處理的異常類型,也能夠不填默認爲全部異常類,更多信息能夠查看 mvc-ann-exceptionhandler

而後使用 Postman 工具模擬請求項目的 API 接口 /exception2 來觸發異常,看下響應數據:

image-20190518134744575

這樣方式使用 @ExceptionHandler 存在一個缺陷,就是隻會針對當前控制器下的異常處理,若須要實現全局控制器的異常處理,還須要配合註解 @ControllerAdvice 一塊兒使用,接下來就介紹這個處理方式。

@ControllerAdvice

Spring 3.2 引入了一種新註解 @ControllerAdvice,用於將全部控制器中異常的處理放在一處進行,將指定一個類做爲全局異常處理器,用 @ExceptionHandler 註解標註的方法去處理異常,具體示例代碼以下:

@ControllerAdvice
public class NormalExceptionHandler {
    @ExceptionHandler()
    public ResponseEntity handleException(Exception e) {
        System.out.println("NormalExceptionHandler handle exception");
        return ResponseEntity.ok(new Result<>(400, e.getMessage(), null));
    }
}
複製代碼

代碼中的 Result 對象只是一個數據傳輸對象 (DTO),便於返回客戶端統一格式的數據。

再來看下使用 Postman 工具模擬請求 API 接口 /exception3 響應的數據,見下圖。

image-20190518144403940

還有一個註解 @RestControllerAdvice@ControllerAdvice 很類似,其實就是 @ControllerAdvice@ResponseBody註解的組合,效果就是異常處理方法返回的對象,直接就會被序列化成 JSON 數據給客戶端,使用方式以下:

@RestControllerAdvice
public class RestExceptionHandler {
    @ExceptionHandler({ArithmeticException.class})
    public Result handlerException(Exception e) {
        return new Result<>(400, e.getMessage(), null);
    }
}
複製代碼

這個註解是在 Spring 4.3 版本引入的,主要就是便於針對 REST 請求異常時直接返回 JSON 格式的數據,而不使用 ResponseEntity 對象方式傳遞數據。

@ControllerAdvice 默認攔截全部控制器中發生的異常,固然也能夠限定範圍,限定方式有限定註解,包名等,具體示例以下:

// Target all Controllers annotated with @RestController
@ControllerAdvice(annotations = RestController.class)
public class ExampleAdvice1 {}

// Target all Controllers within specific packages
@ControllerAdvice("org.example.controllers")
public class ExampleAdvice2 {}

// Target all Controllers assignable to specific classes
@ControllerAdvice(assignableTypes = {ControllerInterface.class, AbstractController.class})
public class ExampleAdvice3 {}
複製代碼

對於 全局 @ExceptionHandler 方法處理的描述,官方文檔還有額外的備註以下:

Global @ExceptionHandler methods (from a @ControllerAdvice) are applied after local ones (from the @Controller).

這代表了異常處理也存在優先級,先交給當前控制器內的 @ExceptionHandler方法處理,若未處理再由全局的@ExceptionHandler 方法處理。

ResponseEntityExceptionHandler 擴展

ResponseEntityExceptionHandler 類是主要針對 Spring MVC 所拋出異常的處理類,好比 405 請求,400 請求等,都默認由 ResponseEntityExceptionHandler處理,咱們能夠過繼承這個類覆寫它的方法,來實現特定請求異常的處理。好比下面代碼實現對 405 請求異常的響應處理。

@@ControllerAdvice
public class CustomWebResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {
    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        switch (status) {
            case METHOD_NOT_ALLOWED:
                return getMethodNotAllowedResponse(request);
            default:
                return ResponseEntity.ok(new Result<>(status.value(), status.getReasonPhrase(), null));
        }
    }

    public ResponseEntity getMethodNotAllowedResponse(WebRequest request) {
        String uri = "";
        if (request instanceof ServletWebRequest) {
            uri = ((ServletWebRequest) request).getRequest().getRequestURI();
        }
        Result<Object> result = new Result<>();
        result.setCode(HttpStatus.METHOD_NOT_ALLOWED.value());
        result.setMessage(uri + " 請求方式不正確");
        return ResponseEntity.ok(result);
    }
}
複製代碼

經過這樣的方式,咱們嘗試發送 GET 請求給 API 接口/hello,會有以下返回信息:

image-20190518162624412

當時 ResponseEntityExceptionHandler 也存在侷限性,目前支持的 SpringMVC 標準異常只有下面 15 種異常類型:

  • HttpRequestMethodNotSupportedException

  • HttpMediaTypeNotSupportedException

  • HttpMediaTypeNotAcceptableException

  • MissingPathVariableException

  • MissingServletRequestParameterException

  • ServletRequestBindingException

  • ConversionNotSupportedException

  • TypeMismatchException

  • HttpMessageNotReadableException

  • HttpMessageNotWritableException

  • MethodArgumentNotValidException

  • MissingServletRequestPartException

  • BindException

  • NoHandlerFoundException

  • AsyncRequestTimeoutException

ResponseStatusException

ResponseStatusException類是在 Spring 5.0 引入,關聯 HTTP 狀態碼和可選的緣由,咱們直接就能夠在請求方法中構建這個異常對象進行返回,使用起來十分簡單:

@GetMapping("/exception4")
public ResponseEntity<String> exception4(String param) {
    throw new ResponseStatusException(HttpStatus.NOT_FOUND, "資源未找到");
}
複製代碼

使用這種方式雖然能直接返回響應碼和具體緣由,可是沒有統一處理異常的效果,一般配合 @ControllerAdvice 一塊兒組合使用。

Spring Boot ErrorController

ErrorController 是 Spring Boot 2.0 引入接口,基於此的實現類 org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController 爲咱們提供了一種通用的方式進行錯誤處理, 下面是這個實現類的關鍵方法:

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
    HttpStatus status = getStatus(request);
    Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes(
            request, isIncludeStackTrace(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    Map<String, Object> body = getErrorAttributes(request,
            isIncludeStackTrace(request, MediaType.ALL));
    HttpStatus status = getStatus(request);
    return new ResponseEntity<>(body, status);
}
複製代碼

能夠從這兩個方法看出針對錯誤請求,BasicErrorController 提供了兩種數據形式的返回,一種是 HTML 頁面,一種是 JSON 數據;若是咱們直接使用瀏覽器訪問接口的話見到的就是 errorHtml方法返回的 HTML 頁面數據,它們的區別就在於請求時 Header 裏 Accept 值的不一樣。

image-20190518170154527

另外,Spring Boot 提供統一錯誤信息處理,是容許關閉的,只要在配置文件 application.properties 設置 server.error.whitelabel.enabledfalse便可。

server.error.whitelabel.enabled=false
複製代碼

固然咱們也能夠基於此進行擴展,好比實現一個自定義的錯誤控制器,繼承 BasicErrorController,編寫本身的錯誤展現邏輯和內容,好比下面代碼:

@Component
public class CustomErrorController extends BasicErrorController {

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @RequestMapping(produces = MediaType.APPLICATION_XML_VALUE)
    public ResponseEntity<Map<String, Object>> xmlError(HttpServletRequest request, HttpStatus status) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", status.value());
        map.put("message", status.getReasonPhrase());
        return ResponseEntity.ok(map);
    }
}
複製代碼

實現的 CustomErrorController 針對請求時 Aceept 爲 application/xml的發生的異常都統一以 XML 格式進行返回,如圖:

image-20190518171944860

注意: Spring Boot 默認不支持數據進行 XML 格式的轉換,POM 文件須要額外添加依賴庫:

<dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-xml</artifactId>
</dependency>
複製代碼

結語

本文咱們主要學習了 Spring 框架 5 種異常處理的方式以及 Spring Boot 的通用異常處理行爲,形式多樣,但具體狀況須要具體定製,爲了保證程序的健壯性和便於快速定位請求出現的異常問題,咱們必須爲程序提供統一的異常處理方式,也在平時的項目裏使用起來吧。

若是讀完以爲有收穫的話,歡迎點【好看】,點擊文章頭圖,掃碼關注【聞人的技術博客】😄😄😄。

參考

相關文章
相關標籤/搜索