Skip to content

Spring MVC 异常处理机制详解 🛡️

概述

在 Web 应用开发中,异常处理是一个至关重要的环节。想象一下,如果你的应用在处理用户请求时发生了错误,但没有合适的异常处理机制,用户看到的可能是一个毫无意义的 500 错误页面,这会严重影响用户体验。

Spring MVC 提供了强大而灵活的异常处理机制,通过 @ExceptionHandler 注解,我们可以优雅地处理各种异常情况,为用户提供友好的错误响应。

IMPORTANT

Spring MVC 的异常处理机制基于 DispatcherServlet 级别的 HandlerExceptionResolver,这意味着它能够统一处理整个应用的异常情况。

核心概念与设计哲学 💡

为什么需要统一异常处理?

在没有统一异常处理机制之前,开发者通常需要在每个 Controller 方法中使用 try-catch 块来处理异常:

kotlin
@RestController
class UserController {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): ResponseEntity<User> {
        return try {
            val user = userService.findById(id) 
            ResponseEntity.ok(user)
        } catch (e: UserNotFoundException) { 
            ResponseEntity.notFound().build() 
        } catch (e: DatabaseException) { 
            ResponseEntity.internalServerError().build() 
        } 
    }
    
    @PostMapping("/users")
    fun createUser(@RequestBody user: User): ResponseEntity<User> {
        return try {
            val savedUser = userService.save(user) 
            ResponseEntity.ok(savedUser)
        } catch (e: ValidationException) { 
            ResponseEntity.badRequest().build() 
        } catch (e: DatabaseException) { 
            ResponseEntity.internalServerError().build() 
        } 
    }
}
kotlin
@RestController
class UserController {
    
    @GetMapping("/users/{id}")
    fun getUser(@PathVariable id: Long): User { 
        return userService.findById(id) 
    } 
    
    @PostMapping("/users")
    fun createUser(@RequestBody user: User): User { 
        return userService.save(user) 
    } 
}

@ControllerAdvice
class GlobalExceptionHandler { 
    
    @ExceptionHandler(UserNotFoundException::class) 
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> { 
        return ResponseEntity.notFound().build() 
    } 
    
    @ExceptionHandler(ValidationException::class) 
    fun handleValidation(ex: ValidationException): ResponseEntity<ErrorResponse> { 
        return ResponseEntity.badRequest().body(ErrorResponse(ex.message)) 
    } 
} 

设计哲学

Spring MVC 异常处理机制遵循以下设计原则:

  1. 关注点分离:业务逻辑专注于核心功能,异常处理逻辑独立管理
  2. 统一性:全局统一的异常处理策略,确保错误响应的一致性
  3. 灵活性:支持多种异常匹配策略和响应格式
  4. 可扩展性:易于添加新的异常类型和处理逻辑

基础用法 🚀

简单异常处理

让我们从一个最基础的例子开始:

kotlin
@RestController
class FileController {
    
    @GetMapping("/files/{filename}")
    fun downloadFile(@PathVariable filename: String): ResponseEntity<ByteArray> {
        // 这里可能抛出 IOException
        val fileContent = fileService.readFile(filename)
        return ResponseEntity.ok(fileContent)
    }
    
    // 处理 IO 异常
    @ExceptionHandler(IOException::class) 
    fun handleIOException(ex: IOException): ResponseEntity<String> { 
        return ResponseEntity.internalServerError() 
            .body("文件读取失败: ${ex.message}") 
    } 
}

TIP

@ExceptionHandler 可以直接定义在 Controller 类中,这样它只会处理该 Controller 中抛出的异常。

全局异常处理

更常见的做法是使用 @ControllerAdvice 创建全局异常处理器:

kotlin
@ControllerAdvice
class GlobalExceptionHandler {
    
    @ExceptionHandler(IOException::class)
    fun handleIOException(ex: IOException): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            code = "FILE_ERROR",
            message = "文件操作失败",
            details = ex.message
        )
        return ResponseEntity.internalServerError().body(errorResponse)
    }
}

data class ErrorResponse(
    val code: String,
    val message: String,
    val details: String? = null,
    val timestamp: Long = System.currentTimeMillis()
)

异常映射机制 🎯

异常匹配规则

Spring MVC 的异常匹配机制非常智能,它不仅能匹配直接抛出的异常,还能匹配嵌套在包装异常中的原因异常:

多异常类型处理

你可以在一个处理方法中处理多种异常类型:

kotlin
@ControllerAdvice
class GlobalExceptionHandler {
    
