Spring Boot之全局異常處理:404異常爲何捕獲不到?

Spring Boot有很多非常好的特性,可以幫助我們更快速的完成開發工作。今天和大家聊聊Spring boot的全局異常處理。

問題

1、spring boot中怎麼進行全局異常處理?
2、爲什麼我的404異常捕獲不到?
3、常見的http請求異常,能統一封裝成json返回嗎?

實戰說明

項目依賴包:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

接口聲明:

@SpringBootApplication
@RestController
public class ErrorApplication {

    public static void main(String[] args) {
        SpringApplication.run(ErrorApplication.class, args);
    }

    @GetMapping("/hello")
    public String hello(){
        return "hello laowan!";
    }

    @GetMapping("/testGet")
    public String testGet(String name) throws Exception {
        if (name==null) {
           throw new BusinessException(ResultCode.PAPAM_IS_BLANK);
        }
        return "laowan!";
    }

    @PostMapping("/testPost")
    public String testPost(){
        return "post laowan!";
    }
}

自定義返回碼枚舉類:

/**
 * @program: error
 * @description:返回狀態碼
 * @author: wanli
 * @create: 2020-05-09 22:03
 **/
@Getter
public enum ResultCode {

    /*成功狀態嗎*/
    SUCCESS(1,"成功"),

    /*系統異常:4001-1999*/
    SYS_ERROR(4000,"系統異常,請稍後重試"),

    /*參數錯誤:1001-1999*/
     PAPAM_IS_INVALID(1001,"參數無效"),
     PAPAM_IS_BLANK(1002,"參數爲空"),
     PAPAM_TYPE_BIND_ERROR(1003,"參數類型錯誤"),
     PAPAM_NOT_COMPLETE(1003,"參數缺失"),

    /*用戶錯誤:2001-2999*/
    USER_NOT_LOGGED_IN(2001,"用戶未登錄,請登錄後重試"),
    USER_LOGIN_ERROR(2002,"賬號不存在或密碼錯誤"),
    USER_ACCOUNT_FORBIDDERN(2003,"賬號已被禁用"),
    USER_NOT_EXIST(2004,"用戶不存在"),
    USER_HAS_EXISTED(2005,"賬號已存在")
    ;
    //狀態碼
    private Integer code;
    //提示信息
    private String message;


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

}

通用返回類:

/**
 * 通用返回響應
 */
@JsonInclude(JsonInclude.Include.NON_NULL)
@Data
public class CommonResp<T> {
    private Integer code;
    private String message;
    private T data;

    public CommonResp(ResultCode resultCode) {
        this.code=resultCode.getCode();
        this.message=resultCode.getMessage();
    }

    public CommonResp(ResultCode resultCode, T data) {
        this.code=resultCode.getCode();
        this.message=resultCode.getMessage();
        this.data = data;
    }

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

    public static <T> CommonResp create(ResultCode resultCode) {
        return new CommonResp( resultCode);
    }


    public static <T> CommonResp getErrorResult(String message) {
        return new CommonResp(-1,message);
    }

    public static <T> CommonResp create(ResultCode resultCode, T data) {
        return new CommonResp( resultCode,data);
    }
}

自定義業務異常:

/**
 * 自定義業務異常
 * @program: error
 * @description:
 * @author: wanli
 * @create: 2020-05-09 21:49
 **/
@Getter
public class BusinessException extends  Exception{
    private ResultCode resultCode;

    public BusinessException(){}


    public BusinessException(ResultCode resultCode){
        super(resultCode.getMessage());
        this.resultCode = resultCode;
    }

    public BusinessException(String message){
        super(message);
    }

}

如果我們不進行異常處理,直接拋出BusinessException異常的話,請求接口如下:
請求鏈接:http://localhost:8080/testGet
返回結果如下,是一個異常提示頁面,顯然和我們現在主流的前後端分離,統一採用json格式返回結果不符。
在這裏插入圖片描述

聲明全局異常處理:

/**
 * @ClassName: GlobalExceptionHandler
 * @Description: 異常處理
 * @date: 2017年6月6日 下午2:12:08
 */
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler{

    /**
     * 業務異常處理
     * @param e
     * @return
     * @throws Exception
     */
    @ResponseBody
    @ExceptionHandler( BusinessException.class )
    public CommonResp handleBusinessException (BusinessException e ) throws Exception {
        log.error("BusinessException error", e);
        return CommonResp.create(e.getResultCode());
    }
}    

