SpringBoot 實踐-Filter 中的異常處理和 Controller 中的異常處理

https://juejin.im/post/5e47f5a8e51d4526ff02484ahtml


本篇主要是記錄如何使用 SpringBoot 所提供的 ErrorController 這個接口能力;其內置了一個 BasicErrorController 對異常進行統一的處理,當在 Controller 發生異常的時候會自動把請求 forward 到 /error 這個請求 path 下(/error 是 SpringBoot 提供的一個默認的mapping)。BasicErrorController 提供兩種返回錯誤:一、頁面返回;二、json 返回。java

背景

開發中遇到的一個問題:項目中全部的 rest 請求均是經過 json 形式返回,且自定義了一個統一的數據結構對象,以下:json

public class Response<T{
    // 數據
    private T data;
    // success 標記
    private boolean success;
    // 異常信息
    private String error;
    // 省略 get set
}
複製代碼








這個結構很是常見,相信不少開發者都這麼玩過。項目中 rest 請求返回的全部結果都是以 Response 對象形式返回,以下:安全

@RequestMapping("test")
public Response<String> testApi(){
    Response<String> result = new Response<>();
    result.setData("this is glmapper blog");
    result.setSuccess(true);
    return result;
}
複製代碼






這基本是最簡化版的一個模型;出於安全考慮,如今有個需求是須要對每一個請求作校驗,好比校驗請求中是否攜帶 token 這種。思路很簡單就是經過攔截器或者過濾器的方式來對請求作攔截檢驗。數據結構

其實不論是攔截器仍是過濾器,須要考慮的一個問題是,在校驗不經過或者校驗時產生異常的狀況下,怎麼把異常信息以項目中規定的統一數據格式返回,即返回 Response。app

直接將 Response 寫回去

利用 ServletResponse 中提供的 PrintWriter,將 Response 以 json 格式直接 print 回去。大概代碼以下:ide

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                        FilterChain chain)
 throws IOException, ServletException 
{
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    String requestURI = request.getRequestURI();
    // mock 測試異常請求
    if (requestURI.contains("testTokenError")) {
        Response<String> response = new Response<>();
        response.setError("token validation fails");
        // 回寫異常信息
        returnResponse((HttpServletResponse)servletResponse,JSONObject.toJSONString(response));
        // 返回
        return;
    }
    chain.doFilter(servletRequest, servletResponse);
}

private void returnResponse(HttpServletResponse response, String data) {
    PrintWriter writer = null;
    response.setCharacterEncoding("UTF-8");
    response.setContentType("text/html; charset=utf-8");
    try {
        writer = response.getWriter();
        // 經過 PrintWriter 將 data 數據直接 print 回去
        writer.print(data);
    } catch (IOException e) {
    } finally {
        if (writer != null)
            writer.close();
    }
}
複製代碼





























這種方式比較簡單和直接,print 異常數據以後直接 return,再也不繼續過濾器鏈。post

拋出異常,經過 BasicErrorController 方式處理

這種方式是利用了 SpringBoot 自己提供的能力,能夠更優雅的處理錯誤信息。代碼大體以下:測試

一、是在 Filter 中就直接拋出一個異常this

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
                        FilterChain chain)
 throws IOException, ServletException 
{
    HttpServletRequest request = (HttpServletRequest) servletRequest;
    String requestURI = request.getRequestURI();
    // mock 測試異常請求
    if (requestURI.contains("testTokenError")) {
        // 直接返回一個自定義的異常
        throw new ValidationException("token validation fails");
    }
    chain.doFilter(servletRequest, servletResponse);
}
複製代碼










二、定義一個異常處理的 Controller

這裏定義一個 TokenErrorController ,繼承自 SpringBoot 提供的 BasicErrorController 這個類,而後重寫 error 這個方法(若是是頁面的話,重寫 errorHtml 這個方法),用於返回自定義的 Response 數據。代碼以下:

@RestController
public class TokenErrorController extends BasicErrorController {
    // 重寫 error 方法
    @Override
    @RequestMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Map<String, Object> body = getErrorAttributes(request,
            isIncludeStackTrace(request, MediaType.ALL));
        HttpStatus status = getStatus(request);
        // 拿到 body 中的異常 message 
        String message = body.get("message").toString();
        // 構建 Response 對象
        Response response = new Response();
        // 將 message 的 設置到 response 
        response.setError(message);
        // 返回
        return new ResponseEntity(response, status);
    }
    // 省略其餘無關代碼
}
複製代碼



















這樣就能夠實如今不改動以前工程任何代碼的狀況下只處理額外 Filter 中拋出的異常了。須要注意的是,上述是經過 BasicErrorController 來接受了 Filter 拋出的異常信息,而後再經過 BasicErrorController 將異常信息進行包裝而且返回。爲何要提一下這個呢?主要是爲了和 SpringBoot 中基於 REST 請求層所提供的兩個用於處理全局異常的註解區分,這兩個註解分別是 @ControllerAdvice 和 @RestControllerAdvice,經過註解的名字其實就能看出,SpringBoot 中,能夠經過這兩個註解來實現對 @Controller 和 @RestController 標註的類進行全局攔截,由於是 Controller 層面的 AOP 攔截,因此對於 Filter 中拋出的異常,經過 @ControllerAdvice 和 @RestControllerAdvice 兩個註解定義的全局異常處理器是無法處理的。

下面就簡單介紹下 @ControllerAdvice 和 @RestControllerAdvice 這兩個註解的使用。

全局異常處理

自定義一個 OtherExcepetion ,而後再使用基於 @RestControllerAdvice 註解編寫一個全局異常處理器。

@RestControllerAdvice
public class OtherExceptionHandler {
    // 這裏只處理 OtherException 異常類型
    @ExceptionHandler(value = OtherException.class)
    public Response<String> otherExceptionHandler(HttpServletRequest req, OtherException e){
        Response response = new Response();
        response.setError(e.getMessage());
        return response;
    }
    // 固然你也能夠定義處理其餘異常的 @ExceptionHandler
}
複製代碼










這種方式是無法處理 Filter 中異常的,只能處理 Controller 裏面拋出的異常。

小結

本篇主要記錄了在 SpringBoot 中如何保證 Filter 中拋出的異常能和業務同樣以指定類型的對象返回,並對 SpringBoot 中提供的基於 Controller 層異常捕獲處理進行簡單介紹。二者處理異常的思路是不一樣的:

  • BasicErrorController:接受來自 /error 的異常請求處理,Filter 中拋出的異常先 forward 到 /error,而後處理。
  • @RestControllerAdvice:經過對於全部 @Controller 註解所標註的類進行 AOP 攔截,可以根據異常類型匹配具體的 ExceptionHandler 進行處理。

水平有限,文章若是表述錯誤的地方,但願各位大佬給予指正~

相關文章
相關標籤/搜索