Лучшие практики — обработка ошибок API

Java

Как определяется ошибка в API, ошибка возникает при запросе или ошибка при обработке запроса. API не может разобрать переданные данные, у самого API много проблем, и даже правильно сформированные запросы не выполняются. В обоих случаях требуется анализ для выяснения причины.

Вероятно, самый полезный диагностический элемент в мире API, коды ошибок очень полезны, будь то ошибка в виде кода или простой ответ на ошибку. Коды ошибок на этапе ответа API — это основной способ, с помощью которого разработчики могут сообщать пользователям об ошибках.

напишите хороший код ошибки

Хороший код ошибки должен соответствовать трем основным критериям, чтобы быть действительно функциональным. Хороший код ошибки должен включать:

  • Идентификация бизнес-домена, чтобы можно было легко определить основную причину и область проблемы;
  • Идентификатор внутренней ссылки для символов ошибок, характерных для документа. В некоторых случаях коды состояния 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 определяет кодировку внутренней ошибки
  • сообщение описывает причину ошибки
  • error подробно описывает некоторые конкретные ошибки

Код Дополнительное описание: Описание кода исключения состоит из 8 цифр, первые три — системные идентификаторы (начиная со 100), средние две — идентификаторы модуля (бизнес-подразделения), а последние три — идентификаторы исключений (конкретные исключения) Дополнительное описание ошибки: если сообщение не может точно описать причину ошибки, и каждое описание ошибки необходимо уточнить, рассмотрите возможность использования поля ошибки для дополнения элемента ошибки. домен Дополнительное примечание: базовая структура инкапсулирует некоторую обработку исключений, например, ошибки проверки параметров, которые должны использоваться всей системой без идентификации системы. В результате по коду невозможно определить, в какой системе ошибка, а при длинной ссылке сложно выяснить, в чем проблема, поэтому идентификатор бизнес-домена текущего приложения динамически получается во время обработка ошибок.

Обработка ошибок — Spring Boot

Определите модель ответа

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 в пользовательском методе, а затем определить тип захваченного исключения для единообразной обработки этих захваченных исключений. Определить базовый класс исключения

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();
    }

}

Глобальный обработчик исключений — перечисление информации

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 R & D, Base Shanghai Yangpu Internet Treasure, приглашаем заинтересованных студентов отправлять свои резюме на liweichao0102@gmail.com.