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

30 篇文章 3 订阅

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);
    }
}

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();
        }
    }
}

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

@RestController
public class MyController {

    @PostMapping("/hello")
    public User hello(@RequestBody User user){
        return user;
    }
}

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
    }
}

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

  • 我们的过滤器都是在一条调用链上按顺序执行的。调用顺序依赖于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;
    }
}

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

@Override
public FilterMap[] findFilterMaps() {
    return filterMaps.asArray();
}

到这里为止,我们发现过滤器的调用顺序实际上由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);
}

我们打个断点,调试一下看下调用栈:
在这里插入图片描述
这里看到了一个熟悉的身影: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);
		}
	}
}

再调试(重启项目,这段代码是容器启动的时候执行的)看下:
在这里插入图片描述
我们确信,过滤器的调用顺序由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);
}

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

也就是说,过滤器的执行顺序依赖于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);
		}
		// ...
	}
}

我们关注第二个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());
}

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;
    }
}

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

再或者,你可以用另外一种方式,使用普通的@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. @WebFilter + @ServletComponentScan的这种过滤器定义方式,使用简单。但是过滤器链中的执行顺序不可控。
  2. 通过自定义FilterRegistrationBean的方式,虽然相对来说麻烦点,但是可以控制过滤器的一个执行顺序,功能使用上会更好。
  3. 如果想要更简单点,都希望用注解的形式完成这种功能的话。可以使用@Component来进行注解。(注意不要同时使用@WebFilter + @ServletComponentScan)。
  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值