你还在统一返回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给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。

相关文章
|
5月前
|
监控 安全 API
电商API安全最佳实践:保护用户数据免受攻击
在电商领域,API是连接用户、商家和支付系统的核心枢纽,但也常成为黑客攻击目标。本文系统介绍电商API安全的最佳实践,涵盖HTTPS加密、强认证授权、输入验证、速率限制、日志监控、安全审计及数据加密等关键措施,帮助您有效防范数据泄露与攻击风险,保障业务安全稳定运行。
187 0
|
2月前
|
存储 监控 安全
132_API部署:FastAPI与现代安全架构深度解析与LLM服务化最佳实践
在大语言模型(LLM)部署的最后一公里,API接口的设计与安全性直接决定了模型服务的可用性、稳定性与用户信任度。随着2025年LLM应用的爆炸式增长,如何构建高性能、高安全性的REST API成为开发者面临的核心挑战。FastAPI作为Python生态中最受青睐的Web框架之一,凭借其卓越的性能、强大的类型安全支持和完善的文档生成能力,已成为LLM服务化部署的首选方案。
|
4月前
|
人工智能 JSON 前端开发
Mock 在 API 研发中的痛点、价值与进化及Apipost解决方案最佳实践
在 API 开发中,Mock 技术能有效解决后端接口未就绪带来的开发阻碍,保障前端独立高效开发。本文通过电商平台支付接口的实例,分析了常见 Mock 方案的局限性,并深入介绍了 Apipost 提供的灵活 Mock 能力:从固定值返回,到使用内置函数生成动态数据,再到自定义函数处理复杂逻辑,最后实现根据请求参数返回不同响应。这些能力不仅提升了开发效率,也增强了测试的全面性,为前后端协作提供了更高效的解决方案。
232 3
|
6月前
|
存储 监控 安全
电商API安全指南:保护数据与防止欺诈的最佳实践
在电商领域,API安全至关重要。本文从基础到实践,全面解析电商API安全策略:通过强认证、数据加密、输入验证及访问控制保护敏感数据;借助速率限制、欺诈检测与行为分析防范恶意行为。同时,强调将安全融入开发生命周期,并提供应急计划。遵循最佳实践,可有效降低风险,增强用户信任,助力业务稳健发展。
217 4
|
5月前
|
存储 安全 NoSQL
【干货满满】API安全加固指南:签名防篡改+Access Token管理最佳实践
API 安全关乎业务与用户隐私,签名机制防篡改、伪造请求,Access Token 管理身份与权限。本文详解签名生成、Token 类型与管理、常见安全问题及最佳实践,助开发者构建安全可靠的 API 体系。
|
8月前
|
人工智能 运维 关系型数据库
云服务API与MCP深度集成,RDS MCP最佳实践
近日,阿里云数据库RDS发布开源RDS MCP Server,将复杂的技术操作转化为自然语言交互,实现"对话即运维"的流畅体验。通过将RDS OpenAPI能力封装为MCP协议工具,用户只需像聊天一样描述需求,即可完成数据库实例创建、性能调优、故障排查等专业操作。本文介绍了RDS MCP(Model Context Protocol)的最佳实践及其应用,0代码,两步即可轻松完成RDS实例选型与创建,快来体验!
云服务API与MCP深度集成,RDS MCP最佳实践
|
8月前
|
JSON 测试技术 API
书写API文档的最佳实践📚
API文档对开发者体验和API成功至关重要。本文探讨了编写清晰、全面且友好的API文档的最佳实践,包括定义API目的、结构化文档、提供代码示例、处理错误、版本控制及测试验证等关键步骤。通过实际案例(如WeatherAPI),展示了如何优化文档内容,帮助开发者快速上手并高效使用API。同时强调交互式功能、国际化支持和用户反馈的重要性,以提升文档的可用性和全球可达性。高质量文档不仅能推动API采用率,还能培养强大的开发者社区,为API的长期成功奠定基础。
|
JavaScript 前端开发 API
探索组合式API与Options API的对比及最佳实践
探索组合式API与Options API的对比及最佳实践
407 83
|
11月前
|
弹性计算 监控 安全
API稳定安全最佳实践:用阿里云SDK为业务保驾护航
阿里云智能集团高级技术专家赵建强和曹佩杰介绍了API稳定安全最佳实践,涵盖业务上云真实案例、集成开发最佳实践、配额管理和共担模型四部分。通过分析企业在不同阶段遇到的问题,如签名报错、异常处理不严谨、扩容失败等,提出了解决方案和工具,确保API调用的安全性和稳定性。特别强调了SDK的使用、无AK方案、自动刷新机制以及配额中心的作用,帮助用户构建更稳定、安全的服务,提升运维效率。最终介绍了集成开发共担模型,旨在通过最佳实践和平台工具,保障业务的稳定与安全,推动行业创新与发展。
|
存储 安全 Java
Spring Boot 编写 API 的 10条最佳实践
本文总结了 10 个编写 Spring Boot API 的最佳实践,包括 RESTful API 设计原则、注解使用、依赖注入、异常处理、数据传输对象(DTO)建模、安全措施、版本控制、文档生成、测试策略以及监控和日志记录。每个实践都配有详细的编码示例和解释,帮助开发者像专业人士一样构建高质量的 API。
404 9