Skip to content

Spring WebFlux 异常处理:@ExceptionHandler 完全指南 🚀

1. 什么是 @ExceptionHandler?为什么需要它? 🤔

在 Web 应用开发中,异常处理是一个永恒的话题。想象一下,如果你的 API 在处理用户请求时发生了异常,而你没有合适的异常处理机制,用户看到的可能是一堆难以理解的错误堆栈信息,这显然不是我们想要的用户体验。

IMPORTANT

@ExceptionHandler 的核心价值:它让我们能够优雅地处理控制器中抛出的异常,将技术错误转换为用户友好的响应。

没有异常处理 vs 有异常处理的对比

kotlin
@RestController
class UserController {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: String): Mono<User> {
        // 如果用户不存在,会抛出 UserNotFoundException
        // 用户会看到 500 错误和复杂的堆栈信息 😱
        return userService.findById(id)
    }
}
kotlin
@RestController
class UserController {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: String): Mono<User> {
        return userService.findById(id)
    }
    
    @ExceptionHandler(UserNotFoundException::class) 
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> { 
        return ResponseEntity.status(HttpStatus.NOT_FOUND) 
            .body(ErrorResponse("用户不存在", "USER_NOT_FOUND")) 
    } 
}

2. @ExceptionHandler 的工作原理 ⚙️

让我们通过时序图来理解 @ExceptionHandler 的工作流程:

NOTE

Spring WebFlux 中的 @ExceptionHandler 由 HandlerAdapter 提供支持,它会自动捕获控制器方法中抛出的异常,并路由到相应的异常处理方法。

3. 基础用法:在控制器中处理异常 📝

3.1 简单的异常处理

kotlin
@RestController
class FileController {
    
    @GetMapping("/files/{filename}")
    fun downloadFile(@PathVariable filename: String): Mono<ByteArray> {
        return fileService.readFile(filename)
    }
    
    // 处理 IOException
    @ExceptionHandler(IOException::class) 
    fun handleIOException(ex: IOException): ResponseEntity<String> { 
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 
            .body("文件读取失败: ${ex.message}") 
    } 
}

3.2 处理多种异常类型

kotlin
@RestController
class ProductController {
    
    @GetMapping("/products/{id}")
    fun getProduct(@PathVariable id: Long): Mono<Product> {
        return productService.findById(id)
    }
    
    // 处理多种异常类型
    @ExceptionHandler(ProductNotFoundException::class, CategoryNotFoundException::class) 
    fun handleNotFound(ex: RuntimeException): ResponseEntity<ErrorResponse> { 
        val errorCode = when (ex) { 
            is ProductNotFoundException -> "PRODUCT_NOT_FOUND"
            is CategoryNotFoundException -> "CATEGORY_NOT_FOUND"
            else -> "RESOURCE_NOT_FOUND"
        } 
        
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse(ex.message ?: "资源未找到", errorCode))
    }
}

4. 高级特性:媒体类型映射 🎯

Spring WebFlux 的 @ExceptionHandler 支持根据客户端请求的媒体类型返回不同格式的错误响应。

4.1 根据 Accept 头返回不同格式

kotlin
@RestController
class OrderController {
    
    @PostMapping("/orders")
    fun createOrder(@RequestBody order: Order): Mono<Order> {
        return orderService.create(order)
    }
    
    // 为 JSON 客户端返回 JSON 错误
    @ExceptionHandler(IllegalArgumentException::class, produces = ["application/json"]) 
    fun handleJsonError(ex: IllegalArgumentException): ResponseEntity<JsonErrorResponse> { 
        return ResponseEntity.badRequest() 
            .body(JsonErrorResponse( 
                error = "INVALID_REQUEST", 
                message = ex.message ?: "请求参数无效", 
                timestamp = Instant.now(), 
                code = 400
            )) 
    } 
    
    // 为浏览器返回 HTML 错误页面
    @ExceptionHandler(IllegalArgumentException::class, produces = ["text/html"]) 
    fun handleHtmlError(ex: IllegalArgumentException, model: Model): String { 
        model.addAttribute("error", "请求参数无效") 
        model.addAttribute("message", ex.message) 
        return "error/bad-request" // 返回错误页面模板
    } 
}

// 定义错误响应数据类
data class JsonErrorResponse(
    val error: String,
    val message: String,
    val timestamp: Instant,
    val code: Int
)

4.2 内容协商的工作原理

TIP

使用媒体类型映射可以让同一个应用同时服务于 Web 页面和 API 客户端,提供不同格式的错误响应。

5. 全局异常处理:@ControllerAdvice 🌐

对于需要在多个控制器间共享的异常处理逻辑,我们可以使用 @ControllerAdvice

kotlin
@ControllerAdvice
class GlobalExceptionHandler {
    
    // 处理所有控制器的验证异常
    @ExceptionHandler(MethodArgumentNotValidException::class) 
    fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ValidationErrorResponse> { 
        val errors = ex.bindingResult.fieldErrors.map { fieldError ->
            FieldError( 
                field = fieldError.field, 
                message = fieldError.defaultMessage ?: "验证失败"
            ) 
        } 
        
        return ResponseEntity.badRequest()
            .body(ValidationErrorResponse("请求参数验证失败", errors))
    }
    
