关于SpringBoot实现Servlet的过滤器

当我们百度搜索“SpringBoot 过滤器 排序”时,有很多文章会讲如何在SpringBoot项目中配置过滤器。大致会分为两种方案:

  • 使用@WebFilter+@Order并配置@ServletComponentScan
  • 使用代码配置的方式编写FilterRegistrationBean,在其中调用setOrder方法来配置顺序。

实际使用时就会发现使用@WebFilter+@Order的方式虽然可以使过滤器生效,但是却无法使顺序生效。而百度搜索的文章基本上都是抄袭复制的,导致错误的内容广泛传播。
所以建议在想要学习框架的某种功能时,第一优先级应该去查询官方文档, SpringBoot的官方文档 就有关于过滤器顺序的说明。
在文档中说明了可以使用@Order或者实现Ordered接口,以及使用FilterRegistrationBeansetOrder方法。那么问题就来了,这官方文档不是说了可以使用@Order注解嘛?难道官方文档出错啦?带着疑问一探究竟

第一问:SpringBoot是如何进行过滤器注册的?

首先我们知道,Servlet标准,提供了javax.servlet.Filter接口。在以前的web.xml时候,通过实现接口并将过滤器配置在web.xml中,即可使过滤器生效。
那么在SpringBoot中,首先注意到这个FilterRegistrationBean,通过查看此类的源代码,可看到注册过滤器的代码:

@Override
protected Dynamic addRegistration(String description, ServletContext servletContext) {
	Filter filter = getFilter();
	return servletContext.addFilter(getOrDeduceName(filter), filter);
}

  看上去很简单,将我们配置的过滤器实例,直接添加到servletContext中。接着追溯此方法的调用链路,发现是FilterRegistrationBean的父类RegistrationBean中的onStartup方法,而此方法是由接口ServletContextInitializer定义。
  查看接口ServletContextInitializer的备注说明,知道了它是Spring专门用来初始化配置Servlet的一个初始化器。我们还需要注意到RegistrationBean也实现了接口Ordered,这就是FilterRegistrationBean可以用来配置过滤器顺序的原因。但是为什么官方文档说可以自己使用@Order或者实现Ordered接口呢?这个问题的答案需要继续探寻。
  那么我们就可以进行猜测,在某个阶段,由Spring容器触发了ServletContextInitializer的所有实现类的OnStartup方法, 进而就进行了过滤器的注册。

第二问:为什么文档说可以使用@Order,但是却不生效?

  是不是文档写错了呢?在质疑文档的正确性之前,应该首先想一想,是不是使用的不正确呢?
  我们使用的是@WebFilter+@Order的组合,结果就是顺序根本不起效果。那么在Spring框架下,什么时候见过有使用@Order注解的时候呢?印象就是在配合定义其他SpringBean的时候,那么就试试将@WebFilter改为@Component
  Bingo,@Component+@Order确实可以使过滤器生效并且可以配置顺序了(可以试一试将@Order改为实现Ordered接口,也是生效的)。所以官方文档上说的使用可以使用@Order或者实现Ordered接口就是这种了,但是这样好像就不能配置过滤器参数了!

第三问:那么@WebFilter是咋回事?

  查看该注解源码,可看到这个注解是定义在javax.servlet.annotation中,看包名猜测是由Servlet规范中定义,并且Servlet容易能够识别。那么在Spring框架下,该注解是如何工作的呢?
  在使用注解时,都需要配合@ServletComponentScan注解配置,才能使@WebFilter生效,那么查看@ServletComponentScan源码,可以跟踪到一个WebFilterHandler类中看到如下处理代码:

	@Override
	public void doHandle(Map<String, Object> attributes,
			ScannedGenericBeanDefinition 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());
	}

这里Spring最终也是将@WebFilter注解的过滤器构造为一个FilterRegistrationBean对象。但是,在构造BeanDefinition时,并没有注入order字段值,因此可以知道在@WebFilter注解上使用@Order或者实现Ordered接口是没用的

第四问:那@Component+@Order又是怎么注册过滤器的?

那对于这种不知道从何开头的问题,可以使用代码DEBUG的方式,追踪其调用链路。

简单来说就是写一个无参构造函数,在里面随便写一行代码,然后打个断点,这样就知道这个过滤器是啥时候被Spring实例化的了。

通过断点的方式,其实就可以很方便的看到调用链路,然后查看源代码即可明白原委。对于这个问题的答案,简单来说,就是这样的流程,容器为Tomcat:
--> Spring容器启动,将所有(Servlet的)Filter、Servlet、Listener构造为RegistrationBean
--> 在OnRefresh中new TomcatWebServer
--> 在创建Tomcat容器的过程中new TomcatStarter
--> 实例化TomcatStarter时,Spring会将所有Filter、Servlet、Listener的Bean注入
--> 然后在Tomcat启动完成之后,触发TomcatStarterOnStartup方法
--> 其中触发ServletContextInitializerOnStartup方法
--> 回到问题一中注册过滤器的流程

总结

SpringBoot配置过滤器并且支持排序的方式有两种:

  • 在过滤器上使用@Component+@Order即可
    但是这种无法配置过滤器参数
  • 代码配置的方式定义FilterRegistrationBean
    这种就很灵活,可以实现过滤器所有配置

另外,在学习源码的时候,应该是带着问题的学习的。有时并不需要太过注意源码细节,应该理解其抽象含义,等了解到了整体结构之后再一层层的往里拨。

posted @ 2021-11-02 20:47  Bencakes  阅读(470)  评论(0编辑  收藏  举报