从Spring 6和Spring Boot 3开始,Spring框架支持“HTTP API的问题详细信息”规范RFC 7807。本Spring Boot 教程将详细指导您完成这一新增强。

1.问题详细说明[RFC 7807]

该RFC定义了简单的JSON和XML文档格式,可用于向API消费者传达问题细节。这在HTTP状态代码不足以描述HTTP API问题的情况下非常有用。

以下是从一个银行账户转移到另一个银行帐户时发生的问题的示例,我们的帐户中没有足够的余额。
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
Content-Language: en

{
	"status": 403,
	"type": "https://bankname.com/common-problems/low-balance",
	"title": "You not have enough balance",
	"detail": "Your current balance is 30 and you are transterring 50",
	"instance": "/account-transfer-service"
}
 

关键信息如下:

  • status: 服务器生成的HTTP状态代码。
  • type: 标识问题类型以及如何解决问题的URL。默认值 value is – about:blank.
  • title: 问题的简短说明。
  • detail: 问题的详细说明.
  • instance: 发生此问题的服务的URL。默认值是当前请求URL。

2.Spring框架中的支持

以下是Spring框架中支持问题细节规范的主要抽象:

2.1.问题详情

它是表示问题细节模型的主要对象。如前一节所述,它包含标准字段,非标准字段可以作为属性添加。
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "message");
pd.setType(URI.create("http://my-app-host.com/errors/not-found"));
pd.setTitle("Record Not Found");
要添加非标准字段,请使用setProperty()方法。
pd.setProperty("property-key", "property-value");

2.2.错误响应

此接口公开HTTP错误响应详细信息,包括HTTP状态、响应标头和ProblemDetail类型的主体。与仅发送ProblemDetail对象相比,它可以用于向客户端提供更多信息。

所有Spring MVC异常都实现ErrorResponse接口。因此,所有MVC异常都已经符合规范。

2.3.错误响应异常

它是一个非常基本的ErrorResponse实现,我们可以将其作为一个方便的基类来创建更具体的异常类。
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Null Pointer Exception");
pd.setType(URI.create("http://my-app-host.com/errors/npe"));
pd.setTitle("Null Pointer Exception");

throw new ErrorResponseException(HttpStatus.INTERNAL_SERVER_ERROR, pd, npe);

2.4.ResponseEntityExceptionHandler

它是@ControllerAdvice的一个方便的基类,它根据RFC规范和任何ErrorResponseException处理所有SpringMVC异常,并用主体呈现错误响应。
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
	
	@ExceptionHandler(CustomException.class)
  public ProblemDetail handleCustomException(CustomException ex, WebRequest request) {

    ProblemDetail pd = //build object..
    return pd;
  }
}

我们可以从任何@ExceptionHandler或任何@RequestMapping方法返回ProblemDetail或ErrorResponse以呈现RFC 7807响应。

3.使用ResponseEntity发送ProblemDetail

在失败的情况下,创建ProblemDetail类的新实例,填充相关信息并将其设置到ResponseEntity对象中。

假设当id大于100时,以下API失败。
@Value("${hostname}")
private String hostname;

@GetMapping(path = "/employees/v2/{id}")
public ResponseEntity getEmployeeById_V2(@PathVariable("id") Long id) {
  if (id < 100) {
    return ResponseEntity.ok(new Employee(id, "lokesh", "gupta", "admin@leftso.com"));
  } else {
    ProblemDetail pd = ProblemDetail
        .forStatusAndDetail(HttpStatus.NOT_FOUND, "Employee id '" + id + "' does no exist");
    pd.setType(URI.create("http://my-app-host.com/errors/not-found"));
    pd.setTitle("Record Not Found");
    pd.setProperty("hostname", hostname);
    return ResponseEntity.status(404).body(pd);
  }
}
接下来,让我们用id=101进行测试。它将返回RFC规范中的响应。
{
    "detail": "Employee id '101' does no exist",
    "hostname": "localhost",
    "instance": "/employees/v2/101",
    "status": 404,
    "title": "Record Not Found",
    "type": "http://my-app-host.com/errors/not-found"
}

4.从REST控制器引发ErrorResponseException

发送问题详细信息的另一种方法是从@RequestMapping处理程序方法中抛出ErrorResponseException实例。

