最佳实践 - API 错误处理

API 中的错误如何定义,请求过程中出错或请求处理中出错。API 无法解析传递的数据,API 本身有很多问题,甚至格式正确的请求也会进行失败。在这两种情况下,都需要进行分析查找原因。

无论是代码形式的错误还是简单的错误响应,错误代码可能是 API 领域中最有用的诊断元素,错误代码非常有用。API 响应阶段中的错误代码是开发人员可以将故障传达给用户的基本方式。

编写良好的错误代码

好的错误代码必须通过三个基本标准,才能真正发挥作用。好的错误代码应包括:

  • 业务域标识,因此可以轻松确定问题的根源和领域;
  • 内部参考 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":"对应的用户不存在!"
}
复制代码

错误代码详细说明:

  • 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, 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 上海杨浦互联宝地,欢迎有兴趣的同学投递简历到 [email protected]

猜你喜欢

转载自juejin.im/post/5de0bed36fb9a0717936269d
今日推荐