Skip to content

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 状态码说明
MissingServletRequestParameterException400缺少必需的请求参数
HttpMessageNotReadableException400请求体格式错误
MethodArgumentNotValidException400参数验证失败
NoHandlerFoundException404找不到处理器
HttpRequestMethodNotSupportedException405不支持的请求方法

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. 分层异常处理策略

推荐的异常处理层次

  1. 业务层异常:使用自定义异常类 + @ExceptionHandler
  2. 框架异常:依赖 DefaultHandlerExceptionResolver
  3. 未知异常:全局异常处理器兜底
  4. 容器异常:自定义错误页面

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 的组合方式,它提供了最大的灵活性和可维护性。

通过合理的异常处理策略,我们可以:

  • 🛡️ 保护系统内部实现细节
  • 😊 提供用户友好的错误信息
  • 📊 统一错误响应格式
  • 🔍 便于问题排查和监控

记住:好的异常处理不是隐藏错误,而是优雅地处理错误! 🎯