    // 处理多种文件系统相关异常
    @ExceptionHandler(FileSystemException::class, RemoteException::class) 
    fun handleFileSystemExceptions(ex: IOException): ResponseEntity<ErrorResponse> { 
        val errorResponse = when (ex) {
            is FileSystemException -> ErrorResponse(
                code = "FILE_SYSTEM_ERROR",
                message = "文件系统错误",
                details = ex.message
            )
            is RemoteException -> ErrorResponse(
                code = "REMOTE_ERROR", 
                message = "远程文件访问错误",
                details = ex.message
            )
            else -> ErrorResponse(
                code = "IO_ERROR",
                message = "IO操作错误", 
                details = ex.message
            )
        }
        return ResponseEntity.internalServerError().body(errorResponse)
    }
}

异常优先级

当多个 @ExceptionHandler 方法都能处理同一个异常时,Spring 会根据以下规则选择:

  1. 具体异常优先于通用异常
  2. 根异常优先于原因异常
  3. 使用 ExceptionDepthComparator 比较异常深度
kotlin
@ControllerAdvice
class ExceptionPriorityDemo {
    
    // 更具体的异常处理器 - 优先级高
    @ExceptionHandler(FileNotFoundException::class) 
    fun handleFileNotFound(ex: FileNotFoundException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.notFound().body(
            ErrorResponse("FILE_NOT_FOUND", "文件未找到")
        )
    }
    
    // 通用异常处理器 - 优先级低
    @ExceptionHandler(IOException::class) 
    fun handleIOException(ex: IOException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.internalServerError().body(
            ErrorResponse("IO_ERROR", "IO操作错误")
        )
    }
}

NOTE

FileNotFoundException 继承自 IOException,所以当抛出 FileNotFoundException 时,会优先使用更具体的处理器。

媒体类型映射 🎨

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

kotlin
@ControllerAdvice
class MediaTypeAwareExceptionHandler {
    
    // 为 API 客户端返回 JSON 格式错误
    @ExceptionHandler(produces = ["application/json"]) 
    fun handleJsonError(ex: IllegalArgumentException): ResponseEntity<ErrorResponse> {
        val errorResponse = ErrorResponse(
            code = "INVALID_ARGUMENT",
            message = "参数错误",
            details = ex.message
        )
        return ResponseEntity.badRequest().body(errorResponse)
    }
    
    // 为浏览器返回 HTML 错误页面
    @ExceptionHandler(produces = ["text/html"]) 
    fun handleHtmlError(ex: IllegalArgumentException, model: Model): String {
        model.addAttribute("error", ErrorInfo(
            title = "参数错误",
            message = ex.message ?: "请求参数不正确"
        ))
        return "error/400" // 返回错误页面模板
    }
}

data class ErrorInfo(
    val title: String,
    val message: String
)

这种机制的工作流程如下:

实际业务场景示例 💼

让我们通过一个完整的用户管理系统来演示异常处理的最佳实践:

完整的用户管理异常处理示例
kotlin
// 自定义异常类
sealed class UserException(message: String) : RuntimeException(message)

class UserNotFoundException(userId: Long) : UserException("用户不存在: $userId")
class UserAlreadyExistsException(email: String) : UserException("用户已存在: $email")
class InvalidUserDataException(field: String) : UserException("无效的用户数据: $field")

// 用户控制器
@RestController
@RequestMapping("/api/users")
class UserController(private val userService: UserService) {
    
    @GetMapping("/{id}")
    fun getUser(@PathVariable id: Long): User {
        return userService.findById(id) // 可能抛出 UserNotFoundException
    }
    
    @PostMapping
    fun createUser(@RequestBody @Valid user: CreateUserRequest): User {
        return userService.createUser(user) // 可能抛出 UserAlreadyExistsException
    }
    
    @PutMapping("/{id}")
    fun updateUser(@PathVariable id: Long, @RequestBody user: UpdateUserRequest): User {
        return userService.updateUser(id, user) // 可能抛出多种异常
    }
}

// 全局异常处理器
@ControllerAdvice
class UserExceptionHandler {
    
    private val logger = LoggerFactory.getLogger(UserExceptionHandler::class.java)
    
