Appearance
Spring MVC 异常处理机制深度解析 🚀
引言:为什么需要异常处理机制?
想象一下,你正在开发一个在线购物网站。用户在浏览商品时,可能会遇到各种意外情况:
- 商品库存不足
- 网络连接超时
- 用户权限不够
- 系统内部错误
如果没有合适的异常处理机制,这些错误会直接暴露给用户,显示一堆技术性的错误信息,这不仅用户体验糟糕,还可能暴露系统的内部实现细节。
IMPORTANT
Spring MVC 的异常处理机制就是为了解决这个问题:将技术异常转换为用户友好的错误响应,同时保持系统的稳定性和安全性。
核心概念:异常处理的设计哲学
Spring MVC 的异常处理机制基于一个核心理念:责任链模式。当异常发生时,系统会按照预定的顺序,让不同的异常解析器尝试处理这个异常,直到找到合适的处理方式。
四大异常解析器详解
Spring MVC 提供了四种内置的异常解析器,每种都有其特定的使用场景:
1. ExceptionHandlerExceptionResolver 🎯
最灵活、最常用的异常处理方式
这是现代 Spring 应用中最推荐的异常处理方式,通过 @ExceptionHandler
注解来处理异常。
kotlin
@ControllerAdvice
class GlobalExceptionHandler {
// 处理业务异常
@ExceptionHandler(BusinessException::class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
return ResponseEntity.badRequest().body(
ErrorResponse(
code = "BUSINESS_ERROR",
message = ex.message ?: "业务处理失败",
timestamp = System.currentTimeMillis()
)
)
}
// 处理参数验证异常
@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(HttpStatus.INTERNAL_SERVER_ERROR).body(
ErrorResponse(
code = "INTERNAL_ERROR",
message = "系统内部错误,请稍后重试"
)
)
}
}
kotlin
@RestController
@RequestMapping("/api/products")
class ProductController {
@GetMapping("/{id}")
fun getProduct(@PathVariable id: Long): Product {
return productService.findById(id)
?: throw ProductNotFoundException("商品不存在: $id")
}
// 只处理当前控制器的异常
@ExceptionHandler(ProductNotFoundException::class)
fun handleProductNotFound(ex: ProductNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity.notFound().build()
}
}
TIP
@ControllerAdvice
是全局异常处理器,而控制器内的 @ExceptionHandler
只处理当前控制器的异常。当两者都存在时,控制器级别的处理器优先级更高。
2. ResponseStatusExceptionResolver 📊
通过注解声明异常对应的 HTTP 状态码
这种方式适合简单的异常处理场景,直接在异常类上声明对应的 HTTP 状态码。
kotlin
// 自定义异常类
@ResponseStatus(HttpStatus.NOT_FOUND, reason = "用户不存在")
class UserNotFoundException(message: String) : RuntimeException(message)
@ResponseStatus(HttpStatus.BAD_REQUEST, reason = "参数无效")
class InvalidParameterException(message: String) : RuntimeException(message)
@RestController
class UserController {
@GetMapping("/users/{id}")
fun getUser(@PathVariable id: Long): User {
return userService.findById(id)
?: throw UserNotFoundException("用户ID: $id 不存在")
}
@PostMapping("/users")
fun createUser(@RequestBody @Valid userRequest: UserRequest): User {
if (userRequest.email.isBlank()) {
throw InvalidParameterException("邮箱不能为空")
}
return userService.create(userRequest)
}
}
NOTE
使用 @ResponseStatus
的优点是简单直接,缺点是无法自定义响应体的内容,只能返回简单的错误信息。
3. DefaultHandlerExceptionResolver 🔧
处理 Spring MVC 内置异常
这个解析器专门处理 Spring MVC 框架本身抛出的异常,将它们映射为合适的 HTTP 状态码。
kotlin
@RestController
class ApiController {
@GetMapping("/users")
fun getUsers(
@RequestParam(required = true) name: String,
@RequestParam page: Int = 0
): List<User> {
// 如果不传 name 参数,会抛出 MissingServletRequestParameterException
// DefaultHandlerExceptionResolver 会将其转换为 400 Bad Request
return userService.findByName(name, page)
}
@PostMapping("/users")
fun createUser(@RequestBody user: User): User {
// 如果请求体格式错误,会抛出 HttpMessageNotReadableException
// DefaultHandlerExceptionResolver 会将其转换为 400 Bad Request
return userService.create(user)
}
}
常见的 Spring MVC 异常映射:
异常类型 | HTTP 状态码 | 说明 |
---|---|---|
MissingServletRequestParameterException | 400 | 缺少必需的请求参数 |
HttpMessageNotReadableException | 400 | 请求体格式错误 |
MethodArgumentNotValidException | 400 | 参数验证失败 |
NoHandlerFoundException | 404 | 找不到处理器 |
HttpRequestMethodNotSupportedException | 405 | 不支持的请求方法 |
4. SimpleMappingExceptionResolver 🗺️
基于配置的异常映射
这是传统的异常处理方式,通过配置文件将异常类映射到错误页面。
kotlin
@Configuration
class WebConfig : WebMvcConfigurer {
@Bean
fun simpleMappingExceptionResolver(): SimpleMappingExceptionResolver {
val resolver = SimpleMappingExceptionResolver()
// 异常类名到视图名的映射
val mappings = Properties().apply {
setProperty("java.lang.Exception", "error/general")
setProperty("java.lang.RuntimeException", "error/runtime")
setProperty("com.example.BusinessException", "error/business")
}
resolver.setExceptionMappings(mappings)
// 默认错误视图
resolver.setDefaultErrorView("error/default")
// 设置异常属性名(在视图中可以访问)
resolver.setExceptionAttribute("exception")
return resolver
}
}
对应的错误页面模板示例
html
<!-- error/general.html -->
<!DOCTYPE html>
<html>
<head>
<title>系统错误</title>
</head>
<body>
<h1>抱歉,系统遇到了问题</h1>
<p>错误信息:<span th:text="${exception.message}"></span></p>
<p>请稍后重试或联系管理员</p>
</body>
</html>
异常处理链的执行顺序
Spring MVC 按照以下顺序执行异常解析器:
IMPORTANT
解析器的执行顺序可以通过 order
属性来调整。数值越小,优先级越高。
容器错误页面处理
当所有异常解析器都无法处理异常时,异常会冒泡到 Servlet 容器,此时可以配置自定义的错误页面。
传统方式:web.xml 配置
xml
<error-page>
<error-code>404</error-code>
<location>/error/404</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error/500</location>
</error-page>
<error-page>
<location>/error</location>
</error-page>
Spring Boot 方式:错误控制器
kotlin
@RestController
class ErrorController : org.springframework.boot.web.servlet.error.ErrorController {
@RequestMapping("/error")
fun handleError(request: HttpServletRequest): ResponseEntity<Map<String, Any>> {
val status = request.getAttribute("jakarta.servlet.error.status_code") as? Int ?: 500
val message = request.getAttribute("jakarta.servlet.error.message") as? String ?: "未知错误"
val path = request.getAttribute("jakarta.servlet.error.request_uri") as? String ?: ""
val errorResponse = mapOf(
"timestamp" to System.currentTimeMillis(),
"status" to status,
"error" to getErrorReason(status),
"message" to message,
"path" to path
)
return ResponseEntity.status(status).body(errorResponse)
}
private fun getErrorReason(status: Int): String = when (status) {
400 -> "Bad Request"
401 -> "Unauthorized"
403 -> "Forbidden"
404 -> "Not Found"
500 -> "Internal Server Error"
else -> "Unknown Error"
}
}
最佳实践与实战建议
1. 统一的错误响应格式
kotlin
data class ErrorResponse(
val code: String, // 错误代码
val message: String, // 用户友好的错误信息
val details: List<String>? = null, // 详细错误信息(可选)
val timestamp: Long = System.currentTimeMillis(),
val path: String? = null // 请求路径(可选)
)
2. 分层异常处理策略
推荐的异常处理层次
- 业务层异常:使用自定义异常类 +
@ExceptionHandler
- 框架异常:依赖
DefaultHandlerExceptionResolver
- 未知异常:全局异常处理器兜底
- 容器异常:自定义错误页面
3. 完整的异常处理示例
kotlin
// 自定义业务异常
sealed class BusinessException(message: String) : RuntimeException(message) {
class ResourceNotFoundException(resource: String, id: Any) :
BusinessException("$resource with id $id not found")
class ValidationException(field: String, value: Any?) :
BusinessException("Invalid value '$value' for field '$field'")
class PermissionDeniedException(action: String) :
BusinessException("Permission denied for action: $action")
}
@ControllerAdvice
class GlobalExceptionHandler {
private val logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java)
@ExceptionHandler(BusinessException.ResourceNotFoundException::class)
fun handleResourceNotFound(ex: BusinessException.ResourceNotFoundException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(
ErrorResponse(
code = "RESOURCE_NOT_FOUND",
message = ex.message ?: "资源不存在"
)
)
}
@ExceptionHandler(BusinessException.ValidationException::class)
fun handleValidation(ex: BusinessException.ValidationException): ResponseEntity<ErrorResponse> {
return ResponseEntity.badRequest().body(
ErrorResponse(
code = "VALIDATION_ERROR",
message = ex.message ?: "数据验证失败"
)
)
}
@ExceptionHandler(BusinessException.PermissionDeniedException::class)
fun handlePermissionDenied(ex: BusinessException.PermissionDeniedException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(
ErrorResponse(
code = "PERMISSION_DENIED",
message = ex.message ?: "权限不足"
)
)
}
@ExceptionHandler(Exception::class)
fun handleGenericException(ex: Exception, request: HttpServletRequest): ResponseEntity<ErrorResponse> {
logger.error("Unhandled exception occurred", ex)
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(
ErrorResponse(
code = "INTERNAL_ERROR",
message = "系统内部错误,请稍后重试",
path = request.requestURI
)
)
}
}
总结
Spring MVC 的异常处理机制通过责任链模式,提供了灵活而强大的异常处理能力:
✅ ExceptionHandlerExceptionResolver:最灵活,推荐用于业务异常处理
✅ ResponseStatusExceptionResolver:简单直接,适合状态码映射
✅ DefaultHandlerExceptionResolver:处理框架异常,开箱即用
✅ SimpleMappingExceptionResolver:传统配置方式,适合视图应用
NOTE
现代 Spring 应用推荐使用 @ExceptionHandler
+ @ControllerAdvice
的组合方式,它提供了最大的灵活性和可维护性。
通过合理的异常处理策略,我们可以:
- 🛡️ 保护系统内部实现细节
- 😊 提供用户友好的错误信息
- 📊 统一错误响应格式
- 🔍 便于问题排查和监控
记住:好的异常处理不是隐藏错误,而是优雅地处理错误! 🎯