乐趣区

关于java:你还在统一返回-ApiResult-吗✋-duck-不必快来看-API-错误处理的最佳实践-✔️

分享 Java、.NET、Javascript、效率、软件工程、编程语言等技术常识。

本文 GitHub TechShare 已收录,没有广告,Just For Fun。

为什么写这篇文章?

置信不少 Java 开发都在我的项目中应用过相似 ApiResult 这样的对象来包装 Api 返回类型,这相比什么都不包装有肯定的益处,但这真的就是最好的做法吗?

对于封装 ResultBean 对象,晓风轻在他的 程序员你为什么这么累 系列文章中有过不错的分享,但对立封装 ResultBean 实际上也是一种反复工作,秉承 DRY 的理念,还有必要对其持续优化。

对立返回 ApiResult 还不是最佳实际,必须一直思考优化,就像 React 所提倡的 Rethinking Best Practices

ApiResult 现状

咱们先看一个常见的 ApiResult 对象,代码如下:

@Data
public class ApiResult<T> implements Serializable {
    private int code;
    private String message;
    private T data;
}

益处:客户端能够应用对立的解决形式。

存在的问题:

  1. 在对立返回 ApiResult 的状况下,即便是失常返回,也会带上 code、message 属性,少数状况 下属于 冗余
  2. Controller 层代码存在反复,返回对象反复定义、包装调用编写反复,代码整洁度降落
  3. 对立返回 200 状态码不利于 申请监控
  4. ApiResult 同时承当了 Api 后果和谬误后果的职责,不合乎 繁多职责 准则。

像上面这样一段获取列表数据的代码,若不波及业务预期内的申请验证,是没必要包装一层 ApiResult 的,什么是业务预期内的验证呢,举个例子,比方非会员无奈获取列表,业务上须要揭示用户购买会员,这属于非法申请,此时依然能够应用 ApiResult 携带 code 明确返回给客户端。

public ApiResult<List<Data>> demo() {return ApiResult.ok(getList());
}

ApiResult 要依据业务场景应用,不须要每个场景都应用它。

当 API 越来越多时,对立返回 ApiResult 的问题会被放大,如何解决这些问题呢?请接着看。

应用 HTTP 状态码

有许多我的项目采纳的形式是,在 API 调用胜利时应用失常的数据模型,而在呈现谬误时,返回相应的 HTTP 错误码 和形容信息。咱们看一段 jhipster 中的代码:

@GetMapping("/authors/{id}")
public ResponseEntity<AuthorDTO> getAuthor(@PathVariable Long id) {Optional<AuthorDTO> authorDTO = authorService.findOne(id);
    return ResponseUtil.wrapOrNotFound(authorDTO);
}

次要 HTTP 状态码的含意:

  • 1XX – Informational
  • 2XX – Success
  • 3XX – Redirection
  • 4XX – Client Error
  • 5XX – Server Error

采纳 HTTP 状态码就不再须要对立返回 ApiResult,但问题也随之而来,那就是 ApiResult 中定义的 error code 很难跟 HTTP 错误码一一对应,光有 HTTP 错误码和形容信息是不够的,还须要定义专门的谬误模型。

API 谬误模型

如何定义一个好的 API 谬误模型,这须要依据 业务的复杂程度 来定,咱们先来看看几个 Big Company 都是怎么做的。

先看 twitter 的,其中省略了无关的 HTTP 输入信息。

HTTP/1.1 400 Bad Request

{"errors":[{"code":215,"message":"Bad Authentication data."}]}

应用了错误码,并且谬误模型是一个数组,意味着可能会返回多个谬误。

再来看 Facebook 的 Graph API。

HTTP/1.1 200

{
  "error": {
    "message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\"at character 23: id,name,picture,picture",
    "type": "OAuthException",
    "code": 2500,
    "fbtrace_id": "xxxxxxxxxxx"
  }
}

留神,其返回的是对立的 200 状态码,谬误模型中还蕴含 异样类型trace_id,这两个属性有助于排查谬误。

最初看看巨头微软 Bing 的谬误模型。

HTTP/1.1 200

{
  "SearchResponse": {
    "Version": "2.2",
    "Query": {"SearchTerms": "api error codes"},
    "Errors": [
      {
        "Code": 1001,
        "Message": "Required parameter is missing.",
        "Parameter": "SearchRequest.AppId",
        "HelpUrl": "http\u003a\u002f\u002fmsdn.microsoft.com\u002fen-us\u002flibrary\u002fdd251042.aspx"
      }
    ]
  }
}

其返回的也是 200 状态码,但能够看到它应用了相似 ApiResult 的包装形式,并且还蕴含了 输出信息 输出参数 帮忙链接 ,原来这就是 大佬 的做事形式吗?

果然 API 谬误模型的设计,依据业务简单程序的不同,实现起来也不太一样,这三个中,咱们参考 twitter 的 API 设计 来看看在 Spring 我的项目中实现起来有哪些须要留神的,毕竟绝大多数我的项目的复杂度都达不到 FB 和 Bing 的水平。

Spring API 谬误模型实战

谬误模型的定义是非常简单的,代码如下。

