Filter 和 Interceptor

Filter

过滤器的配置比较简单,直接实现 javax.servlet.Filter 接口即可,也可以通过@WebFilter注解实现对特定URL拦截,看到Filter 接口中定义了三个方法。

  • init(FilterConfig config) :用于完成Filter的初始化。
  • doFilter(ServletRequest request,ServletResponse response,FilterChain chain) :容器中的每一次请求都会调用该方法, FilterChain 用来调用下一个过滤器 Filter。
  • destroy(): 用于Filter销毁前,完成某些资源的回收。
@Component
public class MyFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

        System.out.println("Filter 前置");
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        System.out.println("Filter 处理中");
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

        System.out.println("Filter 后置");
    }
}

Interceptor(AOP)

拦截器它是链式调用,一个应用中可以同时存在多个拦截器Interceptor, 一个请求也可以触发多个拦截器 ,而每个拦截器的调用会依据它的声明顺序依次执行。

首先编写一个简单的拦截器处理类,请求的拦截是通过HandlerInterceptor 来实现,看到HandlerInterceptor 接口中也定义了三个方法。

  • preHandle() :这个方法将在请求处理之前进行调用。注意:如果该方法的返回值为false ,将视为当前请求结束,不仅自身的拦截器会失效,还会导致其他的拦截器也不再执行。
  • postHandle():只有在 preHandle 方法返回值为true 时才会执行。会在Controller 中的方法调用之后,DispatcherServlet 返回渲染视图之前被调用。 有意思的是:postHandle 方法被调用的顺序跟 preHandle 是相反的,先声明的拦截器 preHandle方法先执行,而postHandle方法反而会后执行。
  • afterCompletion():只有在 preHandle 方法返回值为true 时才会执行。在整个请求结束之后, DispatcherServlet 渲染了对应的视图之后执行。
@Component
public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        System.out.println("Interceptor 前置");
        return true;
    }

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

        System.out.println("Interceptor 处理中");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        System.out.println("Interceptor 后置");
    }
}

将自定义好的拦截器处理类进行注册,并通过addPathPatterns、excludePathPatterns等属性设置需要拦截或需要排除的 URL。

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
        registry.addInterceptor(new MyInterceptor1()).addPathPatterns("/**");
    }
}

Filter 和 Interceptor 的区别

实现原理不同

过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于Java的反射机制(动态代理)实现的。

这里重点说下过滤器!

在我们自定义的过滤器中都会实现一个 doFilter方法,这个方法有一个FilterChain 参数,而实际上它是一个回调接口。ApplicationFilterChain是它的实现类, 这个实现类内部也有一个 doFilter 方法就是回调方法。

public interface FilterChain {
    void doFilter(ServletRequest var1, ServletResponse var2) throws IOException, ServletException;
}

ApplicationFilterChain里面能拿到我们自定义的xxxFilter类,在其内部回调方法doFilter里调用各个自定义xxxFilter过滤器,并执行 doFilter 方法。

public final class ApplicationFilterChain implements FilterChain {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response) {
            ...//省略
            internalDoFilter(request,response);
    }

    private void internalDoFilter(ServletRequest request, ServletResponse response){
    if (pos < n) {
            //获取第pos个filter    
            ApplicationFilterConfig filterConfig = filters[pos++];        
            Filter filter = filterConfig.getFilter();
            ...
            filter.doFilter(request, response, this);
        }
    }

}

而每个xxxFilter 会先执行自身的 doFilter 过滤逻辑,最后在执行结束前会执行filterChain.doFilter(servletRequest, servletResponse),也就是回调ApplicationFilterChain的doFilter 方法,以此循环执行实现函数回调。

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {

        filterChain.doFilter(servletRequest, servletResponse);
    }

使用范围不同

过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。

而拦截器Interceptor 它是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。不仅能应用在web程序中,也可以用于Application、Swing等程序中。

触发时机不同

过滤器(Filter):可以拿到原始的http请求,但是拿不到你请求的控制器和请求控制器中的方法的信息。

拦截器(Interceptor):可以拿到你请求的控制器和方法,却拿不到请求方法的参数。

