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請求的原因分析
不要總是抱怨平時工作的內容沒有什麼技術含量,很多小的功能特性,你真的掌握了嗎?
點贊,關注,共勉,做一個真正的程序員。
更多精彩,關注我吧。