使用 Spring Boot 提供有用的 API 错误消息

对于 API 用户来说,API 提供有用的错误消息非常重要。否则,可能很难弄清楚为什么事情不起作用。与在服务器端实际实现有用的错误响应相比,调试错误可能很快成为客户端的更大工作量。如果客户无法自己解决问题并且需要额外的沟通,则尤其如此。

尽管如此,这个话题经常被忽视或三心二意地实施。

客户端和安全视角

对错误消息有不同的看法。详细的错误消息对客户更有帮助,而从安全角度来看,最好尽可能少地公开信息。幸运的是,如果正确实施,这两种观点通常不会发生太大冲突。

如果错误是由客户产生的,客户通常对非常具体的错误消息感兴趣。这通常应由4xx 状态代码指示。在这里,我们需要特定的消息来指出客户端所犯的错误,而不暴露任何内部实现细节。

另一方面,如果客户端请求有效并且服务器产生错误(5xx 状态码),我们应该对错误消息保持保守。在这种情况下,客户端无法解决问题,因此不需要有关错误的任何详细信息。

指示错误的响应应至少包含两件事:人类可读的消息和错误代码。第一个帮助开发人员在日志文件中看到错误消息。后者允许在客户端上进行特定的错误处理(例如向用户显示特定的错误消息)。

如何在 Spring Boot 应用程序中构建有用的错误响应?

假设我们有一个可以发布文章的小型应用程序。执行此操作的简单 Spring 控制器可能如下所示:

@RestController
public class ArticleController {
 
    @Autowired
    private ArticleService articleService;
 
    @PostMapping("/articles/{id}/publish")
    public void publishArticle(@PathVariable ArticleId id) {
        articleService.publishArticle(id);
    }
}

这里没什么特别的,控制器只是将操作委托给服务,如下所示:

@Service
public class ArticleService {
 
    @Autowired
    private ArticleRepository articleRepository;
 
    public void publishArticle(ArticleId id) {
        Article article = articleRepository.findById(id)
                .orElseThrow(() -> new ArticleNotFoundException(id));
 
        if (!article.isApproved()) {
            throw new ArticleNotApprovedException(article);
        }
 
        ...
    }
}

在服务内部,我们针对可能的客户端错误抛出特定异常。请注意,这些异常不仅仅描述了错误。它们还带有可能帮助我们稍后产生良好错误消息的信息:

public class ArticleNotFoundException extends RuntimeException {
    private final ArticleId articleId;
 
    public ArticleNotFoundException(ArticleId articleId) {
        super(String.format("No article with id %s found", articleId));
        this.articleId = articleId;
    }
     
    // getter
}

如果异常足够具体,我们不需要通用消息参数。相反,我们可以在异常构造函数中定义消息。

接下来我们可以在@ControllerAdvice bean 中使用@ExceptionHandler 方法来处理实际的异常:

@ControllerAdvice
public class ArticleExceptionHandler {
 
    @ExceptionHandler(ArticleNotFoundException.class)
    public ResponseEntity<ErrorResponse> onArticleNotFoundException(ArticleNotFoundException e) {
        String message = String.format("No article with id %s found", e.getArticleId());
        return ResponseEntity
                .status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("ARTICLE_NOT_FOUND", message));
    }
     
    ...
}

如果控制器方法抛出异常,Spring 会尝试查找带有匹配 @ExceptionHandler 注释的方法。@ExceptionHandler 方法可以具有灵活的方法签名,类似于标准控制器方法。例如,我们可以一个 HttpServletRequest 请求参数,Spring 会传入当前的请求对象。@ExceptionHandler的 Javadocs 中描述了可能的参数和返回类型。

在此示例中,我们创建了一个简单的 ErrorResponse 对象,该对象由错误代码和消息组成。

消息是根据异常携带的数据构造的。也可以将异常消息传递给客户端。但是,在这种情况下,我们需要确保团队中的每个人都知道这一点,并且异常消息不包含敏感信息。否则,我们可能会不小心将内部信息泄露给客户端。

ErrorResponse 是一个用于 JSON 序列化的简单 Pojo:

public class ErrorResponse {
    private final String code;
    private final String message;
 
    public ErrorResponse(String code, String message) {
        this.code = code;
        this.message = message;
    }
 
    // getter
}

测试错误响应

一个好的测试套件不应该错过针对特定错误响应的测试。在我们的示例中,我们可以用不同的方式验证错误行为。一种方法是使用Spring MockMvc测试。

例如:

@SpringBootTest
@AutoConfigureMockMvc
public class ArticleExceptionHandlerTest {
 
    @Autowired
    private MockMvc mvc;
 
    @MockBean
    private ArticleRepository articleRepository;
 
    @Test
    public void articleNotFound() throws Exception {
        when(articleRepository.findById(new ArticleId("123"))).thenReturn(Optional.empty());
 
        mvc.perform(post("/articles/123/publish"))
                .andExpect(status().isNotFound())
                .andExpect(jsonPath("$.code").value("ARTICLE_NOT_FOUND"))
                .andExpect(jsonPath("$.message").value("No article with id 123 found"));
    }
}

在这里,我们使用了一个模拟的 ArticleRepository,它为传递的 id 返回一个空的 Optional。然后我们验证错误代码和消息是否与预期的字符串匹配。

如果您想了解更多关于使用 mock mvc 测试 Spring 应用程序的信息:我最近写了一篇文章,展示了如何改进 Mock mvc 测试。

概括

有用的错误消息是 API 的重要组成部分。

如果客户端产生错误(HTTP 4xx 状态代码),服务器应提供描述性错误响应,其中至少包含错误代码和人类可读的错误消息。对意外服务器错误 (HTTP 5xx) 的响应应该是保守的,以避免意外暴露任何内部信息。

为了提供有用的错误响应,我们可以使用携带相关数据的特定异常。然后在@ExceptionHandler 方法中,我们根据异常数据构造错误消息。

学习更多JAVA知识与技巧,关注与私信博主(学习)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值