Skip to content

Spring MVC Error Responses - RFC 9457 标准化错误处理 🎉

引言:为什么需要标准化的错误响应? 🤔

在传统的 REST API 开发中,我们经常遇到这样的问题:

  • 不同的开发者返回的错误格式五花八门
  • 客户端难以统一处理各种错误情况
  • 错误信息缺乏结构化,调试困难
  • 国际化支持不够完善

想象一下,如果每个 API 都用自己的方式返回错误:

json
{
  "error": "用户名不能为空",
  "code": 400
}
json
{
  "message": "Validation failed",
  "details": ["Username is required"],
  "timestamp": "2024-01-01T10:00:00Z"
}
json
{
  "success": false,
  "errorMsg": "参数错误",
  "data": null
}

这种混乱的局面让前端开发者苦不堪言!Spring Framework 通过支持 RFC 9457 标准,为我们提供了统一、专业的错误响应解决方案。

NOTE

RFC 9457 是 "Problem Details for HTTP APIs" 的官方规范,它定义了一种标准化的方式来表示 HTTP API 中的错误信息。

核心概念解析 🎯

Spring MVC 的错误响应机制基于四个核心抽象:

1. ProblemDetail - 错误详情容器

ProblemDetail 是 RFC 9457 问题详情的表示,它就像一个标准化的"错误信息盒子":

kotlin
// 标准的 RFC 9457 错误响应格式
{
  "type": "https://example.com/problems/validation-error",
  "title": "Validation Error",
  "status": 400,
  "detail": "用户名不能为空",
  "instance": "/api/users"
}

2. ErrorResponse - 错误响应契约

ErrorResponse 定义了 HTTP 错误响应的契约,包括状态码、响应头和响应体。

3. ErrorResponseException - 基础实现

提供了一个便捷的基类,其他异常可以继承它来快速实现错误响应功能。

4. ResponseEntityExceptionHandler - 统一异常处理

这是一个便捷的基类,用于创建 @ControllerAdvice,可以处理所有 Spring MVC 异常。

实战应用:构建标准化错误处理 🚀

基础用法示例

让我们看看如何在实际项目中使用这些功能:

kotlin
@RestController
@RequestMapping("/api/users")
class UserController {

    @PostMapping
    fun createUser(@Valid @RequestBody user: CreateUserRequest): ResponseEntity<User> {
        // 如果验证失败,Spring 会自动抛出 MethodArgumentNotValidException
        // 这个异常会被 ResponseEntityExceptionHandler 处理
        return ResponseEntity.ok(userService.createUser(user))
    }

    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<User> {
        val user = userService.findById(id) 
            ?: throw UserNotFoundException("用户不存在: $id") 
        
        return ResponseEntity.ok(user)
    }
}
kotlin
@ControllerAdvice
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

    // 处理自定义业务异常
    @ExceptionHandler(UserNotFoundException::class)
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ProblemDetail> {
        val problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.NOT_FOUND, 
            ex.message ?: "用户不存在"
        )
        
        // 设置自定义字段
        problemDetail.type = URI.create("https://api.example.com/problems/user-not-found") 
        problemDetail.title = "用户未找到"
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(problemDetail)
    }
}

自定义异常实现 ErrorResponse

kotlin
class UserNotFoundException(
    message: String,
    private val userId: Long
) : ErrorResponseException(HttpStatus.NOT_FOUND) {

    init {
        // 设置问题详情
        val problemDetail = body
        problemDetail.type = URI.create("https://api.example.com/problems/user-not-found")
        problemDetail.title = "用户未找到"
        problemDetail.detail = message
        
        // 添加自定义属性
        problemDetail.setProperty("userId", userId) 
        problemDetail.setProperty("timestamp", Instant.now()) 
    }

    override fun getDetailMessageCode(): String {
        return "problem.user.not-found"
    }

    override fun getDetailMessageArguments(): Array<Any> {
        return arrayOf(userId)
    }
}

