1. 程式人生 > >[轉] SpringBoot RESTful 應用中的異常處理小結

[轉] SpringBoot RESTful 應用中的異常處理小結

bject common -h ports interface cep exce 源碼 tps

[From] https://segmentfault.com/a/1190000006749441

SpringBoot RESTful 應用中的異常處理小結

永順 2016年08月29日發布
  • 8.8k 次瀏覽

@ControllerAdvice 和 @ExceptionHandler 的區別

  • ExceptionHandler, 方法註解, 作用於 Controller 級別. ExceptionHandler 註解為一個 Controler 定義一個異常處理器.

  • ControllerAdvice, 類註解, 作用於 整個 Spring 工程. ControllerAdvice 註解定義了一個全局的異常處理器.

需要註意的是, ExceptionHandler 的優先級比 ControllerAdvice 高, 即 Controller 拋出的異常如果既可以讓 ExceptionHandler 標註的方法處理, 又可以讓 ControllerAdvice 標註的類中的方法處理, 則優先讓 ExceptionHandler 標註的方法處理.

處理 Controller 中的異常

為了方便地展示 Controller 異常處理的方式, 我創建了一個工程 SpringBootRESTfulErrorHandler, 其源碼可以到我的 Github: github.com/yongshun 中找到.
SpringBootRESTfulErrorHandler 工程的目錄結構如下:

技術分享

首先我們定義了三個自定義的異常:
BaseException:

public class BaseException extends Exception {
    public BaseException(String message) {
        super(message);
    }
}

MyException1:

public class MyException1 extends BaseException {
    public MyException1(String message) {
        super(message);
    }
}

MyException2:

public class MyException2 extends BaseException {
    public MyException2(String message) {
        super(message);
    }
}

接著我們在 DemoController 中分別拋出這些異常:

@RestController
public class DemoController {
    private Logger logger = LoggerFactory.getLogger("GlobalExceptionHandler");

    @RequestMapping("/ex1")
    public Object throwBaseException() throws Exception {
        throw new BaseException("This is BaseException.");
    }

    @RequestMapping("/ex2")
    public Object throwMyException1() throws Exception {
        throw new MyException1("This is MyException1.");
    }

    @RequestMapping("/ex3")
    public Object throwMyException2() throws Exception {
        throw new MyException2("This is MyException1.");
    }

    @RequestMapping("/ex4")
    public Object throwIOException() throws Exception {
        throw new IOException("This is IOException.");
    }

    @RequestMapping("/ex5")
    public Object throwNullPointerException() throws Exception {
        throw new NullPointerException("This is NullPointerException.");
    }

    @ExceptionHandler(NullPointerException.class)
    public String controllerExceptionHandler(HttpServletRequest req, Exception e) {
        logger.error("---ControllerException Handler---Host {} invokes url {} ERROR: {}", req.getRemoteHost(), req.getRequestURL(), e.getMessage());
        return e.getMessage();
    }
}
  • /ex1: 拋出 BaseException

  • /ex2: 拋出 MyException1

  • /ex3: 拋出 MyException2

  • /ex4: 拋出 IOException

  • /ex5: 拋出 NullPointerException

當 DemoController 拋出未捕獲的異常時, 我們在 GlobalExceptionHandler 中進行捕獲並處理:
GlobalExceptionHandler:

@RestController
@ControllerAdvice
public class GlobalExceptionHandler {
    private Logger logger = LoggerFactory.getLogger("GlobalExceptionHandler");

    @ExceptionHandler(value = BaseException.class)
    @ResponseBody
    public Object baseErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        logger.error("---BaseException Handler---Host {} invokes url {} ERROR: {}", req.getRemoteHost(), req.getRequestURL(), e.getMessage());
        return e.getMessage();
    }

    @ExceptionHandler(value = Exception.class)
    @ResponseBody
    public Object defaultErrorHandler(HttpServletRequest req, Exception e) throws Exception {
        logger.error("---DefaultException Handler---Host {} invokes url {} ERROR: {}", req.getRemoteHost(), req.getRequestURL(), e.getMessage());
        return e.getMessage();
    }
}

我們看到, GlobalExceptionHandler 類有兩個註解:

  • RestController, 表明 GlobalExceptionHandler 是一個 RESTful Controller, 即它會以 RESTful 的形式返回回復.

  • ControllerAdvice, 表示 GlobalExceptionHandler 是一個全局的異常處理器.

在 GlobalExceptionHandler 中, 我們使用了 ExceptionHandler 註解標註了兩個方法:

  • ExceptionHandler(value = BaseException.class): 表示 baseErrorHandler 處理 BaseException 異常和其子異常.

  • ExceptionHandler(value = Exception.class): 表示 defaultErrorHandler 會處理 Exception 異常和其所用子異常.

要註意的是, 和 try...catch 語句塊, 異常處理的順序也是從具體到一般, 即如果 baseErrorHandler 可以處理此異常, 則調用此方法來處理異常, 反之使用 defaultErrorHandler 來處理異常.