这在我们已经有无法发送到客户端的异常(例如NullPointerException)的情况下尤其有用。在这种情况下,我们在ErrorResponseException中填充基本信息并抛出它。Spring MVC处理程序内部处理此异常并将其解析为RFC指定的响应格式。
@GetMapping(path = "/v3/{id}")
public ResponseEntity getEmployeeById_V3(@PathVariable("id") Long id) {
  try {
  	//somthing抛出了此异常
    throw new NullPointerException("Something was expected but it was null");
  }
  catch (NullPointerException npe) {
    ProblemDetail pd = ProblemDetail
        .forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR,
            "Null Pointer Exception");
    pd.setType(URI.create("http://my-app-host.com/errors/npe"));
    pd.setTitle("Null Pointer Exception");
    pd.setProperty("hostname", hostname);
    throw new ErrorResponseException(HttpStatus.NOT_FOUND, pd, npe);
  }
}

5.将ProblemDetail添加到自定义异常

大多数应用程序都创建更接近其业务域/模型的异常类。一些这样的异常可能是RecordNotFoundException、TransactionLimitException等。它们更易读,并在代码中简洁地表示错误场景。

大多数时候,这些异常都是RuntimeException的子类。
public class RecordNotFoundException extends RuntimeException {
  private final String message;
  public RecordNotFoundException(String message) {
    this.message = message;
  }
}

我们从代码中的几个地方抛出这些异常。

@GetMapping(path = "/v1/{id}")
public ResponseEntity getEmployeeById_V1(@PathVariable("id") Long id) {
  if (id < 100) {
    return ResponseEntity.ok(...);
  } else {
    throw new RecordNotFoundException("Employee id '" + id + "' does no exist");
  }
}
在此类异常中添加问题详细信息的最佳方法是在@ControllerAdvice类中。我们必须在@ExceptionHandler(RecordNotFoundException.class)方法中处理异常,并添加所需的信息。
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
  @Value("${hostname}")
  private String hostname;
  @ExceptionHandler(RecordNotFoundException.class)
  public ProblemDetail handleRecordNotFoundException(RecordNotFoundException ex, WebRequest request) {
    ProblemDetail body = ProblemDetail
        .forStatusAndDetail(HttpStatusCode.valueOf(404),ex.getLocalizedMessage());
    body.setType(URI.create("http://my-app-host.com/errors/not-found"));
    body.setTitle("Record Not Found");
    body.setProperty("hostname", hostname);
    return body;
  }
}

6. 在单元测试中验证ProblemDetail响应

我们还可以在单元测试中使用RestTemplate测试验证上述部分的问题细节响应。
@Test
public void testAddEmployee_V2_FailsWhen_IncorrectId() {
  try {
    this.restTemplate.getForObject("/employees/v2/101", Employee.class);
  } catch (RestClientResponseException ex) {
    ProblemDetail pd = ex.getResponseBodyAs(ProblemDetail.class);
    assertEquals("Employee id '101' does no exist", pd.getDetail());
    assertEquals(404, pd.getStatus());
    assertEquals("Record Not Found", pd.getTitle());
    assertEquals(URI.create("http://my-app-host.com/errors/not-found"), pd.getType());
    assertEquals("localhost", pd.getProperties().get("hostname"));
  }
}
注意,如果我们使用Spring Webflux,我们可以使用WebClient API来验证返回的问题细节。
@Test
void testAddEmployeeUsingWebFlux_V2_FailsWhen_IncorrectId() {
  this.webClient.get().uri("/employees/v2/101")
      .retrieve()
      .bodyToMono(String.class)
      .doOnNext(System.out::println)
      .onErrorResume(WebClientResponseException.class, ex -> {
        ProblemDetail pd = ex.getResponseBodyAs(ProblemDetail.class);
        //assertions...
        return Mono.empty();
      })
      .block();
}

执行v3接口测试

 @Test
  public void testAddEmployee_V3_FailsWhen_IncorrectId() {
    try {
      this.restTemplate.getForObject("/employees/v3/101", Employee.class);
    } catch (RestClientResponseException ex) {
      ProblemDetail pd = ex.getResponseBodyAs(ProblemDetail.class);
      System.out.println(format(ex.getStatusCode(), ex.getResponseHeaders(), pd));
    }
  }



以上测试执行结果参考
执行结果1


执行结果2

执行结果3
 

7. 总结

在这个Spring 6(Spring Boot 3.0)教程中,我们学习了Spring框架中支持问题细节规范的新特性。在这个特性之后,我们可以从@ExceptionHandler或@RequestMapping方法返回ProblemDetail或ErrorResponse的实例,Spring框架会在API响应中添加必要的模式。

本教程中讨论的代码适用于Spring应用程序和Spring boot应用程序。

 

评论区域

评论功能已关闭. 提示:评论功能虽已关闭,关闭之前的评论仍然会展示。