【SpringBoot】統一返回物件和統一異常處理
為什麼要統一異常
Java異常分為unchecked和checked,對於unchecked的那些異常一般都是程式碼寫的有問題,比如你沒有處理null物件,直接就用這個物件的方法或者屬性了(NullPointException),或者是除0(ArithmeticException),或者是陣列下標越界了(ArrayIndexOutOfBoundsException),這種的你要是能意識到try或者throw,那肯定不可能寫錯了。
但是對於checked,就是java要求我們處理的異常了,比如說SQLException , IOException,ClassNotFoundException,如果我們不做統一處理,那前端收到的會是一坨異常呼叫棧,非常恐怖,而且花樣繁多,比如hibernate-validator的異常棧....
還有就是業務異常,這種的都是自定義的異常,一般都是基於RuntimeException改的。
所以,為了把這些亂七八糟的都統一起來,按照與前端約定好的格式返回,統一異常非常有必要。
為什麼要統一返回值
不統一的話,返回值會五花八門,對於前端來說無法做一些統一處理,比如說統一通過狀態為快速判斷介面呼叫情況,介面呼叫失敗原因獲取每個介面都要自定義一個。
如果統一了,則前端可以寫一個呼叫回撥解析方法,就能快速獲取介面呼叫情況了,十分便捷。如下程式碼:
{ "state": false, "code": "-1", "data": "資料庫中未查詢到該學生", "timestamp": 1640142947034 }
前端可以通過code直接判斷介面呼叫情況,通過data獲取異常資訊,或者是需要查詢的資料。
如何實現統一異常
Spring為我們提供了一個註解:@ControllerAdvice
@ControllerAdvice @Slf4j public class GlobalExceptionHandler { /** * 處理自定義的業務異常 * @param e * @return */ @ExceptionHandler(value = BusinessException.class) @ResponseBody public ResponseEntity businessExceptionHandler(BusinessException e){ log.error("發生業務異常!原因是:{}",e.getMessage()); return ResponseHelper.failed(e.getMessage()); } }
我們只需要在@ExceptionHandler這裡寫上想攔截處理的異常,就可以了,在該方法中,你可以把關於這個異常的資訊獲取出來自由拼接,然後通過統一返回格式返回給前端,方便前端處理展示。
如Hibernate-validator報的異常非常恐怖,中間疊了好幾層:
Validation failed for argument [0] in public com.example.template.entity.ValidationTestVo com.example.template.controller.HibernateValidatorController.testValidation(com.example.template.entity.ValidationTestVo) with 4 errors: [Field error in object 'validationTestVo' on field 'userId': rejected value [122121]; codes [Size.validationTestVo.userId,Size.userId,Size.java.lang.String,Size]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.userId,userId]; arguments []; default message [userId],5,1]; default message [使用者ID長度必須在1-5之間]] [Field error in object 'validationTestVo' on field 'age': rejected value [213]; codes [Range.validationTestVo.age,Range.age,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.age,age]; arguments []; default message [age],200,0]; default message [年齡需要在0-200中間]] [Field error in object 'validationTestVo' on field 'email': rejected value [12112]; codes [Email.validationTestVo.email,Email.email,Email.java.lang.String,Email]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.email,email]; arguments []; default message [email],[Ljavax.validation.constraints.Pattern$Flag;@6215aa90,.*]; default message [【郵箱】格式不規範]] [Field error in object 'validationTestVo' on field 'userName': rejected value [/;p[]; codes [Pattern.validationTestVo.userName,Pattern.userName,Pattern.java.lang.String,Pattern]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [validationTestVo.userName,userName]; arguments []; default message [userName],[Ljavax.validation.constraints.Pattern$Flag;@1bc38bc4,^([\\u4e00-\\u9fa5]{1,20}|[a-zA-Z\\.\\s]{1,20})$]; default message [名字只能輸入中文、英文,且在20個字元以內]]
這種的就算是返給前端,他們也得解半天,這個情況就可以通過統一攔截處理:
/**
* 處理引數校驗失敗異常
* @param e
* @return
*/
@ExceptionHandler(value = MethodArgumentNotValidException.class)
@ResponseBody
public ResponseEntity exceptionHandler(MethodArgumentNotValidException e){
// 1.校驗
Boolean fieldErrorUnobtainable = (e == null || e.getBindingResult() == null
|| CollectionUtils.isEmpty(e.getBindingResult().getAllErrors()) || e.getBindingResult().getAllErrors().get(0) == null);
if (fieldErrorUnobtainable) {
return ResponseHelper.successful();
}
// 2.錯誤資訊
StringBuilder sb = new StringBuilder();
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
if(!CollectionUtils.isEmpty(allErrors)){
for (Object fieldError_temp:allErrors) {
FieldError fieldError = (FieldError) fieldError_temp;
String objectName = fieldError.getObjectName();
String field = fieldError.getField();
String defaultMessage = fieldError.getDefaultMessage();
sb.append(objectName).append(".").append(field).append(":").append(defaultMessage).append(";");
}
}
// 3.return
log.error("引數校驗失敗!原因是:{}",sb.toString());
return ResponseHelper.failed(sb.toString());
}
返回結果變為:
{
"state": false,
"code": "-1",
"data": "validationTestVo.age:年齡需要在0-200中間;validationTestVo.userId:使用者ID長度必須在1-5之間;validationTestVo.email:【郵箱】格式不規範;validationTestVo.userName:名字只能輸入中文、英文,且在20個字元以內;",
"timestamp": 1640145039385
}
如何實現自定義業務異常
原先丟擲業務異常的時候,都是直接new RuntimeException,這樣不是很友好,我們可以基於RuntimeException寫一個BusinessException,主要優點是可以自定義異常返回資訊內容格式。
public class BusinessException extends RuntimeException{
private static final long serialVersionUID = 1L;
protected IExceptionCode exCode;
protected String[] params;
public BusinessException(IExceptionCode code) {
super(code.getError());
this.exCode = code;
}
public BusinessException(String message) {
super(message);
}
public BusinessException(IExceptionCode code, String[] params) {
super(code.getError());
this.exCode = code;
this.params = params;
}
public IExceptionCode getExCode() {
return this.exCode;
}
protected String parseMessage(String message) {
if (this.params == null) {
return message;
} else {
String errorString = this.exCode.getError();
for(int i = 0; i < this.params.length; ++i) {
errorString = errorString.replace("{" + i + "}", this.params[i]);
}
return errorString;
}
}
public String getMessage() {
return this.exCode != null && !"".equals(this.exCode.getCode()) ? this.exCode.getCode() + ":" + this.parseMessage(this.exCode.getError()) : super.getMessage();
}
}
其中的IExceptionCode,是規範自定義業務異常用的
public interface IExceptionCode {
String getError();
String getCode();
}
比如我們想寫一個自定義異常列舉類:
public enum BusinessExCode implements IExceptionCode {
DATABASE_NOT_FOUND("000001", "未在資料庫中找到指定資料");
private String code;
private String error;
BusinessExCode(String code, String error) {
this.code = code;
this.error = error;
}
@Override
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
@Override
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
寫例子:
@Override
public String testException() {
if(在字典表中的應該有的資料==null){
throw new BusinessException(BusinessExCode.DATABASE_NOT_FOUND);
}
}
測試結果:
{
"state": false,
"code": "-1",
"data": "000001:未在資料庫中找到指定資料",
"timestamp": 1640161941876
}