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格式返回結果不符。
安全
![](http://static.javashuo.com/static/loading.gif)
聲明全局異常處理:服務器
/**
* @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格式數據。
![](http://static.javashuo.com/static/loading.gif)
404異常捕捉
假設咱們去請求項目下一個不存在的url,會出現什麼樣的返回結果呢?
請求鏈路:http://localhost:8080/test
咱們會發現,返回的是一個404的異常頁面,關鍵是後臺居然沒有打印任何異常日誌。
![](http://static.javashuo.com/static/loading.gif)
首先,添加參數,控制異常拋出:
#出現錯誤時, 直接拋出異常
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異常提示。
![](http://static.javashuo.com/static/loading.gif)
請求的HttpStatus的狀態碼也和提示信息中的吻合。
![](http://static.javashuo.com/static/loading.gif)
這裏提一點注意事項,在全局異常處理類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
![](http://static.javashuo.com/static/loading.gif)
![](http://static.javashuo.com/static/loading.gif)
分析:
這是因爲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的異常處理,有更深的瞭解。
裏面對以下異常進行了捕捉處理。
![](http://static.javashuo.com/static/loading.gif)
核心處理流程:
![](http://static.javashuo.com/static/loading.gif)
能夠發現,默認的實現中,返回結構都是爲空。
![](http://static.javashuo.com/static/loading.gif)
這就是咱們在繼承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);
}
}
若是隻是簡單繼承,不封裝返回值的話,請求結果以下:
![](http://static.javashuo.com/static/loading.gif)
定義server.servlet.context-path後,異常捕獲失敗
新增server.servlet.context-path屬性,讓servlet攔截全部與/tax匹配的請求
server.servlet.context-path=/tax
嘗試請求以下連接:http://localhost:8080/testGet
![](http://static.javashuo.com/static/loading.gif)
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異常,會泄露中間件的版本信息。
![](http://static.javashuo.com/static/loading.gif)
在不少安全級別比較高的項目中,因爲須要進行安全掃描,若是發現中間件的版本信息,就容易針對性的進行攻擊,是一個很是常見的中間件版本信息泄露的安全漏洞問題。
經研究發現,該問題是因爲引入了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請求的緣由分析
不要老是抱怨平時工做的內容沒有什麼技術含量,不少小的功能特性,你真的掌握了嗎?
點贊,關注,共勉,作一個真正的程序員。
![](http://static.javashuo.com/static/loading.gif)
更多精彩,關注我吧。
![圖注:跟着老萬學java](http://static.javashuo.com/static/loading.gif)
本文分享自微信公衆號 - 跟着老萬學java(douzhe_2019)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。