最佳實踐 - API 錯誤處理
API 中的錯誤如何定義,請求過程中出錯或請求處理中出錯。API 無法解析傳遞的資料,API 本身有很多問題,甚至格式正確的請求也會進行失敗。在這兩種情況下,都需要進行分析查詢原因。
無論是程式碼形式的錯誤還是簡單的錯誤響應,錯誤程式碼可能是 API 領域中最有用的診斷元素,錯誤程式碼非常有用。API 響應階段中的錯誤程式碼是開發人員可以將故障傳達給使用者的基本方式。
編寫良好的錯誤程式碼
好的錯誤程式碼必須通過三個基本標準,才能真正發揮作用。好的錯誤程式碼應包括:
- 業務域標識,因此可以輕鬆確定問題的根源和領域;
- 內部參考 ID,用於特定於檔案的錯誤符號。在某些情況下,只要內部參考表中包含 HTTP 狀態碼方案或類似的參考資料,就可以替換 HTTP 狀態碼。
- 人工可讀的訊息,概述了當前錯誤的上下文,原因和一般解決方案。
業界主流的處理方式
curl https://graph.facebook.com/v2.9/me?fields=id%2Cname%2Cpicture%2C%20picture&access_token=xxxxxxxxxxx
複製程式碼
{
- error: {
message: "An active access token must be used to query information about the current user.",type: "OAuthException" ,code: 2500,fbtrace_id: "ABdaipBGDyGFOyVCgrBfL56"
}
}
複製程式碼
curl https://api.twitter.com/1.1/statuses/mentions_timeline.json
複製程式碼
{
- errors: [
- {
code: 215,message: "Bad Authentication data."
}
]
}
複製程式碼
錯誤程式碼的定義
- 請求過程中出錯,未進入處理邏輯。
{
"domain":"pay","code" :10501002,"message":"引數錯誤","errors":[
- {
"name":"bankNo","message":"銀行卡號不符合規範"
}
]
}
複製程式碼
- 請求處理中出錯
{
"domain":"order","code":111501002,"message":"支付通道網路異常"
}
複製程式碼
{
"domain":"user","code":100501001,"message":"對應的使用者不存在!"
}
複製程式碼
錯誤程式碼詳細說明:
- domain 定義了領域,方便定位錯誤的根源。
- code 定義了內部錯誤的編碼
- message 描述了錯誤的原因
- error 對部分具體性錯誤進行了詳細的說明
code 補充說明:異常碼說明是由 8位 數字組成,前三位系統標識(從100開始),中間兩位是模組標識(業務劃分),後三位是異常標識(特定異常) error 補充說明:當 message 不能準確描述錯誤產生的原因,需要細化每項錯誤說明時,可考慮使用 error 欄位,來補充說明錯誤項。 domain 補充說明:底層框架裡面封裝了部分異常處理,比如引數校驗錯誤這種 code 應該是全系統共用的,而不會有系統標識。導致就不能根據 code 識別出來是哪個系統發生錯誤了,鏈路一長就很難排查到底是哪的問題了,所以錯誤處理中動態去拿當前應用的業務域標識。
錯誤處理 - Spring Boot
定義 Response 模型
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
/**
* Result
*
* @author Weichao Li ([email protected])
* @since 2019-08-11
*/
@Data
@AllArgsConstructor
@ApiModel("統一 Response 返回值")
public class Result<T> implements Serializable {
private static final long serialVersionUID = 1L;
public static final long SUCCESS_CODE = 200L;
public static final String DEFAULT_SUCCESS_MESSAGE = "success";
@ApiModelProperty(name = "業務域或應用標識",notes = "僅當產生錯誤時會賦值該欄位")
private String domain;
@ApiModelProperty(name = "結果碼",notes = "正確響應時該值為 Result#SUCCESS_CODE,錯誤響應時為錯誤程式碼")
private long code;
@ApiModelProperty(name = "人工可讀的訊息",notes = "正確響應時該值為 Result#DEFAULT_SUCCESS_MESSAGE,錯誤響應時為錯誤資訊")
private String msg;
@ApiModelProperty(name = "響應體",notes = "正確響應時該值會被使用")
private T data;
/**
* 當驗證錯誤時,各項具體的錯誤資訊
*/
@ApiModelProperty("錯誤資訊")
private List<Error> errors;
public Result(T data) {
this.setData(data);
this.setCode(SUCCESS_CODE);
this.setMsg(DEFAULT_SUCCESS_MESSAGE);
}
public Result() {
this.setCode(SUCCESS_CODE);
this.setMsg(DEFAULT_SUCCESS_MESSAGE);
}
public void addError(String name,String message) {
if (this.errors == null) {
this.errors = new ArrayList<>();
}
this.errors.add(new Error(name,message));
}
@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel("統一 Response 返回值中錯誤資訊的模型")
public class Error {
@ApiModelProperty(name = "錯誤項",notes = "錯誤的具體項")
private String name;
@ApiModelProperty(name = "錯誤項說明",notes = "錯誤的具體項說明")
private String message;
}
}
複製程式碼
異常攔截器處理
Spring Boot 的專案已經對有一定的異常處理了,但是比較泛化不夠精細化,因此需要基礎框架對這些異常進行統一的捕獲並處理。Spring Boot 中有一個 @RestControllerAdvice 的註解,使用該註解表示開啟了全域性異常的捕獲,我們只需在自定義一個方法使用 ExceptionHandler 註解然後定義捕獲異常的型別即可對這些捕獲的異常進行統一的處理。 定義異常基礎類
import com.github.hicolors.best.practices.pojo.Result;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 擴充套件異常
*
* @author Weichao Li ([email protected])
* @since 2019-08-11
*/
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class ExtensionException extends RuntimeException {
/**
* 業務域
*/
private String domain;
/**
* 業務異常碼 ( 詳情參加檔案說明 )
*/
private Long code;
/**
* 業務異常資訊
*/
private String message;
/**
* 額外資料,可支援擴充套件
*/
private Object data;
/**
* cause
*/
private Throwable cause;
/**
* 業務域標識自動取當前服務
*
* @param code code
* @param message message
*/
public ExtensionException(Long code,String message) {
this.code = code;
this.message = message;
}
/**
* 指定業務域標識
*
* @param domain domain
* @param code code
* @param message message
*/
public ExtensionException(String domain,Long code,String message) {
this.domain = domain;
this.code = code;
this.message = message;
}
public ExtensionException(Result result) {
this.domain = result.getDomain();
this.code = result.getCode();
this.message = result.getMsg();
this.data = result.getData();
}
複製程式碼
}
全域性異常處理器 - 資訊列舉
import lombok.Getter;
/**
* WebMvc 模組異常碼定義
* <p>
* 系統標識:100
* 模組標識:02
*
* @author Weichao Li ([email protected])
* @since 2019-08-11
*/
@Getter
public enum EnumExceptionMessageWebMvc {
// 非預期異常
UNEXPECTED_ERROR(10002000L,"服務發生非預期異常,請聯絡管理員!"),PARAM_VALIDATED_UN_PASS(10002001L,"引數校驗(JSR303)不通過,請檢查引數或聯絡管理員!"),NO_HANDLER_FOUND_ERROR(10002002L,"未找到對應的處理器,請檢查 API 或聯絡管理員!"),HTTP_REQUEST_METHOD_NOT_SUPPORTED_ERROR(10002003L,"不支援的請求方法,請檢查 API 或聯絡管理員!"),HTTP_MEDIA_TYPE_NOT_SUPPORTED_ERROR(10002004L,"不支援的網際網路媒體型別,請檢查 API 或聯絡管理員"),;
private final Long code;
private final String message;
EnumExceptionMessageWebMvc(Long code,String message) {
this.code = code;
this.message = message;
}
}
複製程式碼
全域性異常處理器
import com.github.hicolors.best.practices.exception.ExtensionException;
import com.github.hicolors.best.practices.pojo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.text.MessageFormat;
import java.util.List;
import java.util.Objects;
/**
* ExceptionHandlerAdvice
*
* @author Weichao Li ([email protected])
* @since 2019/11/25
*/
@RestControllerAdvice
@Slf4j
public class ExceptionHandlerAdvice {
@Value("${spring.application.domain:${spring.application.name:unknown-spring-boot}}")
private String domain;
/**
* 針對業務異常的處理
*
* @param exception 業務異常
* @param request http request
* @param response http response
* @return 異常處理結果
*/
@ExceptionHandler(value = ExtensionException.class)
@SuppressWarnings("unchecked")
public Result extensionException(ExtensionException exception,HttpServletRequest request,HttpServletResponse response) {
log.warn("請求發生了預期異常,出錯的 url [{}],出錯的描述為 [{}]",request.getRequestURL().toString(),exception.getMessage());
Result result = new Result();
result.setDomain(StringUtils.isEmpty(exception.getDomain()) ? domain : exception.getDomain());
result.setCode(exception.getCode());
result.setMsg(exception.getMessage());
Object data = exception.getData();
if (Objects.nonNull(data) && data instanceof List) {
if (((List) data).size() > 0 && (((List) data).get(0) instanceof Result.Error)) {
result.setErrors((List<Result.Error>) data);
}
}
return result;
}
/**
* 針對引數校驗失敗異常的處理
*
* @param exception 引數校驗異常
* @param request http request
* @param response http response
* @return 異常處理結果
*/
@ExceptionHandler(value = {BindException.class,MethodArgumentNotValidException.class,ConstraintViolationException.class})
public Result databindException(Exception exception,HttpServletResponse response) {
log.error(MessageFormat.format("請求發生了非預期異常,出錯的 url [{0}],出錯的描述為 [{1}]",exception.getMessage()),exception);
Result result = new Result();
result.setDomain(domain);
result.setCode(EnumExceptionMessageWebMvc.PARAM_VALIDATED_UN_PASS.getCode());
result.setMsg(EnumExceptionMessageWebMvc.PARAM_VALIDATED_UN_PASS.getMessage());
if (exception instanceof BindException) {
for (FieldError fieldError : ((BindException) exception).getBindingResult().getFieldErrors()) {
result.addError(fieldError.getField(),fieldError.getDefaultMessage());
}
} else if (exception instanceof MethodArgumentNotValidException) {
for (FieldError fieldError : ((MethodArgumentNotValidException) exception).getBindingResult().getFieldErrors()) {
result.addError(fieldError.getField(),fieldError.getDefaultMessage());
}
} else if (exception instanceof ConstraintViolationException) {
for (ConstraintViolation cv : ((ConstraintViolationException) exception).getConstraintViolations()) {
result.addError(cv.getPropertyPath().toString(),cv.getMessage());
}
}
return result;
}
/**
* 針對spring web 中的異常的處理
*
* @param exception Spring Web 異常
* @param request http request
* @param response http response
* @return 異常處理結果
*/
@ExceptionHandler(value = {
NoHandlerFoundException.class,HttpRequestMethodNotSupportedException.class,HttpMediaTypeNotSupportedException.class
})
public Result springWebExceptionHandler(Exception exception,exception);
Result result = new Result();
result.setDomain(domain);
if (exception instanceof NoHandlerFoundException) {
result.setCode(EnumExceptionMessageWebMvc.NO_HANDLER_FOUND_ERROR.getCode());
result.setMsg(EnumExceptionMessageWebMvc.NO_HANDLER_FOUND_ERROR.getMessage());
} else if (exception instanceof HttpRequestMethodNotSupportedException) {
result.setCode(EnumExceptionMessageWebMvc.HTTP_REQUEST_METHOD_NOT_SUPPORTED_ERROR.getCode());
result.setMsg(EnumExceptionMessageWebMvc.HTTP_REQUEST_METHOD_NOT_SUPPORTED_ERROR.getMessage());
} else if (exception instanceof HttpMediaTypeNotSupportedException) {
result.setCode(EnumExceptionMessageWebMvc.HTTP_MEDIA_TYPE_NOT_SUPPORTED_ERROR.getCode());
result.setMsg(EnumExceptionMessageWebMvc.HTTP_MEDIA_TYPE_NOT_SUPPORTED_ERROR.getMessage());
} else {
result.setCode(EnumExceptionMessageWebMvc.UNEXPECTED_ERROR.getCode());
result.setMsg(EnumExceptionMessageWebMvc.UNEXPECTED_ERROR.getMessage());
}
return result;
}
/**
* 針對全域性異常的處理
*
* @param exception 全域性異常
* @param request http request
* @param response http response
* @return 異常處理結果
*/
@ExceptionHandler(value = Throwable.class)
public Result throwableHandler(Exception exception,exception);
Result result = new Result();
result.setDomain(domain);
result.setCode(EnumExceptionMessageWebMvc.UNEXPECTED_ERROR.getCode());
result.setMsg(EnumExceptionMessageWebMvc.UNEXPECTED_ERROR.getMessage());
return result;
}
}
複製程式碼
使用
- 業務開發異常使用
// 此處只是簡單演示,邏輯處理應該抽象在 mvc 分層中,業務開發過程中只需要拋異常即可。
@GetMapping
public String get() {
throw new ExtensionException(105001001L,"simple 資源不存在");
}
複製程式碼
- 基礎框架異常使用
model
@Data
public class ValidatedModel {
@NotNull(message = "id 不能為空")
@Min(value = 10,message = "id 不能小於 10")
private Long id;
@NotBlank(message = "name 不能為空")
@Length(max = 5,message = "name 長度不能超過 5")
private String name;
}
複製程式碼
controller
// 此處只是簡單演示
@PostMapping("/test/validated")
public String getx(@Validated @RequestBody ValidatedModel model) {
return model.getName();
}
複製程式碼
程式碼連結
招聘
潮流電商平臺行業獨角獸(毒APP)基礎架構團隊誠招 Java / Golang / Kubernetes 研發工程師/架構師,Base 上海楊浦互聯寶地,歡迎有興趣的同學投遞簡歷到 [email protected] 。