1. 问题详细概述[RFC 7807]
此RFC定义了简单的JSON和XML文档格式,可用于将问题详细信息传达给API使用者。这在HTTP状态代码不足以描述HTTP API问题的情况下非常有用。
以下是从一个银行账户转账到另一个银行账户时出现的问题示例,我们的账户余额不足。
```http request 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,默认值为about:blank
- **title**:问题的简短摘要
- **detail**:特定于此事件的问题说明
- **instance**:发生此问题的服务的URL,默认值为当前请求URL
## 2. Spring框架的支持
以下是Spring框架中用于支持问题细节规范的主要抽象:
### 2.1 ProblemDetail
它是表示问题详细模型的主要对象。如上一节所述,它包含标准字段,非标准字段可以作为属性添加。
```java
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, "message");
pd.setType(URI.create("https://my-app-host.com/errors/not-found"));
pd.setTitle("Record Not Found");
要添加非标准字段,请使用setProperty()方法。
pd.setProperty("property-key", "property-value");
2.2 ErrorResponse
此接口公开HTTP错误响应详细信息,包括HTTP状态、响应标头和类型为“ProblemDetail”的正文。与仅发送ProblemDetail对象相比,它可用于向客户端提供更多信息。
所有Spring MVC异常都实现了ErrorResponse接口。因此,所有MVC异常都已经符合规范。
2.3 ErrorResponseException
这是一个非常基本的ErrorResponse实现,我们可以将其用作方便的基类来创建更具体的异常类。
ProblemDetail pd = ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, "Null Pointer Exception");
pd.setType(URI.create("https://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处理所有Spring MVC异常,并使用主体呈现错误响应。
@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@howtodoinjava.com"));
} else {
ProblemDetail pd = ProblemDetail
.forStatusAndDetail(HttpStatus.NOT_FOUND, "Employee id '" + id + "' does not exist");
pd.setType(URI.create("https://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": "https://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 threw this exception
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("https://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 not 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("https://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 not exist", pd.getDetail());
assertEquals(404, pd.getStatus());
assertEquals("Record Not Found", pd.getTitle());
assertEquals(URI.create("https://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();
}
7. 总结
在这个Spring Boot 3教程中,我们了解了Spring框架中支持问题详细信息规范的新功能。有了此功能后,我们可以从任何@ExceptionHandler或任何@RequestMapping方法返回ProblemDetail或ErrorResponse的实例,Spring框架将必要的模式添加到API响应中。
与往常一样,本教程的完整源代码可在GitHub上获得。