1. 引言
本文将深入探讨 Spring 框架中不同的错误响应格式,并学习如何抛出和处理带有自定义属性的 RFC7807 ProblemDetail 异常,以及如何在 Spring WebFlux 中实现自定义异常处理。
2. Spring Boot 3 中的异常响应格式
Spring Boot 3 默认支持多种错误响应格式。当发生未处理异常时,Spring 框架会通过 DefaultErrorAttributes 类(实现了 ErrorAttributes 接口)生成错误响应。默认的 JSON 响应结构如下:
{
"timestamp": "2023-04-01T00:00:00.000+00:00",
"status": 500,
"error": "Internal Server Error",
"path": "/api/example"
}
虽然这种格式包含基本属性,但排查问题时可能信息不足。好在我们可以通过自定义 ErrorAttributes 实现来修改默认行为。
✅ Spring Framework 6 开始支持 RFC7807 规范的 ProblemDetail 表示,它包含以下标准属性:
- type (字符串) – 标识问题类型的 URI
- title (字符串) – 问题类型的简短摘要
- status (数字) – HTTP 状态码
- detail (字符串) – 异常详细信息
- instance (字符串) – 标识具体问题的 URI(如指向导致问题的属性)
此外,ProblemDetail 还包含一个 Map<String, Object> 用于添加自定义参数。下面是一个包含自定义 errors 对象的示例响应:
{
"type": "https://example.com/probs/email-invalid",
"title": "Invalid email address",
"detail": "The email address 'john.doe' is invalid.",
"status": 400,
"timestamp": "2023-04-07T12:34:56.789Z",
"errors": [
{
"code": "123",
"message": "Error message",
"reference": "https//error/details#123"
}
]
}
Spring 还提供了 ErrorResponseException 基础实现,它封装了 ProblemDetail 对象,我们可以通过继承它来定制异常属性。
3. 实现 ProblemDetail RFC 7807 异常
虽然 Spring 6+/Spring Boot 3+ 默认支持 ProblemDetail,但需要通过以下方式之一启用:
3.1. 通过配置文件启用
在 application.yml
中添加属性:
spring:
mvc:
problemdetails:
enabled: true
3.2. 通过异常处理器启用
扩展 ResponseEntityExceptionHandler 并添加自定义异常处理器(即使不重写任何方法):
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
//...
}
本文将采用这种方式,因为我们需要添加自定义异常处理器。
3.3. 实现 ProblemDetail 异常
以一个简单的用户管理应用为例,演示如何抛出和处理带自定义属性的 ProblemDetail 异常。控制器中的 GET /v1/users/{userId} 接口根据用户 ID 查询用户,未找到时抛出 UserNotFoundException:
@GetMapping("/v1/users/{userId}")
public Mono<ResponseEntity<User>> getUserById(@PathVariable Long userId) {
return Mono.fromCallable(() -> {
User user = userMap.get(userId);
if (user == null) {
throw new UserNotFoundException("User not found with ID: " + userId);
}
return new ResponseEntity<>(user, HttpStatus.OK);
});
}
UserNotFoundException 继承自 RuntimeException:
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(String message) {
super(message);
}
}
由于我们扩展了 ResponseEntityExceptionHandler,ProblemDetail 已成为默认异常格式。测试时使用不支持的 HTTP 方法(如 POST)访问接口:
curl --location --request POST 'localhost:8080/v1/users/1'
将得到 ProblemDetail 格式的响应:
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Supported methods: [GET]",
"instance": "/users/1"
}
3.4. 在 Spring WebFlux 中扩展自定义属性
为 UserNotFoundException 添加异常处理器,在 ProblemDetail 响应中包含自定义对象。ProblemDetail 的 properties 属性接受 String 键和任意 Object 值。
定义自定义错误对象 ErrorDetails,包含错误码、消息和参考链接:
@JsonSerialize(using = ErrorDetailsSerializer.class)
public enum ErrorDetails {
API_USER_NOT_FOUND(123, "User not found", "http://example.com/123");
@Getter
private Integer errorCode;
@Getter
private String errorMessage;
@Getter
private String referenceUrl;
ErrorDetails(Integer errorCode, String errorMessage, String referenceUrl) {
this.errorCode = errorCode;
this.errorMessage = errorMessage;
this.referenceUrl = referenceUrl;
}
}
在 GlobalExceptionHandler 中添加 UserNotFoundException 的处理器:
@ExceptionHandler(UserNotFoundException.class)
protected ProblemDetail handleNotFound(RuntimeException ex) {
ProblemDetail problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, ex.getMessage());
problemDetail.setTitle("User not found");
problemDetail.setType(URI.create("https://example.com/problems/user-not-found"));
problemDetail.setProperty("errors", List.of(ErrorDetails.API_USER_NOT_FOUND));
return problemDetail;
}
需要自定义序列化器 ErrorDetailsSerializer 和 ProblemDetailSerializer:
public class ErrorDetailsSerializer extends JsonSerializer<ErrorDetails> {
@Override
public void serialize(ErrorDetails value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeStringField("code", value.getErrorCode().toString());
gen.writeStringField("message", value.getErrorMessage());
gen.writeStringField("reference", value.getReferenceUrl());
gen.writeEndObject();
}
}
public class ProblemDetailsSerializer extends JsonSerializer<ProblemDetail> {
@Override
public void serialize(ProblemDetail value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartObject();
gen.writeObjectField("type", value.getType());
gen.writeObjectField("title", value.getTitle());
gen.writeObjectField("status", value.getStatus());
gen.writeObjectField("detail", value.getDetail());
gen.writeObjectField("instance", value.getInstance());
gen.writeObjectField("errors", value.getProperties().get("errors"));
gen.writeEndObject();
}
}
现在访问无效用户 ID 的接口:
$ curl --location 'localhost:8080/v1/users/1'
将得到包含自定义属性的 ProblemDetail 响应:
{
"type": "https://example.com/problems/user-not-found",
"title": "User not found",
"status": 404,
"detail": "User not found with ID: 1",
"instance": "/users/1",
"errors": [
{
"errorCode": 123,
"errorMessage": "User not found",
"referenceUrl": "http://example.com/123"
}
]
}
也可以使用实现了 ErrorResponse 的 ErrorResponseException 来暴露符合 RFC 7807 规范的 HTTP 状态、响应头和响应体。
⚠️ 除了 ResponseEntityExceptionHandler,也可以使用 AbstractErrorWebExceptionHandler 处理全局 WebFlux 异常。
4. 为什么需要自定义异常
虽然 ProblemDetail 格式灵活且支持自定义属性,但某些场景下我们可能希望抛出包含完整错误信息的自定义对象。此时使用自定义异常能提供更清晰、更具体、更一致的错误处理方式。
5. 在 Spring WebFlux 中实现自定义异常
定义自定义响应对象替代 ProblemDetail:
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class CustomErrorResponse {
private String traceId;
private OffsetDateTime timestamp;
private HttpStatus status;
private List<ErrorDetails> errors;
}
创建对应的自定义异常:
public class CustomErrorException extends RuntimeException {
@Getter
private CustomErrorResponse errorResponse;
public CustomErrorException(String message, CustomErrorResponse errorResponse) {
super(message);
this.errorResponse = errorResponse;
}
}
添加接口版本 v2,抛出此自定义异常(简化处理,部分字段使用随机值):
@GetMapping("/v2/users/{userId}")
public Mono<ResponseEntity<User>> getV2UserById(@PathVariable Long userId) {
return Mono.fromCallable(() -> {
User user = userMap.get(userId);
if (user == null) {
CustomErrorResponse customErrorResponse = CustomErrorResponse
.builder()
.traceId(UUID.randomUUID().toString())
.timestamp(OffsetDateTime.now().now())
.status(HttpStatus.NOT_FOUND)
.errors(List.of(ErrorDetails.API_USER_NOT_FOUND))
.build();
throw new CustomErrorException("User not found", customErrorResponse);
}
return new ResponseEntity<>(user, HttpStatus.OK);
});
}
在 GlobalExceptionHandler 中添加处理器格式化输出:
@ExceptionHandler({CustomErrorException.class})
protected ResponseEntity<CustomErrorResponse> handleCustomError(RuntimeException ex) {
CustomErrorException customErrorException = (CustomErrorException) ex;
return ResponseEntity.status(customErrorException.getErrorResponse().getStatus()).body(customErrorException.getErrorResponse());
}
访问无效用户 ID 的接口:
$ curl --location 'localhost:8080/v2/users/1'
将得到自定义错误响应:
{
"traceId": "e3853069-095d-4516-8831-5c7cfa124813",
"timestamp": "2023-04-28T15:36:41.658289Z",
"status": "NOT_FOUND",
"errors": [
{
"code": "123",
"message": "User not found",
"reference": "http://example.com/123"
}
]
}
6. 总结
本文探讨了如何启用和使用 Spring Framework 提供的 ProblemDetail RFC7807 异常格式,并学习了在 Spring WebFlux 中创建和处理自定义异常的方法。完整示例代码可在 GitHub 获取。