• Spring常见问题解决 - @WebFilter 过滤器使用@Order控制执行顺序失效了?


    Spring常见问题解决 - @WebFilter 过滤器使用@Order控制执行顺序失效了?

    一. 案例复现

    首先我来啰嗦几句。本文是使用@WebFilter 注解来装配过滤器的,与此同时,在启动类上需要加上注解@ServletComponentScan。这样,容器启动的时候,才能将自定义的过滤器注入到容器中。

    当然,还有另外一种做法,你可以自定义FilterRegistrationBean类(这种方案正好是本文对于问题的一种解决方案)。两种方案都行。本文针对第一种方案来说。

    1.首先我们可以自定义一个时间过滤器,用来计算接口的执行时间的。同时希望他是第一个被执行的。

    @WebFilter
    // 数字越小,优先级越大
    @Order(1)
    public class TimeFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            System.out.println("先执行:接口执行时间: 2s");
            chain.doFilter(request, response);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.定义一个检查的过滤器,并模拟耗时需要1秒钟:

    @WebFilter
    @Order(2)
    public class CheckFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            try {
                Thread.sleep(1000);
                System.out.println("~~~~~~~~~~~~~后执行:检查通过~~~~~~~~~~~~~~~~~~");
                chain.doFilter(request, response);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.然后随便自定义一个Controller类,并访问下自定义的接口:

    @RestController
    public class MyController {
    
        @PostMapping("/hello")
        public User hello(@RequestBody User user){
            return user;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.访问接口后,控制台输出如下:
    在这里插入图片描述

    可见,程序运行的结果和我们预想的结果是相反的,也就是说我们定义的Order顺序并没有生效?

    二. 原理分析

    我们来看下过滤器当中的一个重要函数:chain.doFilter(request, response);

    public final class ApplicationFilterChain implements FilterChain {
    	@Override
        public void doFilter(ServletRequest request, ServletResponse response)
            throws IOException, ServletException {
    		// ....
            internalDoFilter(request,response);
        }
    	↓↓↓↓↓↓
    	private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
    		// n 为过滤器的总数,每执行完一个过滤器,pos变量就会+1,然后取下一个过滤器继续执行,即链式调用
            if (pos < n) {
            	// 这里就是我们要执行的过滤器,顺序也由它做决定
                ApplicationFilterConfig filterConfig = filters[pos++];
                try {
                	// 获取y
                    Filter filter = filterConfig.getFilter();
                    // ... 省略了 如果Spring开启了Security功能下的逻辑
                    // this指的是ApplicationFilterChain实例,即调用方需要在过滤器中显式地调用doFilter函数,才能完成整个链路的调用。递归调用了
                    filter.doFilter(request, response, this);
                } 
                // catch / finally
                return;
            }
    
            // 代码运行到这里的时候,过滤器链已经全部过了一遍了。即pos>=n
            try {
                // ... 省略了 如果Spring开启了Security功能下的逻辑
                // 过滤器走完,那么就需要走真正地逻辑处理了。
                servlet.service(request, response);
            } 
            // catch / finally
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    我们来看下调试流程:
    在这里插入图片描述
    从上面我们可以得出这么几个结论:

    • 我们的过滤器都是在一条调用链上按顺序执行的。调用顺序依赖于filters对象中的过滤器顺序。
    • filters属性就是用来保存调用链中的所有过滤器。
    • 只有遍历完调用链,才会去走真正地业务逻辑,即servlet.service(request, response);

    那么可想而知,问题出在filters属性的赋值上。

    2.1 过滤器链中的对象来自哪里?

    Spring中有这么一个类ApplicationFilterFactory,他就是创建调用链的一个类。具体函数在于:

    public final class ApplicationFilterFactory {
    	public static ApplicationFilterChain createFilterChain(ServletRequest request,
                Wrapper wrapper, Servlet servlet) {
    		// ... 
            // 从应用上下文中获取相关的过滤器
            StandardContext context = (StandardContext) wrapper.getParent();
            FilterMap filterMaps[] = context.findFilterMaps();
            // ... 
            return filterChain;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    看图:
    在这里插入图片描述
    我们来看下StandardContext.findFilterMaps()

    @Override
    public FilterMap[] findFilterMaps() {
        return filterMaps.asArray();
    }
    
    • 1
    • 2
    • 3
    • 4

    到这里为止,我们发现过滤器的调用顺序实际上由StandardContext.filterMaps属性来做决定。 那么看下这个filterMaps属性被谁引用赋值了?
    在这里插入图片描述
    从名字上来看,我们应该先去查看addBefore()这个函数,那么定位到这段代码:

    @Override
    public void addFilterMapBefore(FilterMap filterMap) {
        validateFilterMap(filterMap);
        // Add this filter mapping to our registered set
        filterMaps.addBefore(filterMap);
        fireContainerEvent("addFilterMap", filterMap);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    我们打个断点,调试一下看下调用栈:
    在这里插入图片描述
    这里看到了一个熟悉的身影:ServletWebServerApplicationContext,我在@WebFilter注解装配的过滤器无法被@Autowired自动注入?这篇文章里面讲到过,被@WebFilter修饰的过滤器,实际上其类型是一种InnerBean,是在容器加载FilterRegistrationBean的时候被自动装配的。其中,入口函数也包括了ServletWebServerApplicationContext,我们来看下相关的函数:

    public class ServletWebServerApplicationContext extends GenericWebApplicationContext
    		implements ConfigurableWebServerApplicationContext {
    	private void selfInitialize(ServletContext servletContext) throws ServletException {
    		prepareWebApplicationContext(servletContext);
    		registerApplicationScope(servletContext);
    		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
    		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
    			beans.onStartup(servletContext);
    		}
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    再调试(重启项目,这段代码是容器启动的时候执行的)看下:
    在这里插入图片描述
    我们确信,过滤器的调用顺序由getServletContextInitializerBeans()来做决定,那么我们继续深挖:

    protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
    	return new ServletContextInitializerBeans(getBeanFactory());
    }
    ↓↓↓↓↓看下ServletContextInitializerBeans的构造函数
    @SafeVarargs
    public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
    		Class<? extends ServletContextInitializer>... initializerTypes) {
    	this.initializers = new LinkedMultiValueMap<>();
    	this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
    			: Collections.singletonList(ServletContextInitializer.class);
    	addServletContextInitializerBeans(beanFactory);
    	addAdaptableBeans(beanFactory);
    	List<ServletContextInitializer> sortedInitializers = this.initializers.values().stream()
    			.flatMap((value) -> value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE))
    			.collect(Collectors.toList());
    	this.sortedList = Collections.unmodifiableList(sortedInitializers);
    	logMappings(this.initializers);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    调试结果如下:
    在这里插入图片描述

    也就是说,过滤器的执行顺序依赖于ServletContextInitializerBeans.sortedList。而这个集合的对象又来自于initializers属性:
    在这里插入图片描述
    其中,对于sort排序的规则,大致如下:

    1. 先看排序对象是否实现了Ordered接口,若有,则调用getOrder()获取值。
    2. 否则调用findOrder()获取@Order注解属性值。

    那么我们再看下这些排序对象从何而来,查看initializers属性的引用之后我们发现,构造函数中调用的addServletContextInitializerBeans这个函数,会往initializers属性中添加对象,。

    public class ServletContextInitializerBeans extends AbstractCollection<ServletContextInitializer> {
    	@SafeVarargs
    	public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
    			Class<? extends ServletContextInitializer>... initializerTypes) {
    		// 针对@WebFilter注解的Bean
    		addServletContextInitializerBeans(beanFactory);
    	}
    	↓↓↓↓↓↓
    	private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
    		for (Class<? extends ServletContextInitializer> initializerType : this.initializerTypes) {
    			for (Entry<String, ? extends ServletContextInitializer> initializerBean : getOrderedBeansOfType(beanFactory,
    					initializerType)) {
    				addServletContextInitializerBean(initializerBean.getKey(), initializerBean.getValue(), beanFactory);
    			}
    		}
    	}
    	↓↓↓↓↓↓
    	private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer,
    			ListableBeanFactory beanFactory) {
    		if (initializer instanceof ServletRegistrationBean) {
    			Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet();
    			addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
    		}
    		else if (initializer instanceof FilterRegistrationBean) {
    			Filter source = ((FilterRegistrationBean<?>) initializer).getFilter();
    			addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
    		}
    		// ...
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    我们关注第二个if分支,这里加入的是FilterRegistrationBean类型的Bean。他是
    ServletContextInitializer的一个子类:
    在这里插入图片描述

    这里面做个小总结就是:

    1. 过滤器链中的过滤器来源于ServletContextInitializerBeans.initializers

    2. initializers属性的赋值又依赖于FilterRegistrationBean这个对象的装配。、

    3. @WebFIlter修饰过的类通过加载FilterRegistrationBean的时候被注入。

    4. FilterRegistrationBeanWebFilterHandler.doHandle()中构建。

    但是问题在于,包装FilterRegistrationBean类的BeanDefinition的时候,并没有对Order进行相关的指定。同时我们再看下上面的类关系图,我们发现FilterRegistrationBean的父类RegistrationBean实现了Ordered接口。但是上述代码中却没有对这个值进行填充赋值。 看代码:

    @Override
    public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
    		BeanDefinitionRegistry registry) {
    	BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterRegistrationBean.class);
    	builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
    	builder.addPropertyValue("dispatcherTypes", extractDispatcherTypes(attributes));
    	builder.addPropertyValue("filter", beanDefinition);
    	builder.addPropertyValue("initParameters", extractInitParameters(attributes));
    	String name = determineName(attributes, beanDefinition);
    	builder.addPropertyValue("name", name);
    	builder.addPropertyValue("servletNames", attributes.get("servletNames"));
    	builder.addPropertyValue("urlPatterns", extractUrlPatterns(attributes));
    	registry.registerBeanDefinition(name, builder.getBeanDefinition());
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    2.2 总结

    根据上述的的描述,我们知道,过滤器链中的顺序的加载依赖:

    第一步骤:项目启动,会执行ServletWebServerApplicationContext.selfInitialize()函数。此时会加载FilterRegistrationBean类型的Bean


    第二步骤:此时会触发ServletContextInitializerBeans这个类的构造函数执行。

    1. 构造函数中首先通过addServletContextInitializerBeans()函数,按照顺序处理ServletContextInitializer类型的Bean
    2. FilterRegistrationBeanServletContextInitializer的一个子类。因此第二步中加载的实际上是FilterRegistrationBean
    3. FilterRegistrationBean最终又实现了Ordered接口。相关属性存在于其父类RegistrationBean中。即order属性。

    第三步骤:ServletContextInitializerBeans构造函数将会对第二步中收集到的ServletContextInitializerBean进行排序。排序规则:

    1. 先看排序对象是否实现了Ordered接口,若有,则调用getOrder()获取值。
    2. 否则调用findOrder()获取@Order注解属性值。

    结论:

    1. 我们通过@WebFilter注解来装配的自定义过滤器中,自己加上的@Order注解是没有用的。因此真正过滤器的执行顺序依赖于FilterRegistrationBeanorder值。
    2. 第一步中对于FilterRegistrationBean的封装的时候,没有做对于order属性的装配。
    3. 因此在后续第三步对过滤器链中的过滤器进行排序的时候,无法达到我们的预期效果。

    三. 问题解决

    既然在使用@WebFilter的时候,无法让@Order注解生效。那么我们不使用这种方式来装配过滤器。我们通过装配FilterRegistrationBean对象来替代。

    1.首先,我们把CheckFilterTimeFilter这两个过滤器给注释掉。
    2.然后我们在配置类中添加以下Bean

    @Configuration
    public class MyConfig {
    	@Bean
        public FilterRegistrationBean checkFilter() {
            FilterRegistrationBean bean = new FilterRegistrationBean();
            bean.setFilter(new CheckFilter());
            bean.addUrlPatterns("/*");
            bean.setOrder(2);
            return bean;
        }
    
        @Bean
        public FilterRegistrationBean timeFilter() {
            FilterRegistrationBean bean = new FilterRegistrationBean();
            bean.setFilter(new TimeFilter());
            bean.addUrlPatterns("/*");
            bean.setOrder(1);
            return bean;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    结果如下:
    在这里插入图片描述

    再或者,你可以用另外一种方式,使用普通的@Component注解来装配过滤器,也可以出现一样的效果。

    @Order(value = 1)
    @Component
    public class TimeFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            System.out.println("先执行:接口执行时间: 2s");
            chain.doFilter(request, response);
        }
    }
    
    @Component
    @Order(2)
    public class CheckFilter implements Filter {
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            try {
                Thread.sleep(1000);
                System.out.println("~~~~~~~~~~~~~后执行:检查通过~~~~~~~~~~~~~~~~~~");
                chain.doFilter(request, response);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    总结下来就是:

    1. @WebFilter + @ServletComponentScan的这种过滤器定义方式,使用简单。但是过滤器链中的执行顺序不可控。
    2. 通过自定义FilterRegistrationBean的方式,虽然相对来说麻烦点,但是可以控制过滤器的一个执行顺序,功能使用上会更好。
    3. 如果想要更简单点,都希望用注解的形式完成这种功能的话。可以使用@Component来进行注解。(注意不要同时使用@WebFilter + @ServletComponentScan)。
  • 相关阅读:
    Web自动化框架中验证码识别处理全攻略,让测试更得心应手!
    LLMs Python解释器程序辅助语言模型(PAL)Program-aided language models (PAL)
    【MATLAB的方程组求解】
    python-turtle库
    【一起学Rust | 进阶篇 | RMQTT库】RMQTT消息服务器——安装与集群配置
    Java计算机毕业设计电费管理系统源码+系统+数据库+lw文档
    【Kingbase FlySync】界面化管控平台:2.配置数据库同步之KES>KES
    PXE高效批量网络装机(补充) 实验部分
    mindspore课程打卡第二天
    Elasticsearch之拼音搜索(十五)
  • 原文地址:https://blog.csdn.net/Zong_0915/article/details/126747302