SpringBoot中的全局異常處理


本篇要點

  • 介紹SpringBoot默認的異常處理機制。
  • 如何定義錯誤頁面。
  • 如何自定義異常數據。
  • 如何自定義視圖解析。
  • 介紹@ControllerAdvice注解處理異常。

一、SpringBoot默認的異常處理機制

默認情況下,SpringBoot為以下兩種情況提供了不同的響應方式:

  1. Browser Clients瀏覽器客戶端:通常情況下請求頭中的Accept會包含text/html,如果未定義/error的請求處理,就會出現如下html頁面:Whitelabel Error Page,關於error頁面的定制,接下來會詳細介紹。

  1. Machine Clients機器客戶端:Ajax請求,返回ResponseEntity實體json字符串信息。
{
    "timestamp": "2020-10-30T15:01:17.353+00:00",
    "status": 500,
    "error": "Internal Server Error",
    "trace": "java.lang.ArithmeticException: / by zero...",
    "message": "/ by zero",
    "path": "/"
}

SpringBoot默認提供了程序出錯的結果映射路徑/error,這個請求的處理邏輯在BasicErrorController中處理,處理邏輯如下:

// 判斷mediaType類型是否為text/html
@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, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
    response.setStatus(status.value());
    // 創建ModelAndView對象,返回頁面
    ModelAndView modelAndView = resolveErrorView(request, response, status, model);
    return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
    HttpStatus status = getStatus(request);
    if (status == HttpStatus.NO_CONTENT) {
        return new ResponseEntity<>(status);
    }
    Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
    return new ResponseEntity<>(body, status);
}

二、錯誤頁面的定制

相信Whitelabel Error Pag頁面我們經常會遇到,這樣體驗不是很好,在SpringBoot中可以嘗試定制錯誤頁面,定制方式主要分靜態和動態兩種:

  1. 靜態異常頁面

classpath:/public/errorclasspath:/static/error路徑下定義相關頁面:文件名應為確切的狀態代碼,如404.html,或系列掩碼,如4xx.html。

舉個例子,如果你想匹配404或5開頭的所有狀態代碼到靜態的html文件,你的文件目錄應該是下面這個樣子:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- public/
             +- error/
             |   +- 404.html
    		 |   +- 5xx.html
             +- <other public assets>

如果500.html和5xx.html同時生效,那么優先展示500.html頁面。

  1. 使用模板的動態頁面

放置在classpath:/templates/error路徑下:這里使用thymeleaf模板舉例:

src/
 +- main/
     +- java/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 5xx.html
             +- <other templates>

頁面如下:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>5xx</title>
    </head>
    <body>
        <h1>5xx</h1>
        <p th:text="'error -->'+ ${error}"></p>
        <p th:text="'status -->' + ${status}"></p>
        <p th:text="'timestamp -->' + ${timestamp}"></p>
        <p th:text="'message -->' + ${message}"></p>
        <p th:text="'path -->' +${path}"></p>
        <p th:text="'trace -->' + ${trace}"></p>
    </body>
</html>

如果靜態頁面和動態頁面同時存在且都能匹配,SpringBoot對於錯誤頁面的優先展示規則如下:

如果發生了500錯誤:

動態500 -> 靜態500 -> 動態5xx -> 靜態5xx

三、自定義異常數據

默認的數據主要是以下幾個,這些數據定義在org.springframework.boot.web.servlet.error.DefaultErrorAttributes中,數據的定義在getErrorAttributes方法中。

DefaultErrorAttributes類在org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration自動配置類中定義:

	@Bean
	@ConditionalOnMissingBean(value = ErrorAttributes.class,
                              search = SearchStrategy.CURRENT)
	public DefaultErrorAttributes errorAttributes() {
		return new DefaultErrorAttributes();
	}

如果我們沒有提供ErrorAttributes的實例,SpringBoot默認提供一個DefaultErrorAttributes實例。

因此,我們就該知道如何去自定義異常數據屬性:

  1. 實現ErrorAttributes接口。
  2. 繼承DefaultErrorAttributes,本身已經定義對異常數據的處理,繼承更具效率。

定義方式如下:

/**
 * 自定義異常數據
 * @author Summerday
 */
@Slf4j
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {

    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> map = super.getErrorAttributes(webRequest, options);
        if(map.get("status").equals(500)){
            log.warn("服務器內部異常");
        }
        return map;
    }
}

四、自定義異常視圖

自定義視圖的加載邏輯存在於BasicErrorController類的errorHtml方法中,用於返回一個ModelAndView對象,這個方法中,首先通過getErrorAttributes獲取到異常數據,然后調用resolveErrorView去創建一個ModelAndView對象,只有創建失敗的時候,用戶才會看到默認的錯誤提示頁面。

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

DefaultErrorViewResolver類的resolveErrorView方法:

