百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

一口气讲清 过滤器 和 拦截器 6个区别,别再傻傻分不清了

haoteby 2025-01-17 11:48 9 浏览

周末有个小伙伴加我微信,向我请教了一个问题:老哥,「过滤器 (Filter) 和 拦截器 (Interceptor) 有啥区别啊?」 听到题目我的第一感觉就是:「简单」

毕竟这两种工具开发中用到的频率都相当高,应用起来也是比较简单的,可当我准备回复他的时候,竟然不知道从哪说起,支支吾吾了半天,场面炒鸡尴尬有木有,工作这么久一个基础问题答成这样,丢了大人了。

平时觉得简单的知识点,但通常都不会太关注细节,一旦被别人问起来,反倒说不出个所以然来。

归根结底,还是对这些知识了解的不够,一直停留在会用的阶段,以至于现在「一看就会一说就废」!这是典型基础不扎实的表现,哎·~,其实我也就是个虚胖!

知耻而后勇,下边结合实践,更直观的来感受一下两者到底有什么不同?

准备环境

我们在项目中同时配置 拦截器 和 过滤器。

1、过滤器 (Filter)

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

  • init() :该方法在容器启动初始化过滤器时被调用,它在 Filter 的整个生命周期只会被调用一次。「注意」:这个方法必须执行成功,否则过滤器会不起作用。
  • doFilter() :容器中的每一次请求都会调用该方法, 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 后置");
    }
}

2、拦截器 (Interceptor)

拦截器它是链式调用,一个应用中可以同时存在多个拦截器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("/**");
    }
}

我们不一样

过滤器 和 拦截器 均体现了AOP的编程思想,都可以实现诸如日志记录、登录鉴权等功能,但二者的不同点也是比较多的,接下来一一说明。

1、实现原理不同

过滤器和拦截器 底层实现方式大不相同,过滤器 是基于函数回调的,拦截器 则是基于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);
    }

2、使用范围不同

我们看到过滤器 实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter 的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。

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

3、触发时机不同

过滤器 和 拦截器的触发时机也不同,我们看下边这张图。

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

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

4、拦截的请求范围不同

在上边我们已经同时配置了过滤器和拦截器,再建一个Controller接收请求测试一下。

@Controller
@RequestMapping()
public class Test {

    @RequestMapping("/test1")
    @ResponseBody
    public String test1(String a) {
        System.out.println("我是controller");
        return null;
    }
}

项目启动过程中发现,过滤器的init()方法,随着容器的启动进行了初始化。

此时浏览器发送请求,F12 看到居然有两个请求,一个是我们自定义的 Controller 请求,另一个是访问静态图标资源的请求。

看到控制台的打印日志如下:

执行顺序 :Filter 处理中 -> Interceptor 前置 -> 我是controller -> Interceptor 处理中 -> Interceptor 处理后

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

过滤器Filter执行了两次,拦截器Interceptor只执行了一次。这是因为过滤器几乎可以对所有进入容器的请求起作用,而拦截器只会对Controller中请求或访问static目录下的资源请求起作用。

5、注入Bean情况不同

在实际的业务场景中,应用到过滤器或拦截器,为处理业务逻辑难免会引入一些service服务。

下边我们分别在过滤器和拦截器中都注入service,看看有什么不同?

@Component
public class TestServiceImpl implements TestService {

    @Override
    public void a() {
        System.out.println("我是方法A");
    }
}

过滤器中注入service,发起请求测试一下 ,日志正常打印出“我是方法A”。

@Autowired
    private TestService testService;

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

        System.out.println("Filter 处理中");
        testService.a();
        filterChain.doFilter(servletRequest, servletResponse);
    }
Filter 处理中
我是方法A
Interceptor 前置
我是controller
Interceptor 处理中
Interceptor 后置

在拦截器中注入service,发起请求测试一下 ,竟然TM的报错了,debug跟一下发现注入的service怎么是Null啊?

这是因为加载顺序导致的问题,拦截器加载的时间点在springcontext之前,而Bean又是由spring进行管理。

拦截器:老子今天要进洞房; Spring:兄弟别闹,你媳妇我还没生出来呢!

