概述

本教程将重点介绍如何理解并正确使用Spring MVC的HandlerInterceptor

2. Spring MVC处理器

为了理解拦截器的工作原理,让我们先回顾一下HandlerMapping的作用。

HandlerMapping的主要任务是将处理器方法映射到URL,这样DispatcherServlet在处理请求时就能调用它。

实际上,DispatcherServlet通过HandlerAdapter来实际调用方法。

简而言之,拦截器拦截请求并对其进行处理,有助于避免重复的处理器代码,如日志记录和授权检查。

现在我们了解了整体背景,接下来我们将学习如何使用HandlerInterceptor执行预处理和后处理操作。

3. Maven依赖项

要使用拦截器,我们需要在pom.xml中添加spring-web依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-web</artifactId>
    <version>5.3.13</version>
</dependency>

4. Spring处理器拦截器

简单来说,Spring拦截器是一个类,它要么继承HandlerInterceptorAdapter类,要么实现HandlerInterceptor接口。

HandlerInterceptor包含三个主要方法:

  • prehandle() - 在实际处理器执行之前被调用
  • postHandle() - 在处理器执行后调用
  • afterCompletion() - 在整个请求完成后以及生成视图之后调用

这三个方法提供了进行各种预处理和后处理的灵活性。

在继续之前,一个小提示:如果您想跳过理论直接看例子,请直接跳到第5节。

下面是一个简单的preHandle()实现:

@Override
public boolean preHandle(
  HttpServletRequest request,
  HttpServletResponse response, 
  Object handler) throws Exception {
    // your code
    return true;
}

请注意,该方法返回一个boolean值。它告诉Spring是否继续处理请求(true)或停止(false)。

接下来是postHandle()的实现:

@Override
public void postHandle(
  HttpServletRequest request, 
  HttpServletResponse response,
  Object handler, 
  ModelAndView modelAndView) throws Exception {
    // your code
}

拦截器在处理完请求后立即调用此方法,但在生成视图之前。

例如,我们可以使用这个方法将已登录用户的头像添加到模型中。

最后,我们需要实现的方法是afterCompletion()

@Override
public void afterCompletion(
  HttpServletRequest request, 
  HttpServletResponse response,
  Object handler, Exception ex) {
    // your code
}

这个方法允许我们在请求处理完成后执行自定义逻辑。

此外,值得一提的是,我们可以注册多个自定义拦截器。要做到这一点,可以使用DefaultAnnotationHandlerMapping

5. 自定义日志拦截器示例

在这个例子中,我们将专注于在web应用程序中进行日志记录。

首先,我们的类需要实现HandlerInterceptor

public class LoggerInterceptor implements HandlerInterceptor {
    ...
}

我们还需要在拦截器中启用日志:

private static Logger log = LoggerFactory.getLogger(LoggerInterceptor.class);

这使得Log4J能够显示日志,并指示当前哪个类正在向指定输出发送信息。

接下来,让我们关注自定义拦截器的实现。

5.1. preHandle()方法

顾名思义,拦截器在处理请求之前调用preHandle()

默认情况下,此方法返回true以将请求传递给处理器方法。但是,我们可以通过返回false告诉Spring停止执行。

我们可以利用这个钩子来记录有关请求参数的信息,例如请求来自何处。

在我们的示例中,我们使用简单的Log4J日志记录这些信息:

@Override
public boolean preHandle(
  HttpServletRequest request,
  HttpServletResponse response, 
  Object handler) throws Exception {
    
    log.info("[preHandle][" + request + "]" + "[" + request.getMethod()
      + "]" + request.getRequestURI() + getParameters(request));
    
    return true;
}

我们可以看到,我们正在记录一些关于请求的基本信息。

如果在这里遇到密码,我们需要确保不将其记录下来。一个简单的解决方案是替换密码和其他敏感类型的数据为星号。

以下是如何快速实现这一点的示例:

private String getParameters(HttpServletRequest request) {
    StringBuffer posted = new StringBuffer();
    Enumeration<?> e = request.getParameterNames();
    if (e != null) {
        posted.append("?");
    }
    while (e.hasMoreElements()) {
        if (posted.length() > 1) {
            posted.append("&");
        }
        String curr = (String) e.nextElement();
        posted.append(curr + "=");
        if (curr.contains("password") 
          || curr.contains("pass")
          || curr.contains("pwd")) {
            posted.append("*****");
        } else {
            posted.append(request.getParameter(curr));
        }
    }
    String ip = request.getHeader("X-FORWARDED-FOR");
    String ipAddr = (ip == null) ? getRemoteAddr(request) : ip;
    if (ipAddr!=null && !ipAddr.equals("")) {
        posted.append("&_psip=" + ipAddr); 
    }
    return posted.toString();
}

最后,我们的目标是获取HTTP请求的源IP地址。

这是一个简单的实现:

private String getRemoteAddr(HttpServletRequest request) {
    String ipFromHeader = request.getHeader("X-FORWARDED-FOR");
    if (ipFromHeader != null && ipFromHeader.length() > 0) {
        log.debug("ip from proxy - X-FORWARDED-FOR : " + ipFromHeader);
        return ipFromHeader;
    }
    return request.getRemoteAddr();
}

5.2. postHandle()方法

拦截器在处理器执行后但DispatcherServlet渲染视图之前调用此方法。

我们可以使用它向ModelAndView添加额外属性,另一个用例是计算请求的处理时间。

在我们的例子中,我们将在DispatcherServlet渲染视图前简单地记录请求:

@Override
public void postHandle(
  HttpServletRequest request, 
  HttpServletResponse response,
  Object handler, 
  ModelAndView modelAndView) throws Exception {
    
    log.info("[postHandle][" + request + "]");
}

5.3. afterCompletion()方法

我们可以使用此方法在视图渲染后获取请求和响应数据:

@Override
public void afterCompletion(
  HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 
  throws Exception {
    if (ex != null){
        ex.printStackTrace();
    }
    log.info("[afterCompletion][" + request + "][exception: " + ex + "]");
}

6. 配置

现在,我们将所有部分组合在一起,添加自定义拦截器。

为此,我们需要覆盖addInterceptors()方法:

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(new LoggerInterceptor());
}

我们也可以通过编辑XML Spring配置文件来实现相同配置:

<mvc:interceptors>
    <bean id="loggerInterceptor" class="com.baeldung.web.interceptor.LoggerInterceptor"/>
</mvc:interceptors>

有了此配置,拦截器将启用,并且应用程序中的所有请求都将得到适当的日志记录。

请注意,如果有多个Spring拦截器配置,preHandle()方法将按照配置顺序执行,而postHandle()afterCompletion()方法则按相反的顺序调用。

请记住,如果我们使用Spring Boot而不是纯Spring,不需要在配置类上注解@EnableWebMvc

7. 总结

本文简要介绍了使用Spring MVC处理器拦截器拦截HTTP请求的方法。

所有示例和配置可在GitHub上找到。