    @ExceptionHandler(UserNotFoundException::class)
    fun handleUserNotFound(ex: UserNotFoundException): ResponseEntity<ErrorResponse> {
        logger.warn("用户未找到: {}", ex.message)
        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
            ErrorResponse(
                code = "USER_NOT_FOUND",
                message = "用户不存在",
                details = ex.message
            )
        )
    }
    
    @ExceptionHandler(UserAlreadyExistsException::class)
    fun handleUserAlreadyExists(ex: UserAlreadyExistsException): ResponseEntity<ErrorResponse> {
        logger.warn("用户已存在: {}", ex.message)
        return ResponseEntity.status(HttpStatus.CONFLICT).body(
            ErrorResponse(
                code = "USER_ALREADY_EXISTS",
                message = "用户已存在",
                details = ex.message
            )
        )
    }
    
    @ExceptionHandler(InvalidUserDataException::class)
    fun handleInvalidUserData(ex: InvalidUserDataException): ResponseEntity<ErrorResponse> {
        logger.warn("无效用户数据: {}", ex.message)
        return ResponseEntity.badRequest().body(
            ErrorResponse(
                code = "INVALID_USER_DATA",
                message = "用户数据无效",
                details = ex.message
            )
        )
    }
    
    // 处理数据验证异常
    @ExceptionHandler(MethodArgumentNotValidException::class)
    fun handleValidationErrors(ex: MethodArgumentNotValidException): ResponseEntity<ValidationErrorResponse> {
        val errors = ex.bindingResult.fieldErrors.map { error ->
            FieldError(
                field = error.field,
                message = error.defaultMessage ?: "验证失败"
            )
        }
        
        return ResponseEntity.badRequest().body(
            ValidationErrorResponse(
                code = "VALIDATION_ERROR",
                message = "数据验证失败",
                errors = errors
            )
        )
    }
    
    // 通用异常处理
    @ExceptionHandler(Exception::class)
    fun handleGenericException(ex: Exception): ResponseEntity<ErrorResponse> {
        logger.error("未处理的异常", ex)
        return ResponseEntity.internalServerError().body(
            ErrorResponse(
                code = "INTERNAL_ERROR",
                message = "服务器内部错误",
                details = "请联系系统管理员"
            )
        )
    }
}

// 错误响应数据类
data class ErrorResponse(
    val code: String,
    val message: String,
    val details: String? = null,
    val timestamp: Long = System.currentTimeMillis()
)

data class ValidationErrorResponse(
    val code: String,
    val message: String,
    val errors: List<FieldError>,
    val timestamp: Long = System.currentTimeMillis()
)

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

方法参数与返回值 ⚙️

支持的方法参数

@ExceptionHandler 方法支持丰富的参数类型,让你能够获取处理异常所需的各种信息:

kotlin
@ControllerAdvice
class ComprehensiveExceptionHandler {
    
    @ExceptionHandler(DataAccessException::class)
    fun handleDataAccessException(
        ex: DataAccessException,           // 异常对象
        method: HandlerMethod,             // 触发异常的控制器方法信息
        request: HttpServletRequest,       // HTTP 请求对象
        response: HttpServletResponse,     // HTTP 响应对象
        locale: Locale,                    // 当前请求的区域设置
        principal: Principal?              // 当前认证用户
    ): ResponseEntity<ErrorResponse> {
        
        // 记录详细的异常信息
        logger.error(
            "数据访问异常 - 方法: {}, 用户: {}, IP: {}", 
            method.method.name,
            principal?.name ?: "匿名",
            request.remoteAddr
        )
        
        val errorResponse = ErrorResponse(
            code = "DATA_ACCESS_ERROR",
            message = getLocalizedMessage("data.access.error", locale),
            details = if (isDebugMode()) ex.message else null
        )
        
        return ResponseEntity.internalServerError().body(errorResponse)
    }
    
    private fun getLocalizedMessage(key: String, locale: Locale): String {
        // 根据区域设置返回本地化错误消息
        return messageSource.getMessage(key, null, locale)
    }
    
    private fun isDebugMode(): Boolean {
        // 检查是否为调试模式
        return environment.activeProfiles.contains("debug")
    }
}

支持的返回值类型

@ExceptionHandler 方法支持多种返回值类型,以适应不同的响应需求:

kotlin
@ControllerAdvice
class FlexibleExceptionHandler {
    
