Appearance
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
推荐的异常处理策略:
- 控制器级别:处理特定于该控制器的业务异常
- 全局级别:处理通用异常(如验证异常、系统异常)
- 优先级:控制器级别的处理器优先于全局处理器
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 方法支持的参数:
- 异常对象本身
ServerHttpRequest
、ServerHttpResponse
ServerWebExchange
Model
(仅用于视图渲染)- 其他
@RequestMapping
支持的参数(除了请求体相关参数)
NOTE
@ExceptionHandler 方法支持的返回值:
ResponseEntity<T>
Mono<ResponseEntity<T>>
String
(视图名称)@ResponseBody
注解的对象- 其他
@RequestMapping
支持的返回值
8. 总结 📋
@ExceptionHandler 是 Spring WebFlux 中处理异常的强大工具,它让我们能够:
✅ 优雅处理异常:将技术异常转换为用户友好的响应
✅ 支持内容协商:根据客户端需求返回不同格式的错误信息
✅ 灵活的处理策略:支持控制器级别和全局级别的异常处理
✅ 丰富的参数支持:与 @RequestMapping 方法具有相似的参数和返回值支持
通过合理使用 @ExceptionHandler,我们可以构建出既健壮又用户友好的 Web 应用程序。记住,好的异常处理不仅仅是捕获异常,更重要的是为用户提供有意义的反馈,同时为开发者提供足够的调试信息。
TIP
在实际项目中,建议建立统一的异常处理规范,包括错误码定义、日志记录格式、响应结构等,这样可以让整个团队的异常处理更加一致和专业。