Skip to content

Spring MVC 拦截器 (HandlerInterceptor) 深度解析 🚀

什么是拦截器?为什么需要它?

想象一下,你正在经营一家餐厅。每当客人进门时,你希望:

  • 检查客人是否有预约(身份验证)
  • 为客人提供菜单(准备工作)
  • 在客人用餐后清理桌子(清理工作)
  • 记录客人的用餐时间(日志记录)

在 Web 应用中,拦截器就像餐厅的服务员,它们在请求到达控制器之前、之后以及完全处理完成后执行特定的逻辑。

IMPORTANT

拦截器是 Spring MVC 提供的一种 AOP(面向切面编程)机制,允许我们在不修改业务代码的情况下,横向地为多个请求添加通用功能。

拦截器的核心原理

拦截器的三个关键方法 🔧

1. preHandle() - 前置处理

这是请求处理的"门卫",在控制器方法执行前调用。

kotlin
override fun preHandle(
    request: HttpServletRequest,
    response: HttpServletResponse,
    handler: Any
): Boolean {
    // 返回 true:继续执行后续流程
    // 返回 false:中断执行,直接返回响应
    return true
}

2. postHandle() - 后置处理

在控制器方法执行后,但在视图渲染前调用。

kotlin
override fun postHandle(
    request: HttpServletRequest,
    response: HttpServletResponse,
    handler: Any,
    modelAndView: ModelAndView?
) {
    // 可以修改 ModelAndView
    // 添加通用的模型数据
}

3. afterCompletion() - 完成后处理

在整个请求处理完成后调用,包括视图渲染。

kotlin
override fun afterCompletion(
    request: HttpServletRequest,
    response: HttpServletResponse,
    handler: Any,
    ex: Exception?
) {
    // 清理资源、记录日志等
    // 即使发生异常也会执行
}

实战案例:构建一个完整的拦截器

让我们创建一个实用的拦截器,它能够:

  • 记录请求日志
  • 验证用户权限
  • 统计请求耗时
  • 添加通用响应头
kotlin
@Component
class LoggingInterceptor : HandlerInterceptor {
    
    private val logger = LoggerFactory.getLogger(LoggingInterceptor::class.java)
    private val startTimeAttribute = "startTime"
    
    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        val startTime = System.currentTimeMillis()
        request.setAttribute(startTimeAttribute, startTime) 
        
        // 记录请求开始日志
        logger.info("请求开始 - URL: ${request.requestURL}, 方法: ${request.method}")
        
        // 简单的权限检查示例
        val userRole = request.getHeader("User-Role")
        if (isProtectedResource(request) && userRole != "ADMIN") {
            response.status = HttpServletResponse.SC_FORBIDDEN 
            response.writer.write("权限不足")
            return false // 中断执行
        }
        
        return true
    }
    
    override fun postHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        modelAndView: ModelAndView?
    ) {
        // 添加通用的模型数据
        modelAndView?.addObject("serverTime", LocalDateTime.now()) 
        
        // 添加通用响应头
        response.setHeader("X-Powered-By", "Spring Boot Kotlin")
    }
    
    override fun afterCompletion(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any,
        ex: Exception?
    ) {
        val startTime = request.getAttribute(startTimeAttribute) as Long
        val endTime = System.currentTimeMillis()
        val duration = endTime - startTime
        
        // 记录请求完成日志
        logger.info("请求完成 - 耗时: ${duration}ms, 状态码: ${response.status}")
        
        // 如果有异常,记录异常信息
        ex?.let {
            logger.error("请求处理异常", it) 
        }
    }
    
    private fun isProtectedResource(request: HttpServletRequest): Boolean {
        return request.requestURI.startsWith("/admin/")
    }
}
kotlin
@Configuration
class WebMvcConfig(
    private val loggingInterceptor: LoggingInterceptor
) : WebMvcConfigurer {
    
    override fun addInterceptors(registry: InterceptorRegistry) {
        registry.addInterceptor(loggingInterceptor)
            .addPathPatterns("/**") // 拦截所有请求
            .excludePathPatterns( // 排除静态资源
                "/static/**",
                "/css/**",
                "/js/**",
                "/images/**"
            )
    }
}

多个拦截器的执行顺序 📋

当配置多个拦截器时,它们的执行顺序遵循特定规律:

kotlin
@Configuration
class WebMvcConfig : WebMvcConfigurer {
    