    // 返回 ResponseEntity - 完全控制响应
    @ExceptionHandler(BusinessException::class)
    fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(ex.httpStatus).body(
            ErrorResponse(ex.code, ex.message)
        )
    }
    
    // 返回视图名称 - 适用于传统 Web 应用
    @ExceptionHandler(produces = ["text/html"])
    fun handleHtmlError(ex: Exception, model: Model): String {
        model.addAttribute("errorMessage", ex.message)
        return "error/generic"
    }
    
    // 返回 @ResponseBody - 自动序列化为 JSON
    @ExceptionHandler(ValidationException::class)
    @ResponseBody
    fun handleValidationException(ex: ValidationException): ErrorResponse {
        return ErrorResponse("VALIDATION_ERROR", ex.message)
    }
    
    // 返回 ModelAndView - 包含视图和模型数据
    @ExceptionHandler(AuthenticationException::class)
    fun handleAuthenticationException(ex: AuthenticationException): ModelAndView {
        val modelAndView = ModelAndView("error/401")
        modelAndView.addObject("message", "认证失败,请重新登录")
        return modelAndView
    }
    
    // void 返回类型 - 直接写入响应
    @ExceptionHandler(RateLimitException::class)
    fun handleRateLimit(ex: RateLimitException, response: HttpServletResponse) {
        response.status = HttpStatus.TOO_MANY_REQUESTS.value()
        response.setHeader("Retry-After", "60")
        response.writer.write("请求过于频繁,请稍后再试")
    }
}

最佳实践与注意事项 ⭐

1. 异常处理器的优先级管理

当使用多个 @ControllerAdvice 时,可以通过 @Order 注解控制优先级:

kotlin
@ControllerAdvice
@Order(1) // 高优先级
class SecurityExceptionHandler {
    
    @ExceptionHandler(AccessDeniedException::class)
    fun handleAccessDenied(ex: AccessDeniedException): ResponseEntity<ErrorResponse> {
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(
            ErrorResponse("ACCESS_DENIED", "访问被拒绝")
        )
    }
}

@ControllerAdvice  
@Order(2) // 低优先级
class GeneralExceptionHandler {
    
    @ExceptionHandler(Exception::class)
    fun handleGeneral(ex: Exception): ResponseEntity<ErrorResponse> {
        return ResponseEntity.internalServerError().body(
            ErrorResponse("INTERNAL_ERROR", "系统错误")
        )
    }
}

2. 异常重新抛出机制

有时你可能希望在某些条件下不处理异常,而是让它继续传播:

kotlin
@ControllerAdvice
class ConditionalExceptionHandler {
    
    @ExceptionHandler(DatabaseException::class)
    fun handleDatabaseException(ex: DatabaseException, request: HttpServletRequest): ResponseEntity<ErrorResponse> {
        // 只处理来自 API 路径的异常
        if (!request.requestURI.startsWith("/api/")) {
            throw ex // 重新抛出,让其他处理器处理
        }
        
        return ResponseEntity.internalServerError().body(
            ErrorResponse("DATABASE_ERROR", "数据库操作失败")
        )
    }
}

3. 日志记录策略

kotlin
@ControllerAdvice
class LoggingExceptionHandler {
    
    private val logger = LoggerFactory.getLogger(LoggingExceptionHandler::class.java)
    
    @ExceptionHandler(Exception::class)
    fun handleException(ex: Exception, request: HttpServletRequest): ResponseEntity<ErrorResponse> {
        
        // 根据异常类型决定日志级别
        when (ex) {
            is BusinessException -> {
                // 业务异常通常是预期的,使用 WARN 级别
                logger.warn("业务异常: {} - URI: {}", ex.message, request.requestURI)
            }
            is ValidationException -> {
                // 验证异常通常是客户端错误,使用 INFO 级别
                logger.info("验证异常: {} - URI: {}", ex.message, request.requestURI)
            }
            else -> {
                // 未知异常可能是系统错误,使用 ERROR 级别
                logger.error("未处理异常 - URI: {}", request.requestURI, ex)
            }
        }
        
        return ResponseEntity.internalServerError().body(
            ErrorResponse("SYSTEM_ERROR", "系统错误")
        )
    }
}

WARNING

在生产环境中,避免在错误响应中暴露敏感的系统信息,如数据库连接字符串、内部路径等。

CAUTION

过度使用通用异常处理器(如 Exception.class)可能会掩盖真正的问题。建议优先处理具体的异常类型。

总结 🎉

Spring MVC 的异常处理机制为我们提供了一套完整、灵活的解决方案:

  • 统一性:通过 @ControllerAdvice 实现全局异常处理
  • 灵活性:支持多种异常匹配策略和响应格式
  • 可扩展性:易于添加新的异常类型和处理逻辑
  • 用户友好:能够根据客户端类型返回合适的错误响应

通过合理使用这些特性,我们可以构建出健壮、用户友好的 Web 应用程序,让异常处理不再是开发中的痛点,而是提升用户体验的有力工具。

TIP

记住,好的异常处理不仅仅是技术实现,更是用户体验设计的重要组成部分。始终从用户的角度思考,提供清晰、有帮助的错误信息。