spring boot 如何統一處理 Filter、Servlet 中的異常信息




每個成功人士的背後,一定曾經作出過勇敢而又孤獨的決定。前端

放棄不難,但堅持很酷~java

版本:web

springboot:2.2.7spring

1、過濾器 Filter

一、過濾器的做用或使用場景:

  • 用戶權限校驗json

  • 用戶操做的日誌記錄後端

  • 黑名單、白名單api

  • 等等…數組

可使用過濾器對請求進行預處理,預處理完畢以後,再執行 chain.doFilter() 將程序放行。springboot

二、自定義過濾器

自定義過濾器,只須要實現 javax.servlet.Filter 接口便可。服務器

public class TestFilter implements Filter {

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

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("test filter;");

        // 表明過濾經過,必須添加如下代碼,程序才能夠繼續執行
        chain.doFilter(request, response);
    }
}

Filter 接口有三個方法:init()、doFilter()、destroy()。

  • init():項目啓動初始化的時候會被加載。

  • doFilter():過濾請求,預處理。

  • destroy():項目中止前,會執行該方法。

其中 doFilter() 須要本身必須實現,其他兩個是 default 的,能夠不用實現。

注意:若是 Filter 要使請求繼續被處理,就必定要調用 chain.doFilter() !

三、配置 Filter 被 Spring 管理

讓自定義的 Filter 被 Spring 的 IOC 容器管理,經常使用的實現方式有兩種,分別爲:

1)@WebFilter + @ServletComponentScan
  • 在 TestFilter 類上添加 @WebFilter 註解,

  • 而後在啓動類上增長 @ServletComponentScan 註解,就能夠了。

其中在 @WebFilter 註解上能夠指定過濾器的名稱和匹配的 url 數組,以下圖所示:

這種方式雖然能夠指定 filter 名稱和匹配的 url ,可是不能指定各 filter 之間的執行順序。

2)JavaConfig 配置

經過 JavaConfig 配置實現 Filter 被 Spring 管理,推薦使用這種方式,該種方式能夠指定各 filter 之間的執行順序。setOrder 的值越小,越優先執行。

@Configuration
public class FilterConfiguration {

    @Bean
    public Filter testFilter() {
        return new TestFilter();
    }

    @Bean
    public FilterRegistrationBean registrationTestFilter() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new DelegatingFilterProxy("testFilter"));
        registration.setName("testFilter");
        registration.addUrlPatterns("/v1/*");
        registration.addUrlPatterns("/v2/*");
        registration.setOrder(1);
        return registration;
    }

}

經過 JavaConfig 顯式配置 Filter ,功能強大,配置靈活。只須要把每一個自定義的 Filter 聲明成 Bean 交給 Spring 管理便可,還能夠設置匹配的 URL 、指定 Filter 的前後順序。

另外經過這種方式,還能夠實如今自定義 filter 中自動裝配一些對象 @Autowired 。

2、Servlet

一、Servlet 是什麼:

servlet是一個Java編寫的程序,此程序是基於http協議的,在服務器端(如Tomcat)運行的,是按照servlet規範編寫的一個Java類。

客戶端發送請求至服務器端,服務器端將請求發送至servlet,servlet生成響應內容並將其傳給服務器。

二、Servlet 的做用:

處理客戶端的請求並將其結果發送到客戶端。

三、自定義 Servlet

自定義 servlet 須要繼承一個抽象類,那就是 javax.servlet.http.HttpServlet。

而後在類上添加 @WebServlet 註解便可。

@WebServlet(name = "TestServlet", urlPatterns = {"/v1/*""/v2/*"})
public class TestServlet extends HttpServlet {

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.service(req, resp);
    }
}

而後在啓動類上增長 @ServletComponentScan 註解,自定義 Servlet 就完成了。

HttpServlet 中有不少方法,經常使用的仍是重寫 service(HttpServletRequest req, HttpServletResponse resp) 方法,進行請求處理返回。