切片 (Aspect) : 可以拿到方法的参数,但是却拿不到http请求和响应的对象

过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。

拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。

拦截的请求范围不同

Filter 处理中
Interceptor 前置
Interceptor 处理中
Interceptor 后置
Filter 处理中

Interceptor作用

  1. 日志记录:记录请求信息的日志,以便进行信息监控、信息统计、计算 PV(Page View)等;
  2. 权限检查:如登录检测,进入处理器检测是否登录;
  3. 性能监控:通过拦截器在进入处理器之前记录开始时间,在处理完后记录结束时间,从而得到该请求的处理时间。(反向代理,如 Apache 也可以自动记录)
  4. 通用行为:读取 Cookie 得到用户信息并将用户对象放入请求,从而方便后续流程使用,还有如提取 Locale、Theme 信息等,只要是多个处理器都需要的即可使用拦截器实现。

性能监控

如记录一下请求的处理时间,得到一些慢请求(如处理时间超过500毫秒),从而进行性能改进,一般的反向代理服务器如 apache 都具有这个功能,但此处我们演示一下使用拦截器怎么实现。

实现分析

1、在进入处理器之前记录开始时间,即在拦截器的 preHandle 记录开始时间;

2、在结束请求处理之后记录结束时间,即在拦截器的 afterCompletion 记录结束实现,并用结束时间-开始时间得到这次请求的处理时间。

问题:

我们的拦截器是单例,因此不管用户请求多少次都只有一个拦截器实现,即线程不安全,那我们应该怎么记录时间呢?

解决方案是使用 ThreadLocal,它是线程绑定的变量,提供线程局部变量(一个线程一个 ThreadLocal,A线程的ThreadLocal 只能看到A线程的 ThreadLocal,不能看到B线程的 ThreadLocal)。

代码实现:

public class StopWatchHandlerInterceptor extends HandlerInterceptorAdapter {    
    private NamedThreadLocal<Long> startTimeThreadLocal = new NamedThreadLocal<>("StopWatch-StartTime");    
    private Logger logger = LoggerFactory.getLogger(StopWatchHandlerInterceptor.class);    

@Override                               public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {        
        long beginTime = System.currentTimeMillis();//1、开始时间        
                                                                                startTimeThreadLocal.set(beginTime);//线程绑定变量(该数据只有当前请求的线程可见)        
        return true;//继续流程    
                                                                                }    
@Override                                 public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {        
        long endTime = System.currentTimeMillis();//2、结束时间        
        long beginTime = startTimeThreadLocal.get();//得到线程绑定的局部变量(开始时间)        
        long consumeTime = endTime - beginTime;//3、消耗的时间        
        if(consumeTime > 500) {//此处认为处理时间超过500毫秒的请求为慢请求            
logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));        
        }        
        //测试的时候由于请求时间未超过500,所以启用该代码
        //   logger.info(String.format("%s consume %d millis", request.getRequestURI(), consumeTime));    
    }
}

NamedThreadLocal:Spring提供的一个命名的ThreadLocal实现。

在测试时需要把 stopWatchHandlerInterceptor 放在拦截器链的第一个,这样得到的时间才是比较准确的。

拦截器配置类:

@Configuration
    public class WebConfig implements WebMvcConfigurer {    
        @Override    
        public void addInterceptors(InterceptorRegistry registry) {        
        registry.addInterceptor(new StopWatchHandlerInterceptor());        
        registry.addInterceptor(new OldLoginInterceptor()).addPathPatterns("/admin/oldLogin");    
    }
}

   转载规则


《Filter 和 Interceptor》 锦泉 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
highbit和lowbit函数 highbit和lowbit函数
一般后者常用一些
2021-05-31
下一篇 
枚举enum 枚举enum
enum的values方法此方法可以将枚举类转变为一个枚举类型的数组,因为枚举中没有下标,我们没有办法通过下标来快速找到需要的枚举类,这时候,转变为数组之后,我们就可以通过数组的下标,来找到我们需要的枚举类。 testpublic enum
2021-05-24
  目录