Skip to content

Spring MVC Controller Advice 深度解析 🎯

什么是 Controller Advice?

在 Spring MVC 中,@ControllerAdvice 是一个强大的注解,它允许我们定义全局的异常处理、数据绑定和模型属性设置。简单来说,它就像是一个"全局助手",可以为所有的 Controller 提供统一的处理逻辑。

TIP

@ControllerAdvice 想象成一个"超级管家",它可以统一处理所有 Controller 中的异常、数据绑定等通用逻辑,避免在每个 Controller 中重复编写相同的代码。

为什么需要 Controller Advice? 🤔

传统方式的痛点

在没有 @ControllerAdvice 之前,我们通常需要在每个 Controller 中重复编写异常处理逻辑:

kotlin
@RestController
class UserController {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): User {
        return userService.findById(id)
    }
    
    // 每个Controller都需要重复编写异常处理
    @ExceptionHandler(UserNotFoundException::class) 
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> { 
        return ResponseEntity.status(404) 
            .body(ErrorResponse("用户不存在", ex.message)) 
    } 
}

@RestController
class OrderController {
    
    @GetMapping("/orders/{id}")
    fun getOrder(@PathVariable id: Long): Order {
        return orderService.findById(id)
    }
    
    // 同样的异常处理逻辑又要写一遍!
    @ExceptionHandler(UserNotFoundException::class) 
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> { 
        return ResponseEntity.status(404) 
            .body(ErrorResponse("用户不存在", ex.message)) 
    } 
}
kotlin
// 全局异常处理器
@RestControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(UserNotFoundException::class) 
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> { 
        return ResponseEntity.status(404) 
            .body(ErrorResponse("用户不存在", ex.message)) 
    } 
}

@RestController
class UserController {
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): User {
        return userService.findById(id) // 异常会被全局处理器捕获
    }
}

@RestController
class OrderController {
    @GetMapping("/orders/{id}")
    fun getOrder(@PathVariable id: Long): Order {
        return orderService.findById(id) // 异常会被全局处理器捕获
    }
}

Controller Advice 的核心功能 ⚡

1. 全局异常处理 (@ExceptionHandler)

kotlin
@RestControllerAdvice
class GlobalExceptionHandler {
    
    // 处理业务异常
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.badRequest()
            .body(ErrorResponse(
                code = ex.errorCode,
                message = ex.message ?: "业务处理失败"
            ))
    }
    
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException::class) 
    fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> { 
        val errors = ex.bindingResult.fieldErrors.map { 
            "${it.field}: ${it.defaultMessage}"
        } 
        
        return ResponseEntity.badRequest()
            .body(ErrorResponse(
                code = "VALIDATION_ERROR",
                message = "参数校验失败",
                details = errors
            ))
    }
    
    // 处理系统异常
    @ExceptionHandler(Exception::class)
    fun handleGenericException(ex: Exception): ResponseEntity<ErrorResponse> {
        // 记录日志
        logger.error("系统异常", ex)
        
        return ResponseEntity.status(500)
            .body(ErrorResponse(
                code = "SYSTEM_ERROR",
                message = "系统内部错误"
            ))
    }
}

2. 全局数据绑定 (@InitBinder)

kotlin
@ControllerAdvice
class GlobalDataBindingAdvice {
    
    // 全局日期格式处理
    @InitBinder
    fun initDateBinder(binder: WebDataBinder) {
        val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
        dateFormat.isLenient = false
        binder.registerCustomEditor(Date::class.java, CustomDateEditor(dateFormat, false))
    }
    
    // 字符串去空格处理
    @InitBinder
    fun initStringBinder(binder: WebDataBinder) {
        binder.registerCustomEditor(String::class.java, StringTrimmerEditor(true))
    }
}

3. 全局模型属性 (@ModelAttribute)

kotlin
@ControllerAdvice
class GlobalModelAdvice {
    
    // 为所有Controller添加当前用户信息
    @ModelAttribute("currentUser")
    fun getCurrentUser(request: HttpServletRequest): User? {
        val token = request.getHeader("Authorization")
        return if (token != null) {
            userService.getUserFromToken(token)
        } else null
    }
    
    // 添加系统配置信息
    @ModelAttribute("systemInfo")
    fun getSystemInfo(): SystemInfo {
        return SystemInfo(
            version = "1.0.0",
            environment = environment.getProperty("spring.profiles.active") ?: "unknown"
        )
    }
}

@ControllerAdvice vs @RestControllerAdvice 🆚

NOTE

@RestControllerAdvice@ControllerAdvice@ResponseBody 的组合注解,专门用于 REST API 的全局处理。

kotlin
// 等价写法
@ControllerAdvice
@ResponseBody
class GlobalExceptionHandler {
    // ...
}

// 简化写法
@RestControllerAdvice
class GlobalExceptionHandler {
    // ...
}

精确控制作用范围 🎯

@ControllerAdvice 支持多种方式来精确控制其作用范围:

1. 基于注解类型

kotlin
// 只对标注了 @RestController 的控制器生效
@ControllerAdvice(annotations = [RestController::class])
class RestControllerAdvice {
    
    @ExceptionHandler(Exception::class)
    fun handleRestException(ex: Exception): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(500)
            .body(ErrorResponse("REST API 错误", ex.message))
    }
}

2. 基于包路径

kotlin
// 只对指定包下的控制器生效
@ControllerAdvice("com.example.api.controller")
class ApiControllerAdvice {
    