四、HttpServletRequest 與 HttpServletResponse

HttpServletRequest 用來接收請求參數,HttpServletResponse 用來返回請求結果。

1)獲取 header

某個 header 值:

String clientid = req.getHeader("clientid");

遍歷全部 header 值:

Enumeration<String> headerNames = req.getHeaderNames();
while(headerNames.hasMoreElements()){
    String headerKey = headerNames.nextElement();
    log.info("{} : {}", headerKey, req.getHeader(headerKey));
}
2)獲取請求 uri
String requestUri = req.getRequestURI();
3)獲取請求類型
String methodType = req.getMethod();
4)獲取請求 params
String queryString = req.getQueryString();
if (!StrUtil.isBlank(queryString)) {
    log.info("請求行中的參數部分爲: {}", queryString);
    url = url + "?" + queryString;
}
5)獲取請求 body
private String getBody(HttpServletRequest request) {
    //獲取body數據
    StringBuilder sb = null;
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
        String line = null;
        sb = new StringBuilder();
        while ((line = reader.readLine()) != null) {
            sb.append(line);
        }
        return sb.toString();
    } catch (IOException e) {
        log.error("獲取請求體異常:", e);
        return "";
    }
}
6)組裝返回結果

返回結果是用 HttpServletResponse 來組裝。以下述代碼所示:

resp.setCharacterEncoding("UTF-8");
resp.setContentType("application/json; charset=utf-8");
PrintWriter printWriter = resp.getWriter();
printWriter.append(JSON.toJSONString(resultObj, SerializerFeature.WriteMapNullValue));
printWriter.close();

3、Filter 與 Servlet 的執行順序

filter1 -> filter2 -> servlet,  以後 servlet 處理完,再回傳到 filter2 -> filter1 。

  • 若是 servlet 和 filter 都有 response 返回,返回到前端的是 servlet 的 response。

  • 若是 servlet 中沒有 response 返回,filter 中有 response 返回。這時 filter 的 response 有效,返回到前端的是 filter 的 response。

filter 到 filter 或 servlet ,是經過 chain.doFilter(request, response); 這條命令來進行經過的。當從 servlet 中返回到 filter 時,chain.doFilter(request, response); 後面的代碼會繼續被執行。

4、Filter、Servlet 的全局異常統一處理

如今我在 TestFilter 中,添加了一個必報異常的代碼,發現使用 @RestControllerAdvice + @ExceptionHandler 並不能捕獲該 filter 的異常。

其實 @RestControllerAdvice + @ExceptionHandler 並不是能夠解決全部異常返回信息,它卻是能攔截 Controller 層的異常報錯,可是在 Filter、servlet 中的異常,使用以上註解就失效了,須要從別的方面進行入手。

找了很久的資料,才知道怎麼處理,因此也給你們分享一下。

一、spring boot 錯誤邏輯

咱們都知道,當 spring boot 遇到錯誤的時候,擁有本身的一套錯誤提示邏輯,分爲兩種狀況:

  • 頁面訪問形式

  • 接口調用訪問形式

二、繼承 BasicErrorController ,重寫 error() 方法

對於接口調用訪問的形式來講,咱們能夠來繼承 BasicErrorController 類,重寫 error() 方法,在 error() 方法裏面對全局異常進行統一處理。

經過觀察 BasicErrorController 能夠發現,它處理的就是 /error 請求。咱們繼承 BasicErrorController 以後,就只須要從新組裝 /error 的請求返回便可。

代碼實現以下:

@RestController
public class ErrorController extends BasicErrorController {

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

    public ErrorController() {
        super(new DefaultErrorAttributes(), new ErrorProperties());
    }

    /**
     * produces 設置返回的數據類型:application/json
     *
     * @param request 請求
     * @return 自定義的返回實體類
     */

