Appearance
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
执行优先级规则:
- @ExceptionHandler:本地(Controller内)> 全局(ControllerAdvice)
- @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
最佳实践
最佳实践建议
- 按功能模块分离:不同模块使用不同的 ControllerAdvice
- 异常层次化设计:建立清晰的异常继承体系
- 统一响应格式:所有API使用相同的响应结构
- 日志记录:在异常处理中添加适当的日志
- 避免过度捕获:不要捕获过于宽泛的异常类型
总结
Controller Advice 是 Spring WebFlux 中处理横切关注点的强大工具:
- 🎯 解决痛点:消除重复代码,统一异常处理
- 🔧 灵活配置:支持精确控制作用范围
- 📊 执行有序:明确的优先级规则
- 🚀 提升效率:让开发者专注于业务逻辑
通过合理使用 Controller Advice,我们可以构建出更加健壮、可维护的 WebFlux 应用程序! ✨