既然我們已經實現了 Controller 的異常處理, 那麽接下來我們就來測試一下吧.
在瀏覽器中分別訪問這些鏈接, 結果如下:
/ex1:

技術分享

/ex2:

技術分享

/ex3:

技術分享

/ex4:

技術分享

/ex5:

技術分享

可以看到, /ex1, /ex2, /ex3 拋出的異常都由 GlobalExceptionHandler.baseErrorHandler 處理; /ex4 拋出的 IOException 異常由 GlobalExceptionHandler.defaultErrorHandler 處理. 但是 /ex5 拋出的 NullPointerException 異常為什麽不是 defaultErrorHandler 處理, 而是由 controllerExceptionHandler 來處理呢? 回想到 @ControllerAdvice 和 @ExceptionHandler 的區別 這以小節中的內容時, 我們就知道原因了: 因為我們在 DemoController 中使用 ExceptionHandler 註解定義了一個 Controller 級的異常處理器, 這個級別的異常處理器的優先級比全局的異常處理器優先級高, 因此 Spring 發現 controllerExceptionHandler 可以處理 NullPointerException 異常時, 就調用這個方法, 而不會調用全局的 defaultErrorHandler 方法了.

處理 404 錯誤

Spring MVC

SpringBoot 默認提供了一個全局的 handler 來處理所有的 HTTP 錯誤, 並把它映射為 /error. 當發生一個 HTTP 錯誤, 例如 404 錯誤時, SpringBoot 內部的機制會將頁面重定向到 /error 中.
例如下圖中是一個默認的 SpringBoot 404 異常頁面.

技術分享

這個頁面實在是太醜了, 我們能不能自定義一個異常頁面呢? 當然可以了, 並且 SpringBoot 也給我們提示了: This application has no explicit mapping for /error, so you are seeing this as a fallback.
因此我們實現一個 /error 映射的 Controller 即可.

public class HttpErrorHandler implements ErrorController {

    private final static String ERROR_PATH = "/error";

    /**
     * Supports the HTML Error View
     *
     * @param request
     * @return
     */
    @RequestMapping(value = ERROR_PATH, produces = "text/html")
    public String errorHtml(HttpServletRequest request) {
        return "404";
    }

    /**
     * Supports other formats like JSON, XML
     *
     * @param request
     * @return
     */
    @RequestMapping(value = ERROR_PATH)
    @ResponseBody
    public Object error(HttpServletRequest request) {
        return "404";
    }

    /**
     * Returns the path of the error page.
     *
     * @return the error path
     */
    @Override
    public String getErrorPath() {
        return ERROR_PATH;
    }
}

根據上面代碼我們看到, 為了實現自定義的 404 頁面, 我們實現了 ErrorController 接口:

public interface ErrorController {
    String getErrorPath();
}

這個接口只有一個方法, 當出現 HTTP 錯誤時, SpringBoot 會將頁面重定向到 getErrorPath 方法返回的頁面中. 這樣我們就可以實現自定義的錯誤頁面了.

RESTful API

提供一個自定義的 "/error" 頁面對 Spring MVC 的服務來說自然是沒問題的, 但是如果我們的服務是一個 RESTful 服務的話, 這樣做就不行了.
當用戶調用了一個不存在的 RESTful API 時, 我們想記錄下這個異常訪問, 並返回一個代表錯誤的 JSON 給客戶端, 這該怎麽實現呢?
我們很自然地想到, 我們可以使用處理異常的那一套來處理 404 錯誤碼.
那麽我們來試一下這個想法是否可行吧.

技術分享

奇怪的是, 當我們在瀏覽器中隨意輸入一個路徑時, 代碼並沒有執行到異常處理邏輯中, 而是返回了一個 HTML 頁面給我們, 這又是怎麽回事呢?
原來 Spring Boot 中, 當用戶訪問了一個不存在的鏈接時, Spring 默認會將頁面重定向到 **/error** 上, 而不會拋出異常.
既然如此, 那我們就告訴 Spring Boot, 當出現 404 錯誤時, 拋出一個異常即可. 在 application.properties 中添加兩個配置:

spring.mvc.throw-exception-if-no-handler-found=true
spring.resources.add-mappings=false

上面的配置中, 第一個 spring.mvc.throw-exception-if-no-handler-found 告訴 SpringBoot 當出現 404 錯誤時, 直接拋出異常. 第二個 spring.resources.add-mappings 告訴 SpringBoot 不要為我們工程中的資源文件建立映射. 這兩個配置正是 RESTful 服務所需要的.
當加上這兩個配置後, 我們再來試一下:

技術分享

可以看到, 現在確實是在 defaultErrorHandler 中處理了.

本文由 yongshun 發表於個人博客, 采用署名-非商業性使用-相同方式共享 3.0 中國大陸許可協議.
非商業轉載請註明作者及出處. 商業轉載請聯系作者本人
Email: [email protected]
本文標題為: SpringBoot RESTful 應用中的異常處理小結
本文鏈接為: https://segmentfault.com/a/1190000006749441

[轉] SpringBoot RESTful 應用中的異常處理小結