    @Override
    @RequestMapping(value = "", 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);
        // 獲取錯誤信息
        String message = body.get("message").toString();
        int code = EnumUtil.getCodeByMsg(message, ResultEnum.class);

        HttpStatus httpStatus;
        if (code == 500) {
            // 服務端異常,狀態碼爲500
            httpStatus = HttpStatus.INTERNAL_SERVER_ERROR;
        } else {
            // 其他異常(手動throw)爲邏輯校驗,狀態碼爲200
            httpStatus = HttpStatus.OK;
        }
        return new ResponseEntity(Result.failed(code, message), httpStatus);
    }

}

其中註解 @RestController 是必填的,@RequestMapping 的 value 值必須爲空。

三、全局異常統一處理邏輯

核心:
  • 建立 ResultEnum 枚舉類,用來存儲多個異常信息( code 和 msg )。

  • 建立自定義異常類 CustomException,讓其能夠接收 ResultEnum 枚舉類內容。方便程序 throw 。

  • 建立 Result 類,用於封裝返回結果到前端。

  • 重寫 error() 方法。

在 error() 方法中,咱們能夠獲取到原 /error 請求的返回結果,而後獲取 message 報錯信息。而後根據 message 來獲取枚舉類與之對應的 code 值,而後將 code 和 message 填充到 Result 主體,返回到前端。

又對 HttpStatus 請求狀態碼進行了判斷,當手動 throw 拋出的異常,請求狀態碼爲 200;若是是程序預料以外的異常,沒有處理的,請求狀態碼就是 500 。

ResultEnum 枚舉類:
/**
 * 統一管理返回數據結果code和message,返回結果枚舉
 *
 * @author create17
 * @date 2020/5/13
 */

public enum ResultEnum implements CodeEnum {

    /**
     * clientid expired
     */

    ZERO_EXCEPTION(1052"/ by zero")
    ;

    private int code;

    private String msg;

    ResultEnum(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    @Override
    public int getCode() {
        return code;
    }

    @Override
    public String getMsg() {
        return msg;
    }

}
CustomException 自定義異常類:
/**
 * 自定義異常類
 *
 * @author create17
 * @date 2020/5/13
 */

@EqualsAndHashCode(callSuper = true)
@Data
public class CustomException extends RuntimeException {

    private int code;

    private String msg;

    public CustomException(ResultEnum resultEnum) {
        // 自定義錯誤棧中顯示的message
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
        this.msg = resultEnum.getMsg();
    }

    public CustomException(int code, String msg) {
        // 自定義錯誤棧中顯示的message
        super(msg);
        this.code = code;
        this.msg = msg;
    }
}
CodeEnum 接口:
/**
 * @author create17
 * @date 2020/7/24
 */

public interface CodeEnum {

    int getCode();

    String getMsg();

}
EnumUtil 工具類:
/**
 * 經過code找到msg, 經過msg找到code
 * 
 * @author create17
 * @date 2020/7/24
 */

public class EnumUtil {

    public static <T extends CodeEnum> String getMsgByCode(Integer code, Class<T> t){
        for(T item: t.getEnumConstants()){
            if(item.getCode() == code){
                return item.getMsg();
            }
        }
        return "";
    }