非标准字段扩展 🔧

RFC 9457 允许我们添加自定义字段来提供更丰富的错误信息:

方式一:使用 Properties Map

kotlin
@ExceptionHandler(ValidationException::class)
fun handleValidation(ex: ValidationException): ResponseEntity<ProblemDetail> {
    val problemDetail = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, 
        "请求参数验证失败"
    )
    
    // 通过 properties Map 添加自定义字段
    problemDetail.setProperty("errors", ex.errors) 
    problemDetail.setProperty("requestId", UUID.randomUUID().toString()) 
    problemDetail.setProperty("helpUrl", "https://docs.example.com/validation") 
    
    return ResponseEntity.badRequest().body(problemDetail)
}

方式二:扩展 ProblemDetail

kotlin
// 自定义 ProblemDetail 子类
class ValidationProblemDetail(
    original: ProblemDetail,
    val errors: List<FieldError>,
    val requestId: String = UUID.randomUUID().toString()
) : ProblemDetail(original) {
    
    val helpUrl: String = "https://docs.example.com/validation"
    
    init {
        type = URI.create("https://api.example.com/problems/validation-error")
        title = "参数验证失败"
    }
}

// 在异常处理器中使用
@ExceptionHandler(MethodArgumentNotValidException::class)
fun handleValidation(ex: MethodArgumentNotValidException): ResponseEntity<ValidationProblemDetail> {
    val originalProblem = ProblemDetail.forStatusAndDetail(
        HttpStatus.BAD_REQUEST, 
        "请求参数验证失败"
    )
    
    val customProblem = ValidationProblemDetail(
        originalProblem,
        ex.bindingResult.fieldErrors
    )
    
    return ResponseEntity.badRequest().body(customProblem)
}

国际化与自定义 🌍

Spring MVC 支持通过 MessageSource 来实现错误信息的国际化:

配置消息资源

properties
# 默认英文消息
problemDetail.type.com.example.UserNotFoundException=https://api.example.com/problems/user-not-found
problemDetail.title.com.example.UserNotFoundException=User Not Found
problemDetail.com.example.UserNotFoundException=User with ID {0} was not found
properties
# 中文消息
problemDetail.type.com.example.UserNotFoundException=https://api.example.com/problems/user-not-found
problemDetail.title.com.example.UserNotFoundException=用户未找到
problemDetail.com.example.UserNotFoundException=ID为 {0} 的用户不存在

消息代码策略

Spring 使用以下策略来解析消息代码:

TIP

消息代码遵循特定的命名规则:

  • type: problemDetail.type.[完全限定异常类名]
  • title: problemDetail.title.[完全限定异常类名]
  • detail: problemDetail.[完全限定异常类名][后缀]

常见异常的消息代码对照 📋

异常类型消息代码参数说明
MethodArgumentNotValidException(default){0} 全局错误列表, {1} 字段错误列表
MissingServletRequestParameterException(default){0} 请求参数名
HttpRequestMethodNotSupportedException(default){0} 当前HTTP方法, {1} 支持的HTTP方法列表
HttpMediaTypeNotSupportedException(default){0} 不支持的媒体类型, {1} 支持的媒体类型列表

客户端处理 📱

客户端可以轻松处理标准化的错误响应:

使用 WebClient

kotlin
class UserApiClient(private val webClient: WebClient) {

    fun getUser(id: Long): User {
        return try {
            webClient.get()
                .uri("/api/users/{id}", id)
                .retrieve()
                .bodyToMono<User>()
                .block()!!
        } catch (ex: WebClientResponseException) {
            // 解析标准化错误响应
            val problemDetail = ex.getResponseBodyAs(ProblemDetail::class.java) 
            
            when (problemDetail?.status) {
                404 -> throw UserNotFoundException("用户不存在: $id")
                400 -> throw ValidationException("请求参数错误: ${problemDetail.detail}")
                else -> throw ApiException("API调用失败: ${problemDetail?.detail}")
            }
        }
    }
}