    override fun addInterceptors(registry: InterceptorRegistry) {
        // 执行顺序:SecurityInterceptor -> LoggingInterceptor -> PerformanceInterceptor
        registry.addInterceptor(SecurityInterceptor()).order(1) 
        registry.addInterceptor(LoggingInterceptor()).order(2) 
        registry.addInterceptor(PerformanceInterceptor()).order(3) 
    }
}

实际业务场景应用 💼

场景1:API 限流拦截器

kotlin
@Component
class RateLimitInterceptor : HandlerInterceptor {
    
    private val rateLimiter = RateLimiter.create(10.0) // 每秒10个请求
    
    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        if (!rateLimiter.tryAcquire()) {
            response.status = 429 // Too Many Requests
            response.writer.write("请求过于频繁,请稍后再试")
            return false
        }
        return true
    }
}

场景2:用户会话验证拦截器

kotlin
@Component
class SessionValidationInterceptor : HandlerInterceptor {
    
    override fun preHandle(
        request: HttpServletRequest,
        response: HttpServletResponse,
        handler: Any
    ): Boolean {
        val token = request.getHeader("Authorization")
        
        if (token.isNullOrBlank()) {
            response.status = HttpServletResponse.SC_UNAUTHORIZED
            response.writer.write("缺少认证令牌")
            return false
        }
        
        // 验证 token 有效性
        if (!isValidToken(token)) {
            response.status = HttpServletResponse.SC_UNAUTHORIZED 
            response.writer.write("无效的认证令牌")
            return false
        }
        
        // 将用户信息存储到请求属性中
        val userInfo = getUserInfoFromToken(token)
        request.setAttribute("currentUser", userInfo) 
        
        return true
    }
    
    private fun isValidToken(token: String): Boolean {
        // 实现 token 验证逻辑
        return true
    }
    
    private fun getUserInfoFromToken(token: String): UserInfo {
        // 从 token 中解析用户信息
        return UserInfo("user123", "张三")
    }
}

重要注意事项 ⚠️

1. ResponseBody 和 ResponseEntity 的特殊情况

WARNING

对于使用 @ResponseBody 或返回 ResponseEntity 的控制器方法,响应在 postHandle 调用之前就已经写入并提交了。这意味着在 postHandle 中无法修改响应内容。

如果需要修改这类响应,应该使用 ResponseBodyAdvice

kotlin
@ControllerAdvice
class CustomResponseBodyAdvice : ResponseBodyAdvice<Any> {
    
    override fun supports(
        returnType: MethodParameter,
        converterType: Class<out HttpMessageConverter<*>>
    ): Boolean = true
    
    override fun beforeBodyWrite(
        body: Any?,
        returnType: MethodParameter,
        selectedContentType: MediaType,
        selectedConverterType: Class<out HttpMessageConverter<*>>,
        request: ServerHttpRequest,
        response: ServerHttpResponse
    ): Any? {
        // 在响应体写入前修改响应
        response.headers.add("X-Custom-Header", "Custom Value") 
        return body
    }
}

2. 安全性考虑

CAUTION

拦截器不适合作为安全层,因为它们可能与注解控制器的路径匹配存在不一致。建议使用 Spring Security 或在 Servlet 过滤器链中尽早应用安全措施。

拦截器 vs 过滤器 vs AOP 🤔

特性拦截器 (Interceptor)过滤器 (Filter)AOP
执行时机Spring MVC 处理过程中Servlet 容器级别方法调用前后
访问 Spring 上下文
访问 Handler 信息
粒度请求级别请求级别方法级别
配置方式Spring 配置web.xml 或注解Spring 配置

最佳实践建议 💡

性能优化建议

  1. 避免在拦截器中执行耗时操作:拦截器会影响所有匹配的请求
  2. 合理使用路径匹配:精确配置拦截路径,避免不必要的拦截
  3. 异常处理:在 afterCompletion 中进行资源清理,即使发生异常也能执行

设计原则

  • 单一职责:每个拦截器只负责一个特定功能
  • 链式设计:多个拦截器可以组合使用
  • 可配置性:通过配置控制拦截器的启用和路径匹配

总结 📝

Spring MVC 拦截器是一个强大的工具,它让我们能够:

  • ✅ 在不修改业务代码的情况下添加横切关注点
  • ✅ 实现统一的日志记录、权限验证、性能监控
  • ✅ 提供灵活的配置和组合方式
  • ✅ 与 Spring 生态系统完美集成

通过合理使用拦截器,我们可以构建更加模块化、可维护的 Web 应用程序。记住,拦截器是 Spring MVC 架构中的重要组成部分,掌握它将让你的 Spring Boot 应用更加强大和优雅! 🎉