1、使用@ControllerAdvice註解聲明全局異常處理類
2、使用@ExceptionHandler指定要捕捉什麼異常,這裏會優先捕捉子級異常,當沒有匹配到子級異常時,纔會去匹配父級異常。比如同時聲明瞭@ExceptionHandler( BusinessException.class )和@ExceptionHandler(Exception.class )方法進行異常處理,當拋出BusinessException異常時,只會被@ExceptionHandler( BusinessException.class )註解的方法捕獲到。
3、通過@ResponseBody註解控制返回json格式數據。

重啓項目,再次請求,結果如下。
說明我們配置的BusinessException異常的全局捕獲成功,也是按照我們定義的異常碼返回的JSON格式數據。
在這裏插入圖片描述

404異常捕捉

假設我們去請求一個不存在的項目下一個不存在的url,會出現什麼樣的返回結果呢?
請求鏈路:http://localhost:8080/test
在這裏插入圖片描述
我們會發現,返回的是一個404的異常頁面,關鍵是後臺竟然沒有打印任何異常日誌。

那麼針對這類不是經由請求接口裏面拋出的異常,我們怎麼去捕捉,並封裝成json格式進行返回呢?

首先,添加參數,控制異常拋出:

#出現錯誤時, 直接拋出異常
spring.mvc.throw-exception-if-no-handler-found=true
#不要爲我們工程中的資源文件建立映射
spring.resources.add-mappings=false

然後繼承ResponseEntityExceptionHandler,封裝異常處理

@ControllerAdvice
@Slf4j
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    public RestResponseEntityExceptionHandler() {
        super();
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.error(ex.getMessage(),ex);
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute("javax.servlet.error.exception", ex, 0);
        }
        return new ResponseEntity( new CommonResp(status.value(),ex.getMessage()), headers, status);
    }
 }

再次請求,發現404異常捕獲成功,並返回json異常提示。
在這裏插入圖片描述
請求的HttpStatus的狀態碼也和提示信息中的吻合。
在這裏插入圖片描述

這裏提一點注意事項,在全局異常處理類GlobalExceptionHandler中,儘量不要爲了方便,直接對Exception異常進行捕獲處理,會影響返回結果的HttpStatus。
我們演示一下:

/**
 * 統一異常處理
 * @param e
 * @return
 * @throws Exception
 */
@ResponseBody
@ExceptionHandler( Exception.class )
public CommonResp handleException (Exception e){
	log.error( "Exception error", e );
	return  CommonResp.getErrorResult(e.getMessage());
}

然後再次請求http://localhost:8080/test
在這裏插入圖片描述
在這裏插入圖片描述
分析:
這是由於RestResponseEntityExceptionHandler類先對異常處理,返回ResponseEntity,由於ResponseEntity中的HttpStatus是一個異常碼,異常會緊接着被我們自定義的GlobalExceptionHandler類中的@ExceptionHandler( Exception.class )捕獲,這裏由於返回的是一個封裝的CommonResp對象,而不是一個ResponseEntity對象,默認就相當於把異常捕捉封裝處理了,雖然返回的結果數據是json數據,異常提示也正確,但是原本HttpStatu爲404的請求竟然變成了200成功請求,顯然不是我們想要的。

有人可能會說,我在@ExceptionHandler( Exception.class )方法裏面,也封裝返回一個ResponseEntity對象不就好了,但是這裏比較難獲取原本的HttpStatu,不推薦。

所以,建議大家儘量謹慎使用@ExceptionHandler( Exception.class)去進行異常處理,而是針對具體的異常進行特定處理。

最後,推薦大家看看ResponseEntityExceptionHandler類的源碼,會對Spring Boot中對ResponseEntity的異常處理,有更深的瞭解。
裏面默認對如下異常進行了捕捉處理。
在這裏插入圖片描述
核心處理流程:
在這裏插入圖片描述
可以發現,默認的實現中,返回結構都是爲空。
在這裏插入圖片描述
這就是我們在繼承ResponseEntityExceptionHandler類後,重寫handleExceptionInternal類的原因:

@ControllerAdvice
@Slf4j
public class RestResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

    public RestResponseEntityExceptionHandler() {
        super();
    }

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.error(ex.getMessage(),ex);
        if (HttpStatus.INTERNAL_SERVER_ERROR.equals(status)) {
            request.setAttribute("javax.servlet.error.exception", ex, 0);
        }
        //通過HttpStatus返回碼和異常名稱封裝返回結果
        return new ResponseEntity( new CommonResp(status.value(),ex.getMessage()), headers, status);
    }
}

如果只是簡單繼承,不封裝返回值的話,請求結果如下:
在這裏插入圖片描述

定義server.servlet.context-path後,異常捕獲失敗

新增server.servlet.context-path屬性,讓servlet攔截所有與/tax匹配的請求

server.servlet.context-path=/tax

請求如下鏈接:http://localhost:8080/testGet
在這裏插入圖片描述
分析:
server.servlet.context-path默認爲"/",即servlet攔截tomcat下的所有請求。
如果配置爲server.servlet.context-path=/tax,那麼tomcat只會將請求路徑匹配的請求轉發到項目中。
這也是很多人疑惑,爲什麼已經在spring boot項目中配置了全局異常處理,
但是當前請求localhost:8080/testGet時,404異常請求沒有被項目中配置的全局異常處理捕獲。
因爲請求根本沒有進你的項目中,而且直接被tomcat處理了,所以明明請求報404失敗,但是你的工程下沒有任何異常日誌提示,全局異常處理也沒有生效。

可以想想下以前使用單獨的web服務器部署項目,如果你的請求路徑沒有和server.servlet.context-path匹配的話,請求根本就沒有進入你的項目中。

所以,如果希望對進入tomcat的所有請求都進行處理的話,server.servlet.context-path一定要配置爲"/"
這樣你才能在代碼中,對相關異常做處理,不然,就是直接tomcat返回的默認異常頁面了。

404異常拋出tomcat版本信息問題

有時候我們會發現,經由tomcat直接拋出的404異常,會泄露中間件的版本信息。
在這裏插入圖片描述
在很多安全級別比較高的項目中,由於需要進行安全掃描,如果發現中間件的版本信息,就容易針對性的進行攻擊,是一個非常常見的中間件版本信息泄露的安全漏洞問題。

經研究發現,該問題是由於引入了spring-boot-devtools包導致的。
解決辦法有2種,
方法一:簡單暴力的去除spring-boot-devtools包依賴。
方法二:通過設置scope爲provided,使該包只在測試時有效,編譯打包時自動過濾該jar包依賴。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>provided</scope>
    <optional>true</optional>
</dependency>

給大家複習下Maven的scope屬性的作用:
1.compile:默認值 他表示被依賴項目需要參與當前項目的編譯,還有後續的測試,運行週期也參與其中,是一個比較強的依賴。打包的時候通常需要包含進去

2.test:依賴項目僅僅參與測試相關的工作,包括測試代碼的編譯和執行,不會被打包,例如:junit

3.runtime:表示被依賴項目無需參與項目的編譯,不過後期的測試和運行週期需要其參與。與compile相比,跳過了編譯而已。例如JDBC驅動,適用運行和測試階段

4.provided:打包的時候可以不用包進去,別的設施會提供。事實上該依賴理論上可以參與編譯,測試,運行等週期。相當於compile,但是打包階段做了exclude操作

5.system:從參與度來說,和provided相同,不過被依賴項不會從maven倉庫下載,而是從本地文件系統拿。需要添加systemPath的屬性來定義路徑。

總結

1、通過@ControllerAdvice、@ExceptionHandler、@ResponseBody三個註解的組合使用,實現全局異常處理。
2、通過配置spring.mvc.throw-exception-if-no-handler-found=true,控制404異常拋出
3、通過繼承ResponseEntityExceptionHandler類,可以利用重寫實現404異常的自定義格式返回
4、自定義業務異常和統一的接口返回數據格式,將CommonResp、ResultCode、BusinessException很好的結合使用。
5、404異常導致tomcat版本號泄露問題的解決
6、全局異常處理攔截不到404請求的原因分析

不要總是抱怨平時工作的內容沒有什麼技術含量,很多小的功能特性,你真的掌握了嗎?

點贊,關注,共勉,做一個真正的程序員。
在這裏插入圖片描述

更多精彩,關注我吧。
圖注:跟着老萬學java

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章