@Override
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
    // 以異常狀態碼作為視圖名去locations路徑中去查找頁面
    ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
    // 如果找不到,以4xx,5xx series去查找
    if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
        modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
    }
    return modelAndView;
}

那么如何自定義呢?和自定義異常數據相同,如果我們定義了一個ErrorViewResolver的實例,默認的配置就會失效。

/**
 * 自定義異常視圖解析
 * @author Summerday
 */

@Component
public class MyErrorViewResolver extends DefaultErrorViewResolver {

    public MyErrorViewResolver(ApplicationContext applicationContext, ResourceProperties resourceProperties) {
        super(applicationContext, resourceProperties);
    }

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        return new ModelAndView("/hyh/resolve",model);
    }
}

此時,SpringBoot將會去/hyh目錄下尋找resolve.html頁面。

五、@ControllerAdvice注解處理異常

前后端分離的年代,后端往往需要向前端返回統一格式的json信息,以下為封裝的AjaxResult對象:

public class AjaxResult extends HashMap<String, Object> {
    
    //狀態碼
    public static final String CODE_TAG = "code";
    
    //返回內容
    public static final String MSG_TAG = "msg";
    
    //數據對象
    public static final String DATA_TAG = "data";

    private static final long serialVersionUID = 1L;

    public AjaxResult() {
    }

    public AjaxResult(int code, String msg) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    public AjaxResult(int code, String msg, Object data) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (data != null) {
            super.put(DATA_TAG, data);
        }
    }

    public static AjaxResult ok() {
        return AjaxResult.ok("操作成功");
    }

    public static AjaxResult ok(Object data) {
        return AjaxResult.ok("操作成功", data);
    }

    public static AjaxResult ok(String msg) {
        return AjaxResult.ok(msg, null);
    }

    public static AjaxResult ok(String msg, Object data) {
        return new AjaxResult(HttpStatus.OK.value(), msg, data);
    }

    public static AjaxResult error() {
        return AjaxResult.error("操作失敗");
    }

    public static AjaxResult error(String msg) {
        return AjaxResult.error(msg, null);
    }

    public static AjaxResult error(String msg, Object data) {
        return new AjaxResult(HttpStatus.INTERNAL_SERVER_ERROR.value(), msg, data);
    }

    public static AjaxResult error(int code, String msg) {
        return new AjaxResult(code, msg, null);
    }
}

根據業務的需求不同,我們往往也需要自定義異常類,便於維護:

/**
 * 自定義異常
 *
 * @author Summerday
 */
public class CustomException extends RuntimeException {

    private static final long serialVersionUID = 1L;

    private Integer code;

    private final String message;

    public CustomException(String message) {
        this.message = message;
    }

    public CustomException(String message, Integer code) {
        this.message = message;
        this.code = code;
    }

    public CustomException(String message, Throwable e) {
        super(message, e);
        this.message = message;
    }

    @Override
    public String getMessage() {
        return message;
    }

    public Integer getCode() {
        return code;
    }
}

@ControllerAdvice和@RestControllerAdvice這倆注解的功能之一,就是做到Controller層面的異常處理,而兩者的區別,與@Controller和@RestController差不多。

@ExceptionHandler指定需要處理的異常類,針對自定義異常,如果是ajax請求,返回json信息,如果是普通web請求,返回ModelAndView對象。

/**
 * 全局異常處理器
 * @author Summerday
 */

@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = 
        LoggerFactory.getLogger(GlobalExceptionHandler.class);


    @ExceptionHandler(CustomException.class)
    public Object handle(HttpServletRequest request, CustomException e) {
        AjaxResult info = AjaxResult.error(e.getMessage());
        log.error(e.getMessage());
        // 判斷是否為ajax請求
        if (isAjaxRequest(request)) {
            return info;
        }
        ModelAndView mv = new ModelAndView();
        mv.setViewName("custom"); // templates/custom.html
        mv.addAllObjects(info);
        mv.addObject("url", request.getRequestURL());
        return mv;
    }

    private boolean isAjaxRequest(HttpServletRequest request) {
        return "XMLHttpRequest".equals(request.getHeader("X-Requested-With"));
    }
}

在Controller層,人為定義拋出異常:

@RestController
public class TestController {

    @GetMapping("/ajax")
    public AjaxResult ajax() {
        double alpha = 0.9;
        if (Math.random() < alpha) {
            throw new CustomException("自定義異常!");
        }
        return AjaxResult.ok();
    }
}

最后,通過/templates/custom.html定義的動態模板頁面展示數據:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>自定義界面</title>
    </head>
    <body>
        <p th:text="'msg -->'+ ${msg}"></p>
        <p th:text="'code -->'+ ${code}"></p>
        <p th:text="'url -->'+ ${url}"></p>
    </body>
</html>

源碼下載

本文內容均為對優秀博客及官方文檔總結而得,原文地址均已在文中參考閱讀處標注。最后,文中的代碼樣例已經全部上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn

參考閱讀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM