在 Spring Boot 中使用 ProblemDetail 返回错误
1、简介
本文将带你了解如何在 Spring Boot 应用中使用 ProblemDetail
响应错误信息,无论我们处理的是 REST API 还是 Reactive Stream(响应式流),它都提供了一种向客户端传达错误的标准化方式。
2、为什么要关注 ProblemDetail?
使用 ProblemDetail
来标准化错误响应对任何 API 都至关重要。
它可以帮助客户理解和处理错误,提高 API 的可用性和可调试性。这将带来更好的开发体验和更强大的应用。
采用它还有助于提供更翔实的错误信息,这对维护我们的服务和排除故障至关重要。
3、传统的错误处理方式
在 ProblemDetail
之前,我们经常在 Spring Boot 中实现自定义 ExceptionHandler
和 ResponseEntity
来处理错误。我们会创建自定义的错误响应结构。这导致了不同 API 之间的不一致性。
这种方式不仅需要大量的模板代码。而且,缺乏表示错误的标准化方式,因此客户端很难统一解析和理解错误信息。
4、ProblemDetail 规范
ProblemDetail
规范是 RFC 7807 标准的一部分。它为错误响应定义了一致的结构,包括诸如类型(type)、标题(title)、状态(status)、详情(detail)和实例(instance)等字段。这种标准化提供了一个通用的错误信息格式,有助于 API 开发人员和使用者。
实现 ProblemDetail
可确保我们的错误响应具有可预测性并易于理解。这反过来提高了我们的 API 和其客户端之间的整体沟通效果。
5、在 Spring Boot 中实现 ProblemDetail
在 Spring Boot 中有多种方法可以实现 ProblemDetail。
5.1、通过配置属性启用 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
全局异常处理器在 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 异常的方法,如 MissingServletRequestParameterException
、MethodArgumentNotValidException
等。
5.3、测试 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
中,我们自动装配了 MockMvc
和 ObjectMapper
以测试 SalesController
。
测试方法 givenFreeSale_whenSellingPriceIsCalculated_thenReturnError()
模拟了对 /sales/calculate
端点的 POST 请求,其中的 OperationRequest
包含基本价格 100.0 和折扣 140.0。因此,这将在 Controller 中触发 InvalidOperandException
。
最后,验证 BadRequest
类型的响应,其中的 ProblemDetail
显示 “Free sale is not allowed.”。
6、总结
本文介绍了 ProblemDetails
规范及其在 Spring Boot REST 应用中的实现,还介绍了它相对于传统错误处理的优势,以及如何在 Servlet 和 Reactive 栈中使用它。
Ref:https://www.baeldung.com/spring-boot-return-errors-problemdetail