1. 概述

本文将探讨如何在Spring Boot应用中使用ProblemDetail返回错误。无论是处理REST API还是响应式流,它都提供了一种标准化的方式向客户端传递错误信息。

我们首先分析为什么需要关注这个特性,然后回顾其出现前的错误处理方式,接着介绍其背后的规范,最后学习如何用它构建错误响应。

2. 为什么需要关注ProblemDetail

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

它能帮助客户端理解和处理错误,提升API的可用性和可调试性,从而带来更好的开发体验和更健壮的应用程序。

采用它还能提供更丰富的错误信息,这对服务维护和故障排查至关重要。

3. 传统错误处理方式

ProblemDetail出现前,我们通常通过自定义异常处理器和响应实体来处理Spring Boot中的错误。这种方式会导致:

  • ✅ 不同API间的错误响应结构不一致
  • ❌ 需要大量样板代码
  • ⚠️ 缺乏统一的错误表示标准,客户端难以统一解析错误信息

4. ProblemDetail规范

ProblemDetail规范基于RFC 7807标准它定义了包含type, title, status, detailinstance等字段的统一错误响应结构。 这种标准化为API开发者和消费者提供了通用的错误信息格式。

实现ProblemDetail能确保错误响应可预测且易于理解,从而改善API与客户端间的通信质量。

接下来我们将在Spring Boot应用中实现它,从基础配置开始。

5. 在Spring Boot中实现ProblemDetail

Spring Boot提供了多种实现方式:

5.1. 通过配置属性启用

首先可通过配置属性启用。对于RESTful服务,在application.properties中添加:

spring.mvc.problemdetails.enabled=true

该属性会自动在MVC(Servlet栈)应用中启用ProblemDetail错误处理。

对于响应式应用,添加:

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. 在异常处理器中实现

全局异常处理器在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控制器

@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"*接口的POST请求:

  • 验证OperationRequest对象
  • 计算售价(考虑可选折扣)
  • 抛出异常当折扣无效(≥100%或>30%)
  • 返回封装在*ResponseEntity中的OperationResult*

现在在全局异常处理器中实现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提供集中式异常处理
  • 处理InvalidInputException时:
    • 创建状态为BAD_REQUESTProblemDetail
    • 设置instance为URI("discount")标识错误上下文

**ResponseEntityExceptionHandler*简化了异常到HTTP响应的转换过程,并内置了对常见Spring MVC异常(如MissingServletRequestParameterExceptionMethodArgumentNotValidException等)的ProblemDetail*支持。

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中:

  • 自动装配MockMvcObjectMapper
  • 测试方法模拟POST请求:
    • 请求体包含basePrice=100.0和discount=140.0
    • 触发控制器中的InvalidOperandException
  • 验证响应:
    • 状态码为400
    • ProblemDetail包含"Free sale is not allowed."错误信息

6. 总结

本文探讨了ProblemDetails的规范及其在Spring Boot REST应用中的实现方式。我们分析了它相比传统错误处理的优势,并展示了在Servlet和响应式栈中的使用方法。

完整源代码可在GitHub获取。


原始标题:Returning Errors Using ProblemDetail in Spring Boot | Baeldung