解决方案也很简单,我们在注册拦截器之前,先将Interceptor 手动进行注入。「注意」:在registry.addInterceptor()注册的是getMyInterceptor() 实例。

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {

    @Bean
    public MyInterceptor getMyInterceptor(){
        System.out.println("注入了MyInterceptor");
        return new MyInterceptor();
    }
    
    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(getMyInterceptor()).addPathPatterns("/**");
    }
}

6、控制执行顺序不同

实际开发过程中,会出现多个过滤器或拦截器同时存在的情况,不过,有时我们希望某个过滤器或拦截器能优先执行,就涉及到它们的执行顺序。

过滤器用@Order注解控制执行顺序,通过@Order控制过滤器的级别,值越小级别越高越先执行。

@Order(Ordered.HIGHEST_PRECEDENCE)
@Component
public class MyFilter2 implements Filter {

拦截器默认的执行顺序,就是它的注册顺序,也可以通过Order手动设置控制,值越小越先执行。

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

看到输出结果发现,先声明的拦截器 preHandle() 方法先执行,而postHandle()方法反而会后执行。

postHandle() 方法被调用的顺序跟 preHandle() 居然是相反的!如果实际开发中严格要求执行顺序,那就需要特别注意这一点。

Interceptor1 前置
Interceptor2 前置
Interceptor 前置
我是controller
Interceptor 处理中
Interceptor2 处理中
Interceptor1 处理中
Interceptor 后置
Interceptor2 处理后
Interceptor1 处理后

「那为什么会这样呢?」 得到答案就只能看源码了,我们要知道controller 中所有的请求都要经过核心组件DispatcherServlet路由,都会执行它的 doDispatch() 方法,而拦截器postHandle()、preHandle()方法便是在其中调用的。

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    
        try {
         ...........
            try {
           
                // 获取可以执行当前Handler的适配器
                HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

                // Process last-modified header, if supported by the handler.
                String method = request.getMethod();
                boolean isGet = "GET".equals(method);
                if (isGet || "HEAD".equals(method)) {
                    long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                    if (logger.isDebugEnabled()) {
                        logger.debug("Last-Modified value for [" + getRequestUri(request) + "] is: " + lastModified);
                    }
                    if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                        return;
                    }
                }
                // 注意: 执行Interceptor中PreHandle()方法
                if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                    return;
                }

                // 注意:执行Handle【包括我们的业务逻辑,当抛出异常时会被Try、catch到】
                mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

                if (asyncManager.isConcurrentHandlingStarted()) {
                    return;
                }
                applyDefaultViewName(processedRequest, mv);

                // 注意:执行Interceptor中PostHandle 方法【抛出异常时无法执行】
                mappedHandler.applyPostHandle(processedRequest, response, mv);
            }
        }
        ...........
    }

看看两个方法applyPreHandle()、applyPostHandle()具体是如何被调用的,就明白为什么postHandle()、preHandle() 执行顺序是相反的了。

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if(!ObjectUtils.isEmpty(interceptors)) {
            for(int i = 0; i < interceptors.length; this.interceptorIndex = i++) {
                HandlerInterceptor interceptor = interceptors[i];
                if(!interceptor.preHandle(request, response, this.handler)) {
                    this.triggerAfterCompletion(request, response, (Exception)null);
                    return false;
                }
            }
        }

        return true;
    }
void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) throws Exception {
        HandlerInterceptor[] interceptors = this.getInterceptors();
        if(!ObjectUtils.isEmpty(interceptors)) {
            for(int i = interceptors.length - 1; i >= 0; --i) {
                HandlerInterceptor interceptor = interceptors[i];
                interceptor.postHandle(request, response, this.handler, mv);
            }
        }
    }

发现两个方法正在调用拦截器数组 HandlerInterceptor[] 时,循环的顺序竟然是相反的。。。,导致postHandle()、preHandle() 方法执行的顺序相反。

总结

我相信大部分人都能熟练使用滤器和拦截器,但两者的差别还是需要多了解下,不然开发中使用不当,时不时就会出现奇奇怪怪的问题,以上内容比较简单,新手学习老鸟复习,有遗漏的地方还望大家积极补充,如有理解错误之处,还望不吝赐教。


相关推荐

一日一技:用Python程序将十进制转换为二进制

用Python程序将十进制转换为二进制通过将数字连续除以2并以相反顺序打印其余部分,将十进制数转换为二进制。在下面的程序中,我们将学习使用递归函数将十进制数转换为二进制数,代码如下:...

