文章目錄
1 摘要
異常是程序的一部分,在項目運行時可能由於各種問題而拋出。一份規範、簡潔的程序代碼需要有一套合理的異常處理機制,在同一個項目中使用統一的異常處理,能夠極大地方便問題的排查、接口的對接以及提升用戶體驗。本文將介紹一種在 Spring Boot 項目符合 REST 風格的全局異常處理解決方案。
2 核心依賴
<!-- web,mvc -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${springboot.version}</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${servlet-api.version}</version>
</dependency>
其中 ${springboot.version}
的版本爲 2.0.6.RELEASE
, ${servlet-api.version}
的版本爲 3.1.0
3 核心代碼
3.1 接口返回碼封裝類
../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ResponseCode.java
package com.ljq.demo.springboot.common.api;
import lombok.Getter;
import lombok.ToString;
/**
* @Description: 返回碼枚舉
* @Author yemiaoxin
* @Date 2018/5/22
*/
@Getter
@ToString
public enum ResponseCode {
/**
* 成功與失敗
*/
SUCCESS(200, "成功"),
FAIL(-1, "失敗"),
/**
* 公共參數
*/
PARAM_ERROR(1001, "參數錯誤"),
PARAM_NOT_NULL(1002, "參數不能爲空"),
SIGN_ERROR(1003,"簽名錯誤"),
REQUEST_METHOD_ERROR(1004, "請求方式錯誤"),
MEDIA_TYPE_NOT_SUPPORT_ERROR(1005, "參數(文件)格式不支持"),
PARAM_BIND_ERROR(1006, "參數格式錯誤,數據綁定失敗"),
NOT_FOUND_ERROR(1007, "請求資源(接口)不存在"),
MISS_REQUEST_PART_ERROR(1008, "缺少請求體(未上傳文件)"),
MISS_REQUEST_PARAM_ERROR(1009, "缺少請求參數"),
/**
* 用戶模塊
*/
ACCOUNT_ERROR(2001, "賬號錯誤"),
PASSWORD_ERROR(2002,"密碼錯誤"),
ACCOUNT_NOT_EXIST(2003,"賬號不存在"),
/**
* 其他
*/
UNKNOWN_ERROR(-1000,"未知異常");
/**
* 返回碼
*/
private int code;
/**
* 返回信息
*/
private String msg;
private ResponseCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
3.2 接口返回結果封裝類
../demo-common/src/main/java/com/ljq/demo/springboot/common/api/ApiResult.java
package com.ljq.demo.springboot.common.api;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Map;
/**
* @Description: 接口請求返回結果
* @Author: junqiang.lu
* @Date: 2018/10/9
*/
@Data
@ApiModel(value = "接口返回結果")
public class ApiResult<T> implements Serializable {
private static final long serialVersionUID = -2953545018812382877L;
/**
* 返回碼,200 正常
*/
@ApiModelProperty(value = "返回碼,200 正常", name = "code")
private int code = 200;
/**
* 返回信息
*/
@ApiModelProperty(value = "返回信息", name = "msg")
private String msg = "成功";
/**
* 返回數據
*/
@ApiModelProperty(value = "返回數據對象", name = "data")
private T data;
/**
* 附加數據
*/
@ApiModelProperty(value = "附加數據", name = "extraData")
private Map<String, Object> extraData;
/**
* 系統當前時間
*/
@ApiModelProperty(value = "服務器系統時間,時間戳(精確到毫秒)", name = "timestamp")
private Long timestamp = System.currentTimeMillis();
/**
* 獲取成功狀態結果
*
* @return
*/
public static ApiResult success() {
return success(null, null);
}
/**
* 獲取成功狀態結果
*
* @param data 返回數據
* @return
*/
public static ApiResult success(Object data) {
return success(data, null);
}
/**
* 獲取成功狀態結果
*
* @param data 返回數據
* @param extraData 附加數據
* @return
*/
public static ApiResult success(Object data, Map<String, Object> extraData) {
ApiResult apiResult = new ApiResult();
apiResult.setCode(ResponseCode.SUCCESS.getCode());
apiResult.setMsg(ResponseCode.SUCCESS.getMsg());
apiResult.setData(data);
apiResult.setExtraData(extraData);
return apiResult;
}
/**
* 獲取失敗狀態結果
*
* @return
*/
public static ApiResult failure() {
return failure(ResponseCode.FAIL.getCode(), ResponseCode.FAIL.getMsg(), null);
}
/**
* 獲取失敗狀態結果
*
* @param msg (自定義)失敗消息
* @return
*/
public static ApiResult failure(String msg) {
return failure(ResponseCode.FAIL.getCode(), msg, null);
}
/**
* 獲取失敗狀態結果
*
* @param responseCode 返回狀態碼
* @return
*/
public static ApiResult failure(ResponseCode responseCode) {
return failure(responseCode.getCode(), responseCode.getMsg(), null);
}
/**
* 獲取失敗狀態結果
*
* @param responseCode 返回狀態碼
* @param data 返回數據
* @return
*/
public static ApiResult failure(ResponseCode responseCode, Object data) {
return failure(responseCode.getCode(), responseCode.getMsg(), data);
}
/**
* 獲取失敗返回結果
*
* @param code 錯誤碼
* @param msg 錯誤信息
* @param data 返回結果
* @return
*/
public static ApiResult failure(int code, String msg, Object data) {
ApiResult result = new ApiResult();
result.setCode(code);
result.setMsg(msg);
result.setData(data);
return result;
}
}
3.3 自義定異常類
../demo-common/src/main/java/com/ljq/demo/springboot/common/exception/ParamsCheckException.java
package com.ljq.demo.springboot.common.exception;
import com.ljq.demo.springboot.common.api.ResponseCode;
import com.ljq.demo.springboot.common.api.ResponseCodeI18n;
import lombok.Data;
/**
* @Description: 自定義參數校驗異常
* @Author: junqiang.lu
* @Date: 2019/1/24
*/
@Data
public class ParamsCheckException extends Exception{
private static final long serialVersionUID = 2684099760669375847L;
/**
* 異常編碼
*/
private int code;
/**
* 異常信息
*/
private String message;
public ParamsCheckException(){
super();
}
public ParamsCheckException(int code, String message){
this.code = code;
this.message = message;
}
public ParamsCheckException(String message){
this.message = message;
}
public ParamsCheckException(ResponseCode responseCode){
this.code = responseCode.getCode();
this.message = responseCode.getMsg();
}
}
3.4 全局異常處理類
../demo-common/src/main/java/com/ljq/demo/springboot/common/interceptor/GlobalExceptionHandler.java
package com.ljq.demo.springboot.common.interceptor;
import com.ljq.demo.springboot.common.api.ApiResult;
import com.ljq.demo.springboot.common.api.ResponseCode;
import com.ljq.demo.springboot.common.exception.ParamsCheckException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
/**
* @Description: 全局異常處理
* @Author: junqiang.lu
* @Date: 2019/12/2
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 全局異常處理
*
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(value = {ParamsCheckException.class, HttpRequestMethodNotSupportedException.class,
HttpMediaTypeNotSupportedException.class, BindException.class, NoHandlerFoundException.class,
MissingServletRequestPartException.class, MissingServletRequestParameterException.class,
Exception.class})
public ResponseEntity exceptionHandler(Exception e) {
log.warn("class: {}, message: {}",e.getClass().getName(), e.getMessage());
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
// 自定義異常
if (ParamsCheckException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(((ParamsCheckException) e).getCode(),e.getMessage(), null),headers, HttpStatus.BAD_REQUEST);
}
// 請求方式錯誤異常
if (HttpRequestMethodNotSupportedException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(ResponseCode.REQUEST_METHOD_ERROR), headers, HttpStatus.BAD_REQUEST);
}
// 參數格式不支持
if (HttpMediaTypeNotSupportedException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(ResponseCode.MEDIA_TYPE_NOT_SUPPORT_ERROR), headers, HttpStatus.BAD_REQUEST);
}
// 參數格式錯誤,數據綁定失敗
if (BindException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(ResponseCode.PARAM_BIND_ERROR), headers, HttpStatus.BAD_REQUEST);
}
// 404
if (NoHandlerFoundException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(ResponseCode.NOT_FOUND_ERROR), headers, HttpStatus.BAD_REQUEST);
}
// 缺少請求體(未上傳文件)
if (MissingServletRequestPartException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(ResponseCode.MISS_REQUEST_PART_ERROR), headers, HttpStatus.BAD_REQUEST);
}
// 缺少請求參數
if (MissingServletRequestParameterException.class.isAssignableFrom(e.getClass())) {
return new ResponseEntity(ApiResult.failure(ResponseCode.MISS_REQUEST_PARAM_ERROR), headers, HttpStatus.BAD_REQUEST);
}
/**
* 根據情況添加異常類型(如IO,線程,DB 相關等)
*/
// 其他
return new ResponseEntity(ApiResult.failure(ResponseCode.UNKNOWN_ERROR), headers, HttpStatus.BAD_REQUEST);
}
}
說明:
與網絡請求相關的常見異常可參考:
Handling Standard Spring MVC Exceptions
部分異常與出現的場景歸納:
異常名稱 | 出現情況 |
---|---|
HttpRequestMethodNotSupportedException |
請求方式不是 Controller 中指定的, eg: Controller 指定 POST 請求,實際用 GET 請求 |
HttpMediaTypeNotSupportedException |
Controller 中指定接收參數格式爲文本,但實際請求 包含文件 |
BindException |
接收參數爲 int 類型, 實際傳參爲 String 類型 |
NoHandlerFoundException |
請求的地址不存在 |
MissingServletRequestPartException |
Controller 指明需要傳文件,但實際上沒有上傳文件 |
MissingServletRequestParameterException |
在使用 @RequestParam 註解時,沒有傳指定的參數(這裏建議使用封裝的 Bean 來接收參數,不要使用 @RequestParam ) |
3.5 配置文件
../demo-web/src/main/resources/application.yml
## 異常處理
spring:
mvc:
throw-exception-if-no-handler-found: true
resources:
add-mappings: false
若不添加此配置,則無法手動攔截 NoHandlerFoundException
異常,此時系統會調用 SpringBoot
默認的攔截器,返回信息也是 REST 風格,但不統一,具體示例如下:
{
"timestamp": "2019-12-02T07:34:40.485+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/api/rest/user/info/dede"
}
4 測試
4.1 拋出自定義異常
展示層:
請求日誌:
2019-12-02 15:41:18 | INFO | http-nio-8088-exec-5 | com.ljq.demo.springboot.web.acpect.LogAspectLogAspect.java 68| [AOP-LOG-START]
requestMark: cf0c3e90-39fc-4df9-9fe8-86fcaf3c37a8
requestIP: 127.0.0.1
contentType:application/x-www-form-urlencoded
requestUrl: http://127.0.0.1:8088/api/rest/user/info
requestMethod: GET
requestParams: id = [1];
targetClassAndMethod: com.ljq.demo.springboot.web.controller.RestUserController#info
2019-12-02 15:41:18 | WARN | http-nio-8088-exec-5 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: com.ljq.demo.springboot.common.exception.ParamsCheckException, message: 失敗
2019-12-02 15:41:18 | WARN | http-nio-8088-exec-5 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: ParamsCheckException(code=-1, message=失敗)
4.2 拋出其他異常
展示層:
請求日誌:
2019-12-02 15:41:07 | INFO | http-nio-8088-exec-4 | com.ljq.demo.springboot.web.acpect.LogAspectLogAspect.java 68| [AOP-LOG-START]
requestMark: 51e727ac-28fb-4fee-a20f-3e2cd9ccc5f2
requestIP: 127.0.0.1
contentType:application/x-www-form-urlencoded
requestUrl: http://127.0.0.1:8088/api/rest/user/info
requestMethod: GET
requestParams: id = [2];
targetClassAndMethod: com.ljq.demo.springboot.web.controller.RestUserController#info
2019-12-02 15:41:07 | WARN | http-nio-8088-exec-4 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: java.lang.Exception, message: 未知異常
2019-12-02 15:41:07 | WARN | http-nio-8088-exec-4 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: java.lang.Exception: 未知異常
4.3 錯誤的請求方式
展示層:
請求日誌:
2019-12-02 15:49:08 | WARN | http-nio-8088-exec-6 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.HttpRequestMethodNotSupportedException, message: Request method 'POST' not supported
2019-12-02 15:49:08 | WARN | http-nio-8088-exec-6 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported
4.4 向不接受文件參數的接口上傳文件
展示層:
請求日誌:
2019-12-02 15:54:46 | WARN | http-nio-8088-exec-8 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.HttpMediaTypeNotSupportedException, message: Content type 'multipart/form-data;boundary=--------------------------099318382469038507746221;charset=UTF-8' not supported
2019-12-02 15:54:46 | WARN | http-nio-8088-exec-8 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.HttpMediaTypeNotSupportedException: Content type 'multipart/form-data;boundary=--------------------------099318382469038507746221;charset=UTF-8' not supported
4.5 參數字段類型對不上
展示層:
請求日誌:
2019-12-02 15:57:50 | WARN | http-nio-8088-exec-1 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.validation.BindException, message: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'restUserInfoParam' on field 'id': rejected value [aaa]; codes [typeMismatch.restUserInfoParam.id,typeMismatch.id,typeMismatch.java.lang.Long,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [restUserInfoParam.id,id]; arguments []; default message [id]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Long' for property 'id'; nested exception is java.lang.NumberFormatException: For input string: "aaa"]
2019-12-02 15:57:50 | WARN | http-nio-8088-exec-1 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors
Field error in object 'restUserInfoParam' on field 'id': rejected value [aaa]; codes [typeMismatch.restUserInfoParam.id,typeMismatch.id,typeMismatch.java.lang.Long,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [restUserInfoParam.id,id]; arguments []; default message [id]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'java.lang.Long' for property 'id'; nested exception is java.lang.NumberFormatException: For input string: "aaa"]
4.6 上傳文件爲空
展示層:
請求日誌:
2019-12-02 15:59:57 | WARN | http-nio-8088-exec-2 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.multipart.support.MissingServletRequestPartException, message: Required request part 'file' is not present
2019-12-02 15:59:57 | WARN | http-nio-8088-exec-2 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present
4.7 缺少請求參數
展示層:
請求日誌:
2019-12-02 16:02:16 | WARN | http-nio-8088-exec-6 | c.l.d.s.common.interceptor.GlobalExceptionHandlerGlobalExceptionHandler.java 42| class: org.springframework.web.bind.MissingServletRequestParameterException, message: Required String parameter 'passcode' is not present
2019-12-02 16:02:16 | WARN | http-nio-8088-exec-6 | o.s.w.s.m.m.a.ExceptionHandlerExceptionResolverJdk14Logger.java 87| Resolved exception caused by handler execution: org.springframework.web.bind.MissingServletRequestParameterException: Required String parameter 'passcode' is not present
5 參考資料推薦
Error Handling for REST with Spring
Handling Standard Spring MVC Exceptions
解決spring boot中rest接口404,500等錯誤返回統一的json格式
6 Github 源碼
Gtihub 源碼地址 : https://github.com/Flying9001/springBootDemo
個人公衆號:404Code,分享半個互聯網人的技術與思考,感興趣的可以關注.