Skip to content

Spring WebFlux Controller Advice 深度解析 🎯

引言:为什么需要 Controller Advice?

想象一下,你正在开发一个电商系统,有用户管理、商品管理、订单管理等多个控制器。每个控制器都需要处理异常、绑定数据、设置模型属性。如果没有 Controller Advice,你会发现:

  • 🔄 重复代码满天飞:每个控制器都要写相同的异常处理逻辑
  • 🤯 维护成本高:修改一个异常处理,需要改动多个文件
  • 😵 代码耦合严重:业务逻辑和异常处理混杂在一起

Controller Advice 就是为了解决这些痛点而生的!它让我们能够在一个地方统一处理多个控制器的横切关注点。

核心概念解析

什么是 Controller Advice?

NOTE

Controller Advice 是 Spring WebFlux 提供的一种全局处理机制,允许我们在一个地方定义适用于多个控制器的异常处理、数据绑定和模型属性设置逻辑。

设计哲学

Controller Advice 遵循了 AOP(面向切面编程) 的思想:

  • 横切关注点分离:将异常处理、数据绑定等横切逻辑从业务代码中分离出来
  • 全局统一管理:在一个地方管理所有控制器的公共逻辑
  • 可选择性应用:可以精确控制哪些控制器受到影响

核心注解对比

@ControllerAdvice vs @RestControllerAdvice

kotlin
@ControllerAdvice
class GlobalControllerAdvice {
    
    @ExceptionHandler(ValidationException::class)
    fun handleValidationError(ex: ValidationException): ModelAndView {
        val modelAndView = ModelAndView("error")
        modelAndView.addObject("message", ex.message)
        return modelAndView 
    }
}
kotlin
@RestControllerAdvice
class GlobalRestControllerAdvice {
    
    @ExceptionHandler(ValidationException::class)
    fun handleValidationError(ex: ValidationException): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            code = "VALIDATION_ERROR",
            message = ex.message ?: "验证失败"
        )
        return ResponseEntity.badRequest().body(errorResponse) 
    }
}

TIP

选择建议

  • 如果你的应用主要提供 RESTful API,使用 @RestControllerAdvice
  • 如果你的应用需要返回视图模板,使用 @ControllerAdvice

实战应用场景

场景1:全局异常处理

kotlin
// 自定义异常类
class BusinessException(
    val code: String,
    override val message: String
) : RuntimeException(message)

class ValidationException(
    override val message: String
) : RuntimeException(message)

// 统一错误响应格式
data class ErrorResponse(
    val code: String,
    val message: String,
    val timestamp: Long = System.currentTimeMillis()
)

@RestControllerAdvice
class GlobalExceptionHandler {
    
    // 处理业务异常
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            code = ex.code,
            message = ex.message
        )
        return ResponseEntity.badRequest().body(errorResponse) 
    }
    
    // 处理验证异常
    @ExceptionHandler(ValidationException::class)
    fun handleValidationException(ex: ValidationException): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            code = "VALIDATION_ERROR",
            message = ex.message
        )
        return ResponseEntity.badRequest().body(errorResponse) 
    }
    
    // 处理未知异常
    @ExceptionHandler(Exception::class)
    fun handleGenericException(ex: Exception): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            code = "INTERNAL_ERROR",
            message = "系统内部错误,请稍后重试"
        )
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
            .body(errorResponse) 
    }
}

场景2:全局数据绑定

kotlin
@ControllerAdvice
class GlobalDataBindingAdvice {
    
    // 全局初始化数据绑定器
    @InitBinder
    fun initBinder(binder: WebDataBinder) {
        // 设置日期格式
        val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, true)) 
        
        // 防止XSS攻击,过滤HTML标签
        binder.registerCustomEditor(String::class.java, StringTrimmerEditor(true)) 
    }
    
    // 全局模型属性
    @ModelAttribute("currentUser")
    fun getCurrentUser(request: ServerHttpRequest): Mono<User> {
        // 从请求中获取当前用户信息
        val token = request.headers.getFirst("Authorization")
        return if (token != null) {
            userService.getUserByToken(token) 
        } else {
            Mono.empty()
        }
    }
}

精确控制作用范围

按注解类型限制

kotlin
// 只对标注了 @RestController 的控制器生效
@RestControllerAdvice(annotations = [RestController::class]) 
class RestApiExceptionHandler {
    
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.badRequest().body(
            ErrorResponse(ex.code, ex.message)
        )
    }
}

按包路径限制

kotlin
// 只对指定包下的控制器生效
@RestControllerAdvice("com.example.api.controller") 
class ApiExceptionHandler {
    
    @ExceptionHandler(ApiException::class)
    fun handleApiException(ex: ApiException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(ex.httpStatus).body(
            ErrorResponse(ex.code, ex.message)
        )
    }
}

按类型限制

kotlin
// 基础控制器接口
interface BaseController

// API控制器基类
abstract class ApiController : BaseController

// 只对实现了特定接口或继承了特定类的控制器生效
@RestControllerAdvice(assignableTypes = [BaseController::class, ApiController::class]) 
class TypeBasedExceptionHandler {
    
    @ExceptionHandler(SecurityException::class)
    fun handleSecurityException(ex: SecurityException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(
            ErrorResponse("SECURITY_ERROR", "访问被拒绝")
        )
    }
}

执行顺序与优先级

IMPORTANT

执行优先级规则

  1. @ExceptionHandler:本地(Controller内)> 全局(ControllerAdvice)
  2. @ModelAttribute@InitBinder:全局(ControllerAdvice)> 本地(Controller内)

完整实战示例