使用 RestTemplate

kotlin
class UserApiClient(private val restTemplate: RestTemplate) {

    fun createUser(request: CreateUserRequest): User {
        return try {
            restTemplate.postForObject("/api/users", request, User::class.java)!!
        } catch (ex: RestClientResponseException) {
            // 解析错误响应
            val problemDetail = ex.getResponseBodyAs(ProblemDetail::class.java) 
            
            // 根据错误类型进行不同处理
            when (problemDetail?.type?.toString()) {
                "https://api.example.com/problems/validation-error" -> {
                    val errors = problemDetail.getProperties()["errors"] as? List<*>
                    throw ValidationException("验证失败", errors)
                }
                else -> throw ApiException("创建用户失败: ${problemDetail?.detail}")
            }
        }
    }
}

最佳实践建议 ✅

1. 统一异常处理架构

kotlin
@ControllerAdvice
class GlobalExceptionHandler : ResponseEntityExceptionHandler() {

    // 处理所有未捕获的异常
    @ExceptionHandler(Exception::class)
    fun handleGeneral(ex: Exception): ResponseEntity<ProblemDetail> {
        logger.error("未处理的异常", ex)
        
        val problemDetail = ProblemDetail.forStatusAndDetail(
            HttpStatus.INTERNAL_SERVER_ERROR,
            "服务器内部错误"
        )
        
        // 生产环境不暴露详细错误信息
        if (!isProduction()) {
            problemDetail.setProperty("exception", ex.javaClass.simpleName) 
            problemDetail.setProperty("message", ex.message) 
        }
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(problemDetail)
    }
}

2. 错误响应拦截器

kotlin
@Configuration
class WebConfig : WebMvcConfigurer {

    override fun configureErrorResponseInterceptors(registry: ErrorResponseInterceptorRegistry) {
        // 添加请求ID到所有错误响应
        registry.addInterceptor { errorResponse, exchange ->
            val problemDetail = errorResponse.body
            problemDetail.setProperty("requestId", exchange.request.getHeader("X-Request-ID")) 
            problemDetail.setProperty("timestamp", Instant.now()) 
        }
    }
}

3. 自定义业务异常基类

kotlin
abstract class BusinessException(
    message: String,
    status: HttpStatus = HttpStatus.BAD_REQUEST
) : ErrorResponseException(status) {

    init {
        val problemDetail = body
        problemDetail.detail = message
        problemDetail.type = getErrorType()
        problemDetail.title = getErrorTitle()
    }

    abstract fun getErrorType(): URI
    abstract fun getErrorTitle(): String
}

// 具体业务异常
class InsufficientBalanceException(
    private val currentBalance: BigDecimal,
    private val requiredAmount: BigDecimal
) : BusinessException("余额不足,当前余额: $currentBalance,需要: $requiredAmount") {

    override fun getErrorType() = URI.create("https://api.example.com/problems/insufficient-balance")
    override fun getErrorTitle() = "余额不足"

    init {
        body.setProperty("currentBalance", currentBalance) 
        body.setProperty("requiredAmount", requiredAmount) 
    }
}

总结 🎉

Spring MVC 的 Error Responses 功能通过支持 RFC 9457 标准,为我们提供了:

IMPORTANT

核心价值

  • 🎯 标准化:统一的错误响应格式,提升API的专业性
  • 🌍 国际化:完善的多语言支持
  • 🔧 可扩展:灵活的自定义字段机制
  • 🚀 易集成:客户端可以轻松解析和处理错误

通过合理使用这些功能,我们可以构建出更加健壮、用户友好的 REST API,让错误处理不再是开发中的痛点,而是提升用户体验的利器!

TIP

记住:好的错误处理不仅仅是技术实现,更是用户体验的重要组成部分。标准化的错误响应让我们的API更加专业和易用!