    public static <T extends CodeEnum> Integer getCodeByMsg(String msg, Class<T> t){
        for(T item: t.getEnumConstants()){
            if(StrUtil.equals(item.getMsg(),msg)){
                return item.getCode();
            }
        }
        return 500;
    }

}
Result 類:
/**
 * 響應信息主體
 *
 * @author create17
 * @date 2020/5/28
 */

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<Timplements Serializable {
    private static final long serialVersionUID = 1L;

    public static final int SUCCESS_CODE = 0;

    public static final int FAIL_CODE = 1;

    public static final String SUCCESS_MSG = "success";

    public static final String FAIL_MSG = "error";

    /**
     * 返回標記:成功標記=0,失敗標記=1
     */

    private int code;

    /**
     * 返回信息
     */

//    @JsonProperty("message")
    private String message;

    /**
     * 數據
     */

//    @JsonProperty("results")
    private T results;

    public static <T> Result<T> ok() {
        return restResult(null, SUCCESS_CODE, SUCCESS_MSG);
    }

    public static <T> Result<T> ok(T data) {
        return restResult(data, SUCCESS_CODE, SUCCESS_MSG);
    }

    public static <T> Result<T> ok(T data, String msg) {
        return restResult(data, SUCCESS_CODE, msg);
    }

    public static <T> Result<T> failed() {
        return restResult(null, FAIL_CODE, FAIL_MSG);
    }

    public static <T> Result<T> failed(String msg) {
        return restResult(null, FAIL_CODE, msg);
    }

    public static <T> Result<T> failed(int code, String msg) {
        return restResult(null, code, msg);
    }

    public static <T> Result<T> failed(ResultEnum resultEnum) {
        return restResult(null, resultEnum.getCode(), resultEnum.getMsg());
    }

    private static <T> Result<T> restResult(T data, int code, String msg) {
        Result<T> apiResult = new Result<>();
        apiResult.setCode(code);
        apiResult.setResults(data);
        apiResult.setMessage(msg);
        return apiResult;
    }
}

四、測試

好了,到這裏咱們的全局異常就統一處理完了,filter 和 servlet 的異常不出意外的話,都會通過 ErrorController 類。我先如今測試一下。

在 TestFilter 中,添加如下代碼:

try {
    int aa = 1/0;
catch (Exception e) {
    throw new CustomException(ResultEnum.ZERO_EXCEPTION);
}

不出意外的話,異常會被攔截處理,以下圖所示:

參考博客:https://blog.csdn.net/Chen_RuiMin/article/details/104418904

5、總結

不總結的文章不是好文章,咱們最後來總結一下。

首先是講解了過濾器 Filter 的使用場景,實現方式,而後提供了兩種 Filter 被 Spring 管理的方法,其中特別推薦使用 JavaConfig 配置使 Filter 被 Spring 管理,由於這樣不只能夠指定多個 Filter 之間的執行順序,還能實如今 Filter 裏面自動裝配一些對象。

第二又介紹了 Servlet 的實現方式,HttpServletRequest 與 HttpServletResponse 的使用。

第三是概述了一下 Filter 與 Servlet 的執行順序。

第四是文章中最想分享的地方,那就是如何統一處理 Filter 與 Servlet 的全局異常,嘗試了不少方法,最終認爲繼承 BasicErrorController,重寫 error() 方法是挺好的實現方式,因此就趕快分享給你們嘍~


點關注,不迷路

好了各位,以上就是這篇文章的所有內容了,能看到這裏的人呀,都是人才

也感謝各位的支持和承認,給予我最大的創做動力吧,咱們下篇文章見!

若是本篇博客有任何錯誤,請批評指教,不勝感激 !

 熱 文 推 薦 
  企業都在用的 spring boot 打包插件,真的超好用!
看完這篇文章還不會給spring boot配置logback,請你吃瓜!
  Spring 中如何使用 @Scheduled 建立定時任務
  Spring使用ThreadPoolTaskExecutor自定義線程池及實現異步調用
  用心整理 | Spring AOP 乾貨文章,圖文並茂,附帶 AOP 示例 ~
  Spring IOC,看完這篇文章,我纔算是懂了!
  懶人:使用 idea 插件 Easy Code 自定義 MybatisPlus 模板一鍵快速生成所需代碼
  Spring boot Swagger2 配置使用實戰
  後端字段校驗告別 if else,快來用下 @Valid 註解,省事又方便

歡迎你們留言討論
👆 👆 👆

若是這篇文章對你有所啓發,點贊、轉發都是一種支持!

朕已閱 

本文分享自微信公衆號 - 大數據實戰演練(gh_f942bfc92d26)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索