点击查看完整的电商系统异常处理示例
kotlin
// ============= 异常定义 =============
sealed class ECommerceException(
    val code: String,
    override val message: String,
    val httpStatus: HttpStatus = HttpStatus.BAD_REQUEST
) : RuntimeException(message)

class ProductNotFoundException(productId: String) : ECommerceException(
    code = "PRODUCT_NOT_FOUND",
    message = "商品不存在: $productId",
    httpStatus = HttpStatus.NOT_FOUND
)

class InsufficientStockException(productId: String, available: Int, requested: Int) : ECommerceException(
    code = "INSUFFICIENT_STOCK",
    message = "库存不足,商品ID: $productId, 可用: $available, 请求: $requested"
)

class PaymentFailedException(reason: String) : ECommerceException(
    code = "PAYMENT_FAILED",
    message = "支付失败: $reason"
)

// ============= 响应格式 =============
data class ApiResponse<T>(
    val success: Boolean,
    val data: T? = null,
    val error: ErrorDetail? = null,
    val timestamp: Long = System.currentTimeMillis()
)

data class ErrorDetail(
    val code: String,
    val message: String,
    val details: Map<String, Any>? = null
)

// ============= 全局异常处理器 =============
@RestControllerAdvice
@Slf4j
class ECommerceExceptionHandler {
    
    // 处理电商业务异常
    @ExceptionHandler(ECommerceException::class)
    fun handleECommerceException(ex: ECommerceException): ResponseEntity<ApiResponse<Nothing>> {
        log.warn("业务异常: ${ex.code} - ${ex.message}")
        
        val response = ApiResponse<Nothing>(
            success = false,
            error = ErrorDetail(
                code = ex.code,
                message = ex.message
            )
        )
        
        return ResponseEntity.status(ex.httpStatus).body(response) 
    }
    
    // 处理参数验证异常
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ApiResponse<Nothing>> {
        val errors = ex.bindingResult.fieldErrors.associate { 
            it.field to (it.defaultMessage ?: "验证失败")
        }
        
        val response = ApiResponse<Nothing>(
            success = false,
            error = ErrorDetail(
                code = "VALIDATION_ERROR",
                message = "请求参数验证失败",
                details = errors
            )
        )
        
        return ResponseEntity.badRequest().body(response) 
    }
    
    // 处理访问拒绝异常
    @ExceptionHandler(AccessDeniedException::class)
    fun handleAccessDeniedException(ex: AccessDeniedException): ResponseEntity<ApiResponse<Nothing>> {
        log.warn("访问被拒绝: ${ex.message}")
        
        val response = ApiResponse<Nothing>(
            success = false,
            error = ErrorDetail(
                code = "ACCESS_DENIED",
                message = "您没有权限执行此操作"
            )
        )
        
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response) 
    }
    
    // 处理系统异常
    @ExceptionHandler(Exception::class)
    fun handleGenericException(ex: Exception): ResponseEntity<ApiResponse<Nothing>> {
        log.error("系统异常", ex) 
        
        val response = ApiResponse<Nothing>(
            success = false,
            error = ErrorDetail(
                code = "SYSTEM_ERROR",
                message = "系统繁忙,请稍后重试"
            )
        )
        
        return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response) 
    }
    
    // 全局模型属性 - 添加请求追踪ID
    @ModelAttribute("traceId")
    fun addTraceId(): String {
        return UUID.randomUUID().toString() 
    }
}

// ============= 使用示例 =============
@RestController
@RequestMapping("/api/products")
class ProductController(
    private val productService: ProductService
) {
    
    @GetMapping("/{id}")
    fun getProduct(@PathVariable id: String): Mono<ApiResponse<Product>> {
        return productService.findById(id)
            .map { product ->
                ApiResponse(
                    success = true,
                    data = product
                )
            }
            .switchIfEmpty(
                Mono.error(ProductNotFoundException(id)) 
            )
    }
    
    @PostMapping("/{id}/purchase")
    fun purchaseProduct(
        @PathVariable id: String,
        @RequestBody @Valid request: PurchaseRequest
    ): Mono<ApiResponse<PurchaseResult>> {
        return productService.purchase(id, request.quantity)
            .map { result ->
                ApiResponse(
                    success = true,
                    data = result
                )
            }
            .onErrorMap { ex ->
                when (ex) {
                    is IllegalArgumentException -> InsufficientStockException(id, 0, request.quantity) 
                    else -> ex
                }
            }
    }
}

性能考虑与最佳实践

性能优化建议

WARNING

性能陷阱:过度使用选择器可能影响性能

kotlin
// ❌ 避免:过于复杂的选择器
@RestControllerAdvice(
    annotations = [RestController::class, Controller::class],
    basePackages = ["com.example.api", "com.example.web"],
    assignableTypes = [BaseController::class, ApiController::class]
) 
class OverComplexAdvice

// ✅ 推荐:简单明确的选择器
@RestControllerAdvice("com.example.api") 
class ApiExceptionHandler

最佳实践

最佳实践建议

  1. 按功能模块分离:不同模块使用不同的 ControllerAdvice
  2. 异常层次化设计:建立清晰的异常继承体系
  3. 统一响应格式:所有API使用相同的响应结构
  4. 日志记录:在异常处理中添加适当的日志
  5. 避免过度捕获:不要捕获过于宽泛的异常类型

总结

Controller Advice 是 Spring WebFlux 中处理横切关注点的强大工具:

  • 🎯 解决痛点:消除重复代码,统一异常处理
  • 🔧 灵活配置:支持精确控制作用范围
  • 📊 执行有序:明确的优先级规则
  • 🚀 提升效率:让开发者专注于业务逻辑

通过合理使用 Controller Advice,我们可以构建出更加健壮、可维护的 WebFlux 应用程序! ✨