51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

在 Spring Boot 中使用 ProblemDetail 返回错误

1、简介 {#1简介}

本文将带你了解如何在 Spring Boot 应用中使用 ProblemDetail 响应错误信息,无论我们处理的是 REST API 还是 Reactive Stream(响应式流),它都提供了一种向客户端传达错误的标准化方式。

2、为什么要关注 ProblemDetail? {#2为什么要关注-problemdetail}

使用 ProblemDetail 来标准化错误响应对任何 API 都至关重要。

它可以帮助客户理解和处理错误,提高 API 的可用性和可调试性。这将带来更好的开发体验和更强大的应用。

采用它还有助于提供更翔实的错误信息,这对维护我们的服务和排除故障至关重要。

3、传统的错误处理方式 {#3传统的错误处理方式}

ProblemDetail 之前,我们经常在 Spring Boot 中实现自定义 ExceptionHandlerResponseEntity 来处理错误。我们会创建自定义的错误响应结构。这导致了不同 API 之间的不一致性。

这种方式不仅需要大量的模板代码。而且,缺乏表示错误的标准化方式,因此客户端很难统一解析和理解错误信息。

4、ProblemDetail 规范 {#4problemdetail-规范}

ProblemDetail 规范是 RFC 7807 标准的一部分。它为错误响应定义了一致的结构,包括诸如类型(type)、标题(title)、状态(status)、详情(detail)和实例(instance)等字段。这种标准化提供了一个通用的错误信息格式,有助于 API 开发人员和使用者。

实现 ProblemDetail 可确保我们的错误响应具有可预测性并易于理解。这反过来提高了我们的 API 和其客户端之间的整体沟通效果。

5、在 Spring Boot 中实现 ProblemDetail {#5在-spring-boot-中实现-problemdetail}

在 Spring Boot 中有多种方法可以实现 ProblemDetail。

5.1、通过配置属性启用 ProblemDetail {#51通过配置属性启用-problemdetail}

我们可以添加一个配置属性来启用它。对于 RESTful 服务,在 application.properties 中添加以下属性:

spring.mvc.problemdetails.enabled=true

此属性可使 ProblemDetail 自动用于基于 MVC(servlet 栈)的应用中的错误处理。

对于响应式应用,我们可以添加以下属性:

spring.webflux.problemdetails.enabled=true

启用后,Spring 会使用 ProblemDetail 报告错误:

{
    "type": "about:blank",
    "title": "Bad Request",
    "status": 400,
    "detail": "Invalid request content.",
    "instance": "/sales/calculate"
}

该属性会在错误处理中自动提供 ProblemDetail。如果不需要,我们也可以将其关闭。

5.2、在 ExceptionHandler 中实现 ProblemDetail {#52在-exceptionhandler-中实现-problemdetail}

全局异常处理器在 Spring Boot REST 应用中实现了集中式错误处理。

来看一个计算折扣价格的简单 REST 服务。

它接收操作请求并返回结果。此外,它还执行输入验证和业务逻辑。

请求参数如下:

public record OperationRequest(
    @NotNull(message = "Base price should be greater than zero.")
    @Positive(message = "Base price should be greater than zero.")
        Double basePrice,
    @Nullable @Positive(message = "Discount should be greater than zero when provided.")
        Double discount) {}

返回的结果如下:

public record OperationResult(
    @Positive(message = "Base price should be greater than zero.") Double basePrice,
    @Nullable @Positive(message = "Discount should be greater than zero when provided.")
        Double discount,
    @Nullable @Positive(message = "Selling price should be greater than zero.")
        Double sellingPrice) {}

下面是无效操作异常的实现:

public class InvalidInputException extends RuntimeException {

    public InvalidInputException(String s) {
        super(s);
    }
}

现在,实现 REST Controller,为端点提供服务:

@RestController
@RequestMapping("sales")
public class SalesController {

    @PostMapping("/calculate")
    public ResponseEntity<OperationResult> calculate(
        @Validated @RequestBody OperationRequest operationRequest) {
    
        OperationResult operationResult = null;
        Double discount = operationRequest.discount();
        if (discount == null) {
            operationResult =
                new OperationResult(operationRequest.basePrice(), null, operationRequest.basePrice());
        } else {
            if (discount.intValue() >= 100) {
                throw new InvalidInputException("Free sale is not allowed.");
            } else if (discount.intValue() > 30) {
                throw new IllegalArgumentException("Discount greater than 30% not allowed.");
            } else {
                operationResult = new OperationResult(operationRequest.basePrice(),
                    discount,
                    operationRequest.basePrice() * (100 - discount) / 100);
            }
        }
        return ResponseEntity.ok(operationResult);
    }
}

SalesController 类在 /sales/calculate 端点处理 HTTP POST 请求。

它检查并验证 OperationRequest 对象。如果请求有效,它就会计算销售价格,并考虑可选折扣。如果折扣无效(超过 100% 或超过 30% ),则会抛出异常。如果折扣有效,它将通过应用折扣计算出最终价格,并返回一个封装在 ResponseEntity 中的 OperationResult

现在,来看看如何在 GlobalExceptionHandler 中实现 ProblemDetail

@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(InvalidInputException.class)
    public ProblemDetail handleInvalidInputException(InvalidInputException e, WebRequest request) {
        ProblemDetail problemDetail
            = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, e.getMessage());
        problemDetail.setInstance(URI.create("discount"));
        return problemDetail;
    }
}

GlobalExceptionHandler 类使用 @RestControllerAdvice 进行注解,它继承了 ResponseEntityExceptionHandler,以便在 Spring Boot 应用中提供集中式异常处理。

它定义了一种处理 InvalidInputException 异常的方法。当出现这种异常时,它会创建一个 ProblemDetail 对象,该对象具有 BAD_REQUEST 状态和异常消息。此外,它还会将该实例设置为一个 URI("discount"),以表明错误的具体情况。

这种标准化的错误响应可为客户提供清晰详细的信息,说明出错的原因。

ResponseEntityExceptionHandler 是一个便于在不同应用中以标准化方式处理异常的类。因此,将异常转换为有意义的 HTTP 响应的过程得以简化。此外,它还提供了使用 ProblemDetail 来处理常见 Spring MVC 异常的方法,如 MissingServletRequestParameterExceptionMethodArgumentNotValidException 等。

5.3、测试 ProblemDetail 实现 {#53测试-problemdetail-实现}

测试如下:

@Test
void givenFreeSale_whenSellingPriceIsCalculated_thenReturnError() throws Exception {

    OperationRequest operationRequest = new OperationRequest(100.0, 140.0);
    mockMvc
      .perform(MockMvcRequestBuilders.post("/sales/calculate")
      .content(toJson(operationRequest))
      .contentType(MediaType.APPLICATION_JSON))
      .andDo(print())
      .andExpectAll(status().isBadRequest(),
        jsonPath("$.title").value(HttpStatus.BAD_REQUEST.getReasonPhrase()),
        jsonPath("$.status").value(HttpStatus.BAD_REQUEST.value()),
        jsonPath("$.detail").value("Free sale is not allowed."),
        jsonPath("$.instance").value("discount"))
      .andReturn();
}

在此 SalesControllerUnitTest 中,我们自动装配了 MockMvcObjectMapper 以测试 SalesController

测试方法 givenFreeSale_whenSellingPriceIsCalculated_thenReturnError() 模拟了对 /sales/calculate 端点的 POST 请求,其中的 OperationRequest 包含基本价格 100.0 和折扣 140.0 。因此,这将在 Controller 中触发 InvalidOperandException

最后,验证 BadRequest 类型的响应,其中的 ProblemDetail 显示 "Free sale is not allowed."

6、总结 {#6总结}

本文介绍了 ProblemDetails 规范及其在 Spring Boot REST 应用中的实现,还介绍了它相对于传统错误处理的优势,以及如何在 Servlet 和 Reactive 栈中使用它。


Ref:https://www.baeldung.com/spring-boot-return-errors-problemdetail

赞(3)
未经允许不得转载:工具盒子 » 在 Spring Boot 中使用 ProblemDetail 返回错误