最佳實踐 - API 錯誤處理

API 中的錯誤如何定義,請求過程當中出錯或請求處理中出錯。API 沒法解析傳遞的數據,API 自己有不少問題,甚至格式正確的請求也會進行失敗。在這兩種狀況下,都須要進行分析查找緣由。java

不管是代碼形式的錯誤仍是簡單的錯誤響應,錯誤代碼多是 API 領域中最有用的診斷元素,錯誤代碼很是有用。API 響應階段中的錯誤代碼是開發人員能夠將故障傳達給用戶的基本方式。git

編寫良好的錯誤代碼

好的錯誤代碼必須經過三個基本標準,才能真正發揮做用。好的錯誤代碼應包括:github

  • 業務域標識,所以能夠輕鬆肯定問題的根源和領域;
  • 內部參考 ID,用於特定於文檔的錯誤符號。在某些狀況下,只要內部參考表中包含 HTTP 狀態碼方案或相似的參考資料,就能夠替換 HTTP 狀態碼。
  • 人工可讀的消息,概述了當前錯誤的上下文,緣由和通常解決方案。

業界主流的處理方式

  • facebook
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"
    }
}
複製代碼
  • Twitter
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":"對應的用戶不存在!"
}
複製代碼

錯誤代碼詳細說明:web

  • domain 定義了領域,方便定位錯誤的根源。
  • code 定義了內部錯誤的編碼
  • message 描述了錯誤的緣由
  • error 對部分具體性錯誤進行了詳細的說明

code 補充說明:異常碼說明是由 8位 數字組成,前三位系統標識(從100開始),中間兩位是模塊標識(業務劃分),後三位是異常標識(特定異常) error 補充說明:當 message 不能準確描述錯誤產生的緣由,須要細化每項錯誤說明時,可考慮使用 error 字段,來補充說明錯誤項。 domain 補充說明:底層框架裏面封裝了部分異常處理,好比參數校驗錯誤這種 code 應該是全系統共用的,而不會有系統標識。致使就不能根據 code 識別出來是哪一個系統發生錯誤了,鏈路一長就很難排查究竟是哪的問題了,因此錯誤處理中動態去拿當前應用的業務域標識。spring

錯誤處理 - 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 (liweichao0102@gmail.com)
 * @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 註解而後定義捕獲異常的類型便可對這些捕獲的異常進行統一的處理。 定義異常基礎類json

import com.github.hicolors.best.practices.pojo.Result;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

/**
 * 擴展異常
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @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();
    }
複製代碼

}api

全局異常處理器 - 信息枚舉bash

import lombok.Getter;
/**
 * WebMvc 模塊異常碼定義
 * <p>
 * 系統標識:100
 * 模塊標識:02
 *
 * @author Weichao Li (liweichao0102@gmail.com)
 * @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 (liweichao0102@gmail.com)
 * @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, HttpServletRequest request, HttpServletResponse response) {
        log.error(MessageFormat.format("請求發生了非預期異常,出錯的 url [{0}],出錯的描述爲 [{1}]",
                request.getRequestURL().toString(), 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, HttpServletRequest request, HttpServletResponse response) {
        log.error(MessageFormat.format("請求發生了非預期異常,出錯的 url [{0}],出錯的描述爲 [{1}]",
                request.getRequestURL().toString(), exception.getMessage()), 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, HttpServletRequest request, HttpServletResponse response) {
        log.error(MessageFormat.format("請求發生了非預期異常,出錯的 url [{0}],出錯的描述爲 [{1}]",
                request.getRequestURL().toString(), exception.getMessage()), 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 上海楊浦互聯寶地,歡迎有興趣的同窗投遞簡歷到 liweichao0102@gmail.com 。

相關文章
相關標籤/搜索