你还在统一返回ApiResult吗?duck不必,来看API错误处理最佳实践

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

为什么写这篇文章?

相信不少 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 层代码存在重复,返回对象重复定义、包装调用编写重复。
public ApiResult<List<Data>> demo() {
    return ApiResult.ok(getList());
}
复制代码

当 API 越来越多时,这些存在的问题会被被放大,如何解决这些问题呢?请接着看。

使用 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 ,一个是针对 请求验证错误 ,一个是针对 未知服务器错误 ,分别对应的是 400 和 500 的 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,笔者觉得是一件挺无聊的事情,写程序应该是一件能发挥创造力的事情。不断去思考最佳实践,学习优秀的设计,这件小小的事情,我们在工作当中几乎每天都会碰到,它是值得被改进的。

本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
1月前
|
缓存 前端开发 API
构建高效可扩展的RESTful API:后端开发的最佳实践
【2月更文挑战第30天】 在现代Web应用和服务端架构中,RESTful API已成为连接前端与后端、实现服务间通信的重要接口。本文将探讨构建一个高效且可扩展的RESTful API的关键步骤和最佳实践,包括设计原则、性能优化、安全性考虑以及错误处理机制。通过这些实践,开发者可以确保API的健壮性、易用性和未来的可维护性。
|
1月前
|
API 开发者 UED
深入探讨RESTful API设计原则及最佳实践
在当今互联网时代,RESTful API已成为各种软件系统之间进行通信的重要方式。本文将从资源定义、URI设计、HTTP方法选择、状态码规范等方面深入探讨RESTful API设计的原则与最佳实践,帮助开发者更好地构建高效、健壮的API。
|
2月前
|
数据建模 API 定位技术
详解 API 设计最佳实践
应用程序接口(API)是一种接口,它让应用程序可以轻松地使用另一个应用程序的数据和资源,API 对于一个产品或公司的成功至关重要。 如果没有 API,你大部分喜欢的软件今天就不会存在。例如,Google Maps API 可以让你在 app 或 Web 应用中使用 Google Maps。如果没有它,你将不得不设计和开发自己的地图数据库。这样的话,在地图上显示一个位置需要花费多少时间?
|
3月前
|
安全 API 数据安全/隐私保护
API安全性最佳实践
API安全性最佳实践
|
3月前
|
安全 测试技术 API
API测试清单和最佳实践
API测试清单和最佳实践
API测试清单和最佳实践
|
3月前
|
JSON API 数据格式
RESTful API 最佳实践
RESTful API 最佳实践
119 0
|
3月前
|
缓存 供应链 安全
淘宝API接口调用:案例分析与最佳实践(续)
淘宝API接口是连接商家与淘宝平台强大功能的重要桥梁。通过案例分析和最佳实践的分享,我们希望商家能够更深入地理解如何有效地使用这些API来优化电商业务。随着技术的不断进步,淘宝API的功能将会越来越丰富,而商家面临的挑战也会越来越大。因此,商家需要不断地学习新技术、探索新方法,并且不断完善自己的API使用策略,以便更好地适应市场的变化,赢得竞争的优势。
|
3月前
|
供应链 搜索推荐 API
淘宝API接口调用:案例分析与最佳实践
在电子商务迅猛发展的今天,淘宝作为中国最大的在线购物平台之一,为商家们提供了强大的数据分析和市场洞察工具——淘宝API。有效的API调用不仅可以提升商家的运营效率,还可以帮助商家更好地理解消费者需求、优化商品布局、提高用户满意度等。本文将通过案例分析和最佳实践探讨如何高效利用淘宝API接口。
|
3月前
|
JSON 缓存 API
title: 深入理解REST API设计的最佳实践
title: 深入理解REST API设计的最佳实践
36 0
|
3月前
|
机器学习/深度学习 安全 数据挖掘
电商API接口的最佳实践与案例分析
随着电商行业的快速发展,越来越多的企业开始将业务拓展到线上。为了提高用户体验和运营效率,电商平台提供了丰富的API接口,方便商家进行商品管理、订单处理、营销活动等操作。本文将介绍电商API接口的最佳实践和案例分析。