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

Spring Boot有不少很是好的特性,能夠幫助咱們更快速的完成開發工做。今天和你們聊聊Spring boot的全局異常處理。
java

問題

一、spring boot中怎麼進行全局異常處理?
二、爲何個人404異常捕獲不到?
三、常見的http請求異常,能統一封裝成json返回嗎?程序員

實戰說明

項目依賴包:web

<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>

接口聲明:spring

@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!";
    }
}

自定義返回碼枚舉類:json

/**
 * @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);
    }
}

自定義業務異常:tomcat

/**
 * 自定義業務異常
 * @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());
    }
}    

一、使用@ControllerAdvice註解聲明全局異常處理類
二、使用@ExceptionHandler指定要捕捉什麼異常,這裏會優先捕捉子級異常,當沒有匹配到子級異常時,纔會去匹配父級異常。好比同時聲明瞭@ExceptionHandler( BusinessException.class )和@ExceptionHandler(Exception.class )方法進行異常處理,當拋出BusinessException異常時,只會被@ExceptionHandler( BusinessException.class )註解的方法捕獲到。
三、經過@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對象不就行了,可是這裏比較難獲取本來的HttpStatus,不推薦。

因此,建議你們儘可能謹慎使用@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必定要配置爲"/"

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的屬性來定義路徑。

總結

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

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

點贊,關注,共勉,作一個真正的程序員。

 更多精彩,關注我吧。

圖注:跟着老萬學java



本文分享自微信公衆號 - 跟着老萬學java(douzhe_2019)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索