ErrorResponse.java

@Data
public class ErrorResponse implements Serializable {private ErrorDetail error;}

ErrorDetail.java

@Data
public class ErrorDetail implements Serializable {
    private int code;
    private String message;
    private String type;
}

谬误详情中减少了一个 type 属性,能够帮忙更好地定位到异样。

Controller 层编写时至须要返回失常的数据模型,如 List、VO、DTO 之类。

异样应用 AOP 的形式来解决。

编写一个 ControllerAdvice 类,。

@ControllerAdvice
@ResponseBody
@Slf4j
public class CustomExceptionHandler {@ExceptionHandler(value = Exception.class)
    public ResponseEntity<ErrorResponse> exceptionHandler(Exception exception) {return serverErrorResponse(ApiCode.SYSTEM_EXCEPTION, exception);
    }

    private ResponseEntity<ErrorResponse> serverErrorResponse(ApiCode apiCode, Exception exception) {String message = apiCode.getMessage();
        // 服务端异样须要记录日志
        log.error(message, exception);
        // 服务端异样应用 api code 中的 message,防止敏感异样信息发送到客户端
        return new ResponseEntity<>(errorResponse(apiCode, ErrorMessageType.API_CODE, exception), HttpStatus.INTERNAL_SERVER_ERROR);
    }

    private ResponseEntity<ErrorResponse> requestErrorResponse(ApiCode apiCode, Exception exception) {String message = apiCode.getMessage();
        // 客户端申请谬误只记录 debug 日志
        if (log.isDebugEnabled()) {log.debug(message, exception);
        }
        // 客户端异样应用异样中的 message
        return new ResponseEntity<>(errorResponse(apiCode, ErrorMessageType.EXCEPTION, exception), HttpStatus.BAD_REQUEST);
    }

    private ErrorResponse errorResponse(ApiCode code, ErrorMessageType messageType, Exception exception) {ErrorDetail errorDetail = new ErrorDetail();
        errorDetail.setCode(code.getCode());
        if (messageType.equals(ErrorMessageType.API_CODE) || StrUtil.isBlank(exception.getMessage())) {errorDetail.setMessage(code.getMessage());
        } else {errorDetail.setMessage(exception.getMessage());
        }
        errorDetail.setType(exception.getClass().getSimpleName());

        ErrorResponse errorResponse = new ErrorResponse();
        errorResponse.setError(errorDetail);
        return errorResponse;
    }

    @ExceptionHandler(value = RequestVerifyException.class)
    public ResponseEntity<ErrorResponse> requestVerifyExceptionHandler(RequestVerifyException e) {return requestErrorResponse(ApiCode.PARAMETER_EXCEPTION, e);
    }

}

下面的代码只放了两个 ExceptionHandler,一个是针对 申请验证谬误 ,一个是针对 未知服务器谬误 ,别离对应的是 400500 的 HTTP 状态码。须要对其余异样做专门解决,也依然是应用以上的公共 errorResponse 办法,就看异样被定义为 申请异样 还是 服务端异样

至此,API 就能返回 "丑陋" 的谬误模型了。

完结了吗?

先别走,还没完结呢,如果失常和谬误状况下返回的数据模型不一样,那接口文档该如何定义呢?如果应用了 swagger,那么咱们须要增加针对 400 和 500 状态码的 全局输入模型

在最新版本的 springfox 中要实现起来还是有点吃力的,来看局部代码。

@Bean
public Docket createRestApi(TypeResolver typeResolver) {
    // 附加谬误模型
    Docket builder = new Docket(DocumentationType.SWAGGER_2)
            .host(swaggerProperties.getHost())
            .apiInfo(apiInfo(swaggerProperties))
            .additionalModels(typeResolver.resolve(ErrorResponse.class));

    // 增加 400 错误码输入模型
    List<Response> responseMessages = new ArrayList<>();
    ResponseBuilder responseBuilder = new ResponseBuilder();
    responseBuilder.code("400").description("");
    if (!StringUtils.isEmpty(globalResponseMessageBody.getModelRef())) {responseBuilder.representation(MediaType.APPLICATION_JSON)
            .apply(rep -> rep.model(m -> m.referenceModel(re -> re.key(key->key.qualifiedModelName(new QualifiedModelName("com.package.api","ErrorResponse")))
            )));
    }
    responseMessages.add(responseBuilder.build());

    builder.useDefaultResponseMessages(false)
        .globalResponses(HttpMethod.GET, responseMessages)
        .globalResponses(HttpMethod.POST, responseMessages);

    return builder.select().build();
}

以上仅为局部代码,次要在于 须要附加模型 并指定输入模型,在理论我的项目中应该将模型信息放在配置当中,依据配置主动增加,对于 swagger 的主动配置,若读者敌人感兴趣,能够有机会专门写篇文章来解说。

写在最初

在每个接口中返回对立的 ApiResult,笔者感觉是一件挺无聊的事件,写程序应该是一件能施展创造力的事件。一直去思考最佳实际,学习优良的设计,这件小小的事件,咱们在工作当中简直每天都会碰到,它是值得被改良的。

退出移动版