Appearance
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 异常处理机制遵循以下设计原则:
- 关注点分离:业务逻辑专注于核心功能,异常处理逻辑独立管理
- 统一性:全局统一的异常处理策略,确保错误响应的一致性
- 灵活性:支持多种异常匹配策略和响应格式
- 可扩展性:易于添加新的异常类型和处理逻辑
基础用法 🚀
简单异常处理
让我们从一个最基础的例子开始:
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 会根据以下规则选择:
- 具体异常优先于通用异常
- 根异常优先于原因异常
- 使用
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
记住,好的异常处理不仅仅是技术实现,更是用户体验设计的重要组成部分。始终从用户的角度思考,提供清晰、有帮助的错误信息。