    // 处理所有未捕获的异常
    @ExceptionHandler(Exception::class) 
    fun handleGenericException(ex: Exception): ResponseEntity<ErrorResponse> { 
        // 记录异常日志
        logger.error("未处理的异常", ex) 
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(ErrorResponse("服务器内部错误", "INTERNAL_ERROR"))
    }
    
    companion object {
        private val logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
    }
}

// 验证错误响应数据类
data class ValidationErrorResponse(
    val message: String,
    val errors: List<FieldError>
)

data class FieldError(
    val field: String,
    val message: String
)

data class ErrorResponse(
    val message: String,
    val code: String
)

6. 实际业务场景示例 💼

让我们看一个完整的电商订单系统的异常处理示例:

完整的订单控制器异常处理示例
kotlin
@RestController
@RequestMapping("/api/orders")
class OrderController(
    private val orderService: OrderService
) {
    
    @PostMapping
    fun createOrder(@Valid @RequestBody orderRequest: CreateOrderRequest): Mono<OrderResponse> {
        return orderService.createOrder(orderRequest)
            .map { order -> OrderResponse.from(order) }
    }
    
    @GetMapping("/{orderId}")
    fun getOrder(@PathVariable orderId: String): Mono<OrderResponse> {
        return orderService.findById(orderId)
            .map { order -> OrderResponse.from(order) }
    }
    
    // 处理订单不存在异常
    @ExceptionHandler(OrderNotFoundException::class)
    fun handleOrderNotFound(ex: OrderNotFoundException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
            .body(ErrorResponse(
                message = "订单不存在",
                code = "ORDER_NOT_FOUND",
                details = ex.message
            ))
    }
    
    // 处理库存不足异常
    @ExceptionHandler(InsufficientStockException::class)
    fun handleInsufficientStock(ex: InsufficientStockException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.CONFLICT)
            .body(ErrorResponse(
                message = "商品库存不足",
                code = "INSUFFICIENT_STOCK",
                details = "商品 ${ex.productName} 库存不足,当前库存:${ex.availableStock}"
            ))
    }
    
    // 处理支付失败异常
    @ExceptionHandler(PaymentFailedException::class)
    fun handlePaymentFailed(ex: PaymentFailedException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.PAYMENT_REQUIRED)
            .body(ErrorResponse(
                message = "支付失败",
                code = "PAYMENT_FAILED",
                details = ex.reason
            ))
    }
}

// 自定义异常类
class OrderNotFoundException(orderId: String) : RuntimeException("Order not found: $orderId")

class InsufficientStockException(
    val productName: String,
    val availableStock: Int
) : RuntimeException("Insufficient stock for product: $productName")

class PaymentFailedException(val reason: String) : RuntimeException("Payment failed: $reason")

// 响应数据类
data class ErrorResponse(
    val message: String,
    val code: String,
    val details: String? = null,
    val timestamp: Instant = Instant.now()
)

7. 最佳实践与注意事项 ⚠️

7.1 异常处理的层次结构

IMPORTANT

推荐的异常处理策略

  1. 控制器级别:处理特定于该控制器的业务异常
  2. 全局级别:处理通用异常(如验证异常、系统异常)
  3. 优先级:控制器级别的处理器优先于全局处理器

7.2 异常处理的注意事项

重要提醒

  • 不要在异常处理器中再次抛出异常,这会导致无限循环
  • 记录异常日志,特别是对于系统级异常
  • 不要暴露敏感信息,如数据库连接信息、内部系统路径等
kotlin
@ExceptionHandler(DatabaseException::class)
fun handleDatabaseException(ex: DatabaseException): ResponseEntity<ErrorResponse> {
    // ✅ 记录详细的异常信息用于调试
    logger.error("数据库操作失败", ex) 
    
    // ✅ 返回用户友好的错误信息,不暴露内部细节
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) 
        .body(ErrorResponse("系统暂时不可用,请稍后重试", "SYSTEM_ERROR")) 
    
    // ❌ 不要这样做:暴露数据库连接信息
    // .body(ErrorResponse(ex.message, "DB_ERROR"))
}

7.3 方法参数和返回值支持

NOTE

@ExceptionHandler 方法支持的参数

  • 异常对象本身
  • ServerHttpRequestServerHttpResponse
  • ServerWebExchange
  • Model(仅用于视图渲染)
  • 其他 @RequestMapping 支持的参数(除了请求体相关参数)

NOTE

@ExceptionHandler 方法支持的返回值

  • ResponseEntity<T>
  • Mono<ResponseEntity<T>>
  • String(视图名称)
  • @ResponseBody 注解的对象
  • 其他 @RequestMapping 支持的返回值

8. 总结 📋

@ExceptionHandler 是 Spring WebFlux 中处理异常的强大工具,它让我们能够:

优雅处理异常:将技术异常转换为用户友好的响应
支持内容协商:根据客户端需求返回不同格式的错误信息
灵活的处理策略:支持控制器级别和全局级别的异常处理
丰富的参数支持:与 @RequestMapping 方法具有相似的参数和返回值支持

通过合理使用 @ExceptionHandler,我们可以构建出既健壮又用户友好的 Web 应用程序。记住,好的异常处理不仅仅是捕获异常,更重要的是为用户提供有意义的反馈,同时为开发者提供足够的调试信息。

TIP

在实际项目中,建议建立统一的异常处理规范,包括错误码定义、日志记录格式、响应结构等,这样可以让整个团队的异常处理更加一致和专业。