关于SpringBoot实现Servlet的过滤器
当我们百度搜索“SpringBoot 过滤器 排序”时,有很多文章会讲如何在SpringBoot项目中配置过滤器。大致会分为两种方案:
- 使用
@WebFilter+@Order
并配置@ServletComponentScan
。 - 使用代码配置的方式编写
FilterRegistrationBean
,在其中调用setOrder
方法来配置顺序。
实际使用时就会发现使用@WebFilter+@Order
的方式虽然可以使过滤器生效,但是却无法使顺序生效。而百度搜索的文章基本上都是抄袭复制的,导致错误的内容广泛传播。
所以建议在想要学习框架的某种功能时,第一优先级应该去查询官方文档, SpringBoot的官方文档 就有关于过滤器顺序的说明。
在文档中说明了可以使用@Order
或者实现Ordered
接口,以及使用FilterRegistrationBean
的setOrder
方法。那么问题就来了,这官方文档不是说了可以使用@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启动完成之后,触发TomcatStarter
的OnStartup
方法
--> 其中触发ServletContextInitializer
的OnStartup
方法
--> 回到问题一中注册过滤器的流程
总结
SpringBoot配置过滤器并且支持排序的方式有两种:
- 在过滤器上使用@Component+@Order即可
但是这种无法配置过滤器参数 - 代码配置的方式定义FilterRegistrationBean
这种就很灵活,可以实现过滤器所有配置
另外,在学习源码的时候,应该是带着问题的学习的。有时并不需要太过注意源码细节,应该理解其抽象含义,等了解到了整体结构之后再一层层的往里拨。