    @ExceptionHandler(ApiException::class)
    fun handleApiException(ex: ApiException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.badRequest()
            .body(ErrorResponse("API 调用失败", ex.message))
    }
}

3. 基于类型

kotlin
// 只对实现了特定接口或继承特定类的控制器生效
@ControllerAdvice(assignableTypes = [BaseController::class, ApiController::class])
class SpecificControllerAdvice {
    
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.badRequest()
            .body(ErrorResponse("业务异常", ex.message))
    }
}

执行顺序与优先级 📊

Spring 在处理异常和模型属性时有特定的执行顺序:

IMPORTANT

  • 异常处理:本地 @ExceptionHandler 优先于全局的
  • 模型属性:全局 @ModelAttribute 优先于本地的
  • 数据绑定:全局 @InitBinder 优先于本地的

实战案例:构建完整的异常处理体系 🛠️

让我们构建一个完整的异常处理体系:

完整的异常处理示例
kotlin
// 1. 定义统一的错误响应格式
data class ErrorResponse(
    val code: String,
    val message: String,
    val timestamp: Long = System.currentTimeMillis(),
    val details: List<String>? = null
)

// 2. 定义业务异常基类
open class BusinessException(
    val errorCode: String,
    message: String,
    cause: Throwable? = null
) : RuntimeException(message, cause)

// 3. 具体的业务异常
class UserNotFoundException(userId: Long) : BusinessException(
    "USER_NOT_FOUND",
    "用户不存在: $userId"
)

class InsufficientBalanceException(required: BigDecimal, available: BigDecimal) : BusinessException(
    "INSUFFICIENT_BALANCE",
    "余额不足,需要: $required,可用: $available"
)

// 4. 全局异常处理器
@RestControllerAdvice
class GlobalExceptionHandler {
    
    private val logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
    
    // 处理业务异常
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
        logger.warn("业务异常: {}", ex.message)
        
        return ResponseEntity.badRequest()
            .body(ErrorResponse(
                code = ex.errorCode,
                message = ex.message ?: "业务处理失败"
            ))
    }
    
    // 处理参数校验异常
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationException(ex: MethodArgumentNotValidException): ResponseEntity<ErrorResponse> {
        val errors = ex.bindingResult.fieldErrors.map { 
            "${it.field}: ${it.defaultMessage}" 
        }
        
        return ResponseEntity.badRequest()
            .body(ErrorResponse(
                code = "VALIDATION_ERROR",
                message = "参数校验失败",
                details = errors
            ))
    }
    
    // 处理HTTP方法不支持异常
    @ExceptionHandler(HttpRequestMethodNotSupportedException::class)
    fun handleMethodNotSupported(ex: HttpRequestMethodNotSupportedException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(405)
            .body(ErrorResponse(
                code = "METHOD_NOT_ALLOWED",
                message = "不支持的HTTP方法: ${ex.method}"
            ))
    }
    
    // 处理系统异常
    @ExceptionHandler(Exception::class)
    fun handleGenericException(ex: Exception): ResponseEntity<ErrorResponse> {
        logger.error("系统异常", ex)
        
        return ResponseEntity.status(500)
            .body(ErrorResponse(
                code = "SYSTEM_ERROR",
                message = "系统内部错误"
            ))
    }
}

// 5. 使用示例
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): User {
        // 如果用户不存在,会抛出 UserNotFoundException
        // 异常会被 GlobalExceptionHandler 捕获并处理
        return userService.findById(id) ?: throw UserNotFoundException(id)
    }
    
    @PostMapping
    fun createUser(@Valid @RequestBody request: CreateUserRequest): User {
        // 如果参数校验失败,会抛出 MethodArgumentNotValidException
        // 异常会被 GlobalExceptionHandler 捕获并处理
        return userService.create(request)
    }
}

最佳实践与注意事项 ⚠️

1. 合理组织异常处理器

kotlin
// 按功能模块分组
@RestControllerAdvice("com.example.user")
class UserModuleExceptionHandler {
    // 用户模块相关异常处理
}

@RestControllerAdvice("com.example.order")
class OrderModuleExceptionHandler {
    // 订单模块相关异常处理
}

@RestControllerAdvice
class GlobalExceptionHandler {
    // 通用异常处理
}

2. 避免过度使用

WARNING

不要在 @ControllerAdvice 中处理所有异常。只处理真正需要全局处理的异常,特定业务逻辑的异常应该在对应的 Controller 中处理。

3. 性能考虑

kotlin
// ❌ 避免复杂的选择器,会影响性能
@ControllerAdvice(
    annotations = [RestController::class, Controller::class],
    basePackages = ["com.example.api", "com.example.web"],
    assignableTypes = [BaseController::class]
)
class ComplexAdvice

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

总结 📝

@ControllerAdvice 是 Spring MVC 中一个非常强大的功能,它帮助我们:

  • 统一异常处理:避免在每个 Controller 中重复编写异常处理逻辑
  • 全局数据绑定:统一处理数据格式转换和校验
  • 模型属性共享:为所有 Controller 提供通用的模型数据
  • 代码复用:减少重复代码,提高维护性
  • 关注点分离:将横切关注点从业务逻辑中分离出来

TIP

记住:@ControllerAdvice 就像是一个"全局管家",它让我们的代码更加整洁、可维护,是构建健壮 Spring MVC 应用的重要工具! 🎉