从Spring 6和Spring Boot 3开始,Spring框架支持“HTTP API的问题详细信息”规范RFC 7807。本Spring Boot 教程将详细指导您完成这一新增强。
该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"
}
关键信息如下:
以下是Spring框架中支持问题细节规范的主要抽象:
它是表示问题细节模型的主要对象。如前一节所述,它包含标准字段,非标准字段可以作为属性添加。
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");
此接口公开HTTP错误响应详细信息,包括HTTP状态、响应标头和ProblemDetail类型的主体。与仅发送ProblemDetail对象相比,它可以用于向客户端提供更多信息。
所有Spring MVC异常都实现ErrorResponse接口。因此,所有MVC异常都已经符合规范。
它是一个非常基本的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);
它是@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响应。
在失败的情况下,创建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"
}
发送问题详细信息的另一种方法是从@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);
}
}
大多数应用程序都创建更接近其业务域/模型的异常类。一些这样的异常可能是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;
}
}
我们还可以在单元测试中使用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));
}
}
以上测试执行结果参考
在这个Spring 6(Spring Boot 3.0)教程中,我们学习了Spring框架中支持问题细节规范的新特性。在这个特性之后,我们可以从@ExceptionHandler或@RequestMapping方法返回ProblemDetail或ErrorResponse的实例,Spring框架会在API响应中添加必要的模式。
本教程中讨论的代码适用于Spring应用程序和Spring boot应用程序。
https://www.leftso.com/article/1116.html