十进制转化成二进制你会吗?#数学思维

六年级奥赛起跑线:抽屉原理揭秘。同学们好,我是你们的奥耀老师。今天一起来学习奥赛起跑线第三讲二进制计数法。例一:把十进制五十三化成二进制数是多少?首先十进制就是满十进一,二进制就是满二进一。二进制每个...

二进制、十进制、八进制和十六进制,它们之间是如何转换的?

在学习进制时总会遇到多种进制转换的时候,学会它们之间的转换方法也是必须的,这里分享一下几种进制之间转换的方法,也分享两个好用的转换工具,使用它们能够大幅度的提升你的办公和学习效率,感兴趣的小伙伴记得点...

c语言-2进制转10进制_c语言 二进制转十进制

#include<stdio.h>intmain(){charch;inta=0;...

二进制、八进制、十进制和十六进制数制转换

一、数制1、什么是数制数制是计数进位的简称。也就是由低位向高位进位计数的方法。2、常用数制计算机中常用的数制有二进制、八进制、十进制和十六进制。...

二进制、十进制、八进制、十六进制间的相互转换函数

二进制、十进制、八进制、十六进制间的相互转换函数1、输入任意一个十进制的整数,将其分别转换为二进制、八进制、十六进制。2、程序代码如下:#include<iostream>usingna...

二进制、八进制、十进制和十六进制等常用数制及其相互转换

从大学开始系统的接触计算机专业,到现在已经过去十几年了,今天整理一下基础的进制转换,希望给还在上高中的表妹一个入门的引导,早日熟悉这个行业。一、二进制、八进制、十进制和十六进制是如何定义的?二进制是B...

二进制如何转换成十进制?_二进制如何转换成十进制例子图解

随着社会的发展,电器维修由继电器时代逐渐被PLC,变频器,触摸屏等工控时代所替代,特别是plc编程,其数据逻辑往往涉及到数制二进制,那么二进制到底是什么呢?它和十进制又有什么区别和联系呢?下面和朋友们...

二进制与十进制的相互转换_二进制和十进制之间转换

很多同学在刚开始接触计算机语言的时候,都会了解计算机的世界里面大多都是二进制来表达现实世界的任何事物的。当然现实世界的事务有很多很多,就拿最简单的数字,我们经常看到的数字大多都是十进制的形式,例如:我...

十进制如何转换为二进制,二进制如何转换为十进制

用十进制除以2,除的断的,商用0表示;除不断的,商用1表示余0时结束假如十进制用X表示,用十进制除以2,即x/2除以2后为整数的(除的断的),商用0表示;除以2除不断的,商用1表示除完后的商0或1...

十进制数如何转换为二进制数_十进制数如何转换为二进制数举例说明

我们经常听到十进制数和二进制数,电脑中也经常使用二进制数来进行计算,但是很多人却不清楚十进制数和二进制数是怎样进行转换的,下面就来看看,十进制数转换为二进制数的方法。正整数转二进制...

二进制转化为十进制,你会做吗?一起来试试吧

今天孩子问把二进制表示的110101改写成十进制数怎么做呀?,“二进制”简单来说就是“满二进一”,只用0和1共两个数字表示,同理我们平常接触到的“十进制”是“满十进一”,只用0-9共十个数字表示。如果...

Mac终于能正常打游戏了!苹果正逐渐淘汰Rosetta转译

Mac玩家苦转译久矣!WWDC2025苹果正式宣判Rosetta死刑,原生游戏时代终于杀到。Metal4光追和AI插帧技术直接掀桌,连Steam都连夜扛着ARM架构投诚了。看到《赛博朋克2077》...

怎么把视频的声音提出来转为音频?音频提取,11款工具实测搞定

想把视频里的声音单独保存为音频文件(MP3/AAC/WAV/FLAC)用于配音、播客、听课或二次剪辑?本文挑出10款常用工具,给出实测可复现的操作步骤、优缺点和场景推荐。1)转换猫mp3转换器(操作门...

6个mp4格式转换器测评:转换速度与质量并存!

MP4视频格式具有兼容性强、视频画质高清、文件体积较小、支持多种编码等特点,适用于网络媒体传播。如果大家想要将非MP4格式的视频转换成MP4的视频格式的话,可以使用MP4格式转换器更换格式。本文分别从...