Appearance
Spring WebFlux 错误响应处理:RFC 9457 标准化错误响应指南 ⚙️
引言:为什么需要标准化的错误响应? 🤔
在传统的 REST API 开发中,我们经常遇到这样的困扰:
- 不同的开发者返回不同格式的错误信息
- 客户端难以统一处理各种错误响应
- 错误信息缺乏标准化,可读性差
- 国际化支持困难
Spring WebFlux 通过支持 RFC 9457 标准("Problem Details for HTTP APIs"),为我们提供了一套完整的错误响应解决方案。
NOTE
RFC 9457 是一个国际标准,定义了 HTTP API 错误响应的标准格式,让错误信息更加结构化和可预测。
核心概念与架构 🏗️
Spring WebFlux 的错误响应机制基于以下四个核心抽象:
1. ProblemDetail - 错误详情容器
kotlin
// RFC 9457 标准错误响应的数据载体
data class CustomProblemDetail(
val type: String, // 错误类型 URI
val title: String, // 错误标题
val status: Int, // HTTP 状态码
val detail: String, // 详细描述
val instance: String, // 出错实例 URI
val properties: Map<String, Any> = emptyMap() // 扩展字段
)
2. ErrorResponse - 错误响应契约
kotlin
// 所有 Spring WebFlux 异常都实现了这个接口
interface ErrorResponse {
fun getStatusCode(): HttpStatusCode
fun getHeaders(): HttpHeaders
fun getBody(): ProblemDetail
fun getDetailMessageCode(): String
fun getDetailMessageArguments(): Array<Any>
}
3. ErrorResponseException - 基础实现
kotlin
// 自定义异常的便利基类
class CustomBusinessException(
status: HttpStatusCode,
detail: String,
cause: Throwable? = null
) : ErrorResponseException(status, asProblemDetail(detail), cause) {
companion object {
private fun asProblemDetail(detail: String): ProblemDetail {
return ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
detail
).apply {
type = URI.create("https://api.example.com/errors/business-rule")
title = "业务规则违反"
}
}
}
}
4. ResponseEntityExceptionHandler - 统一异常处理
kotlin
@ControllerAdvice
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
// 自动处理所有 ErrorResponse 异常
// 继承的 handleErrorResponse 方法会自动处理
@ExceptionHandler(CustomBusinessException::class)
fun handleCustomException(ex: CustomBusinessException): ResponseEntity<ProblemDetail> {
return ResponseEntity
.status(ex.statusCode)
.body(ex.body)
}
}
错误响应渲染机制 🎨
标准化渲染流程
实际应用示例
kotlin
@RestController
class UserController {
@GetMapping("/users/{id}")
fun getUser(@PathVariable id: Long): ResponseEntity<Any> {
return try {
val user = userService.findById(id)
ResponseEntity.ok(user)
} catch (e: UserNotFoundException) {
// 非标准化错误响应
ResponseEntity.status(404)
.body(mapOf(
"error" to "User not found",
"message" to "用户不存在",
"code" to "USER_NOT_FOUND"
))
}
}
}
kotlin
@RestController
class UserController {
@GetMapping("/users/{id}")
fun getUser(@PathVariable id: Long): User {
// 直接抛出异常,让统一异常处理器处理
return userService.findById(id)
?: throw UserNotFoundException("用户ID: $id 不存在")
}
}
// 自定义异常实现 ErrorResponse
class UserNotFoundException(detail: String) : ErrorResponseException(
HttpStatus.NOT_FOUND,
ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, detail).apply {
type = URI.create("https://api.example.com/errors/user-not-found")
title = "用户未找到"
}
)
响应格式对比
json
{
"error": "User not found",
"message": "用户不存在",
"code": "USER_NOT_FOUND"
}
json
{
"type": "https://api.example.com/errors/user-not-found",
"title": "用户未找到",
"status": 404,
"detail": "用户ID: 123 不存在",
"instance": "/users/123"
}
TIP
RFC 9457 格式的优势:
- type: 提供错误类型的唯一标识
- title: 人类可读的错误标题
- status: 明确的HTTP状态码
- detail: 具体的错误描述
- instance: 出错的具体实例路径
扩展非标准字段 🔧
方式一:使用 Properties Map
kotlin
@ExceptionHandler(ValidationException::class)
fun handleValidation(ex: ValidationException): ProblemDetail {
val problemDetail = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST,
"请求参数验证失败"
)
// 通过 properties Map 添加自定义字段
problemDetail.setProperty("errors", ex.errors)
problemDetail.setProperty("timestamp", Instant.now())
problemDetail.setProperty("requestId", UUID.randomUUID().toString())
return problemDetail
}
生成的响应:
json
{
"type": "about:blank",
"title": "Bad Request",
"status": 400,
"detail": "请求参数验证失败",
"instance": "/api/users",
"errors": ["用户名不能为空", "邮箱格式不正确"],
"timestamp": "2024-01-15T10:30:00Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000"
}
方式二:继承 ProblemDetail
kotlin
// 自定义 ProblemDetail 子类
class ValidationProblemDetail : ProblemDetail {
var errors: List<String> = emptyList()
var timestamp: Instant = Instant.now()
var requestId: String = UUID.randomUUID().toString()
constructor(original: ProblemDetail) : super(original)
companion object {
fun create(detail: String, errors: List<String>): ValidationProblemDetail {
val original = ProblemDetail.forStatusAndDetail(
HttpStatus.BAD_REQUEST, detail
)
return ValidationProblemDetail(original).apply {
this.errors = errors
}
}
}
}
@ExceptionHandler(ValidationException::class)
fun handleValidation(ex: ValidationException): ValidationProblemDetail {
return ValidationProblemDetail.create(
"请求参数验证失败",
ex.errors
)
}
自定义与国际化支持 🌐
消息代码模式
Spring WebFlux 使用以下模式生成消息代码:
problemDetail.[type|title|detail].[完全限定异常类名]
配置消息资源
properties
# 默认英文消息
problemDetail.title.com.example.UserNotFoundException=User Not Found
problemDetail.detail.com.example.UserNotFoundException=The requested user with ID {0} does not exist
problemDetail.title.com.example.ValidationException=Validation Failed
problemDetail.detail.com.example.ValidationException=Request validation failed: {0}
properties
# 中文消息
problemDetail.title.com.example.UserNotFoundException=用户未找到
problemDetail.detail.com.example.UserNotFoundException=请求的用户ID {0} 不存在
problemDetail.title.com.example.ValidationException=参数验证失败
problemDetail.detail.com.example.ValidationException=请求参数验证失败:{0}
国际化异常处理器
kotlin
@ControllerAdvice
class I18nExceptionHandler : ResponseEntityExceptionHandler() {
@Autowired
private lateinit var messageSource: MessageSource
@ExceptionHandler(UserNotFoundException::class)
fun handleUserNotFound(
ex: UserNotFoundException,
locale: Locale
): ResponseEntity<ProblemDetail> {
val problemDetail = ProblemDetail.forStatus(HttpStatus.NOT_FOUND)
// 国际化标题和详情
problemDetail.title = messageSource.getMessage(
"problemDetail.title.${ex.javaClass.name}",
null, locale
)
problemDetail.detail = messageSource.getMessage(
"problemDetail.detail.${ex.javaClass.name}",
arrayOf(ex.userId), locale
)
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail)
}
}
常见异常的消息代码表
异常类型 | 消息代码参数 | 说明 |
---|---|---|
HandlerMethodValidationException | {0} 验证错误列表 | 方法参数验证失败 |
MethodNotAllowedException | {0} 当前方法, {1} 支持的方法列表 | HTTP方法不允许 |
MissingRequestValueException | {0} 值标签, {1} 值名称 | 缺少请求值 |
NotAcceptableStatusException | {0} 支持的媒体类型列表 | 不可接受的响应类型 |
UnsupportedMediaTypeStatusException | {0} 不支持的类型, {1} 支持的类型列表 | 不支持的媒体类型 |
客户端错误处理 💻
WebClient 错误处理
kotlin
@Service
class UserApiClient {
private val webClient = WebClient.builder()
.baseUrl("https://api.example.com")
.build()
suspend fun getUser(id: Long): User? {
return try {
webClient.get()
.uri("/users/{id}", id)
.retrieve()
.awaitBody<User>()
} catch (ex: WebClientResponseException) {
// 解析 RFC 9457 错误响应
val problemDetail = ex.getResponseBodyAs(ProblemDetail::class.java)
when (problemDetail?.status) {
404 -> {
logger.warn("用户不存在: ${problemDetail.detail}")
null
}
400 -> {
logger.error("请求参数错误: ${problemDetail.detail}")
throw IllegalArgumentException(problemDetail.detail)
}
else -> {
logger.error("API调用失败: ${problemDetail?.detail}")
throw RuntimeException("用户服务不可用")
}
}
}
}
}
自定义错误响应拦截器
kotlin
@Configuration
class WebFluxConfig : WebFluxConfigurer {
override fun configureErrorResponseInterceptors(
registry: ErrorResponseInterceptorRegistry
) {
// 注册错误响应拦截器
registry.addInterceptor { errorResponse, exchange ->
// 添加请求追踪ID
val requestId = exchange.request.headers.getFirst("X-Request-ID")
?: UUID.randomUUID().toString()
errorResponse.body.setProperty("requestId", requestId)
errorResponse.body.setProperty("timestamp", Instant.now())
// 记录错误日志
logger.error(
"API错误 [{}]: {} - {}",
requestId,
errorResponse.statusCode,
errorResponse.body.detail
)
errorResponse
}
}
}
完整实践示例 🚀
点击查看完整的用户管理API错误处理实现
kotlin
// 1. 自定义异常
sealed class UserException(
status: HttpStatus,
detail: String,
cause: Throwable? = null
) : ErrorResponseException(status, createProblemDetail(status, detail), cause) {
companion object {
private fun createProblemDetail(status: HttpStatus, detail: String): ProblemDetail {
return ProblemDetail.forStatusAndDetail(status, detail).apply {
type = URI.create("https://api.example.com/errors/user")
}
}
}
}
class UserNotFoundException(userId: Long) : UserException(
HttpStatus.NOT_FOUND,
"用户ID: $userId 不存在"
)
class UserValidationException(
val errors: List<String>
) : UserException(
HttpStatus.BAD_REQUEST,
"用户信息验证失败"
)
// 2. 业务服务
@Service
class UserService {
private val users = mutableMapOf<Long, User>()
fun findById(id: Long): User {
return users[id] ?: throw UserNotFoundException(id)
}
fun create(userRequest: CreateUserRequest): User {
val errors = validateUser(userRequest)
if (errors.isNotEmpty()) {
throw UserValidationException(errors)
}
val user = User(
id = generateId(),
name = userRequest.name,
email = userRequest.email
)
users[user.id] = user
return user
}
private fun validateUser(request: CreateUserRequest): List<String> {
val errors = mutableListOf<String>()
if (request.name.isBlank()) {
errors.add("用户名不能为空")
}
if (!request.email.contains("@")) {
errors.add("邮箱格式不正确")
}
return errors
}
private fun generateId(): Long = System.currentTimeMillis()
}
// 3. 控制器
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
@GetMapping("/{id}")
fun getUser(@PathVariable id: Long): User {
return userService.findById(id)
}
@PostMapping
fun createUser(@RequestBody request: CreateUserRequest): User {
return userService.create(request)
}
}
// 4. 全局异常处理器
@ControllerAdvice
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {
@ExceptionHandler(UserValidationException::class)
fun handleUserValidation(ex: UserValidationException): ResponseEntity<ProblemDetail> {
val problemDetail = ex.body
// 添加验证错误详情
problemDetail.setProperty("errors", ex.errors)
problemDetail.setProperty("errorCount", ex.errors.size)
return ResponseEntity
.status(ex.statusCode)
.body(problemDetail)
}
}
// 5. 数据类
data class User(
val id: Long,
val name: String,
val email: String
)
data class CreateUserRequest(
val name: String,
val email: String
)
最佳实践建议 ⭐
1. 异常设计原则
IMPORTANT
- 为不同的业务场景创建专门的异常类
- 继承
ErrorResponseException
以获得标准化支持 - 提供有意义的错误类型 URI 和标题
2. 错误信息设计
TIP
- 详情字段:提供足够的上下文信息,但避免暴露敏感数据
- 类型字段:使用有意义的 URI,便于客户端分类处理
- 标题字段:简洁明了,适合直接展示给用户
3. 国际化策略
NOTE
- 将所有错误消息外部化到资源文件
- 使用参数化消息支持动态内容
- 考虑不同地区的文化差异
4. 客户端集成
WARNING
客户端应该:
- 优先检查
type
字段进行错误分类 - 使用
status
字段确定HTTP状态 - 将
detail
字段用于日志记录和调试 - 将
title
字段用于用户界面显示
总结 🎉
Spring WebFlux 的 RFC 9457 错误响应支持为我们提供了:
✅ 标准化:统一的错误响应格式,提高API的一致性
✅ 可扩展性:支持自定义字段,满足特殊业务需求
✅ 国际化:完整的多语言支持机制
✅ 客户端友好:结构化的错误信息,便于程序处理
✅ 开发效率:减少样板代码,专注业务逻辑
通过采用这套标准化的错误处理机制,我们可以构建更加健壮、用户友好的 REST API,同时大大简化错误处理的复杂性。