最近碰到了一个关于过滤器的奇怪的问题。
项目中定义了若干过滤器,其中一个是解析header参数,并做相应处理。另一个过滤器覆写了getHeader方法,判断参数值中是否有非法字符,如果有则抛出异常。这两个过滤器均通过WebFilter + ServletComponetScan
注解方式来声明的。
在写本文之前,我认为WebFilter方式声明的过滤器是按照类名的字母表顺序来执行的,本地执行结果和网上的很多资料都显示是这样。
而这两个过滤器类名按照字母表顺序,覆写getHeader方法的过滤器执行在前,解析header参数的过滤器执行在后,这样当解析header参数调用request.getHeader方法时,就会执行覆写的方法了。
实际上本地执行结果也符合这一认知,但是在k8s集群中执行时问题就来了,从返回结果看并没有进入到覆写方法中,开启远程调试也显示没有进入到覆写方法过滤器中,在过滤器中加上日志后又一切正常。这个bug好像是变成了量子态一样不可观测,找了很久都找不到原因。
但其表现总归是覆写方法的过滤器没有执行到,因此直接更换过滤器注册方式。最后问题不再出现。
根据查询到的资料发现,其实WebFilter声明的过滤器执行是没有明确的顺序的,执行顺序不可控。
很多资料都说到了这一点,但是没有给出令人信服的原因。参考资料2中倒是有具体分析,说是通过扫描加载过滤器类文件的时候调用了File类的list方法,该方法不保证文件的有序性。
*
There is no guarantee that the name strings in the resulting array * will appear in any specific order; they are not, in particular, * guaranteed to appear in alphabetical order.
这样问题就能解释得通了。通过WebFilter注册的过滤器执行顺序不可控,在集群上运行时没有按照预期顺序执行,导致出现了前面描述的问题。不过为什么本地不管重启多少次都始终按照字母表顺序执行,而在集群环境下刚好相反呢?这个表象还是挺奇怪的。
WebFilter为什么不支持排序呢?官方对此有明确解释,就是排序和Filter属于不同的规范,两者不能混为一谈,具体讨论见参考资料3。
在早期过滤器是在web.xml中定义的,过滤器的执行顺序是按照定义的顺序来执行的。现在基本不这么写了,而是通过注解的方式,在spring容器初始化过程中自动装配。一共有三种实现方式:
首先就是上面说过的通过@WebFilter
注解来声明过滤器,然后在启动类上添加@ServletComponetScan
注解,该注解会自动扫描@WebServlet、@WebFilter 和 @WebListener 注解的类。WebFilter等注解是在Servlet 3.0引入的。
以这种方式注册的过滤器,以@Order
来指定执行顺序是无效的(filterName也无效)。但事实上,以此方式注册的过滤器有一个默认的order值,即FilterRegistrationBean
的父类RegistrationBean
中定义的order:Integer.MAX_VALUE
。因此,通过@WebFilter注册的过滤器其优先级都是一样的。在优先级都一样的情况下,过滤器执行顺序是不固定的,千万不要认为是按字母表顺序执行。
@ServletComponentScan 这个注解仅对内嵌的 tomcat 生效,如果使用单独的 tomcat,这种方式无效。
参数值中,value 和 urlPatterns 不能共存,如果同时指定,通常忽略 value 的取值。
根据参考资料1我们知道,以WebFilter方式注册的过滤器,实际上还是一种FilterRegistrationBean
。因此,可以直接将filter注册成FilterRegistrationBean
,并设置order的值。如下所示:
@Configuration
public class FilterConfiguration {
private static final String URL_PATTERN = "/*";
private static final String ORDER_VALUE = "1";
@Bean
public FilterRegistrationBean cookieFilter() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CookieFilter());
registrationBean.addUrlPatterns(URL_PATTERN);
registrationBean.setOrder(ORDER_VALUE);
return registrationBean;
}
}
如果不设置order的值,其默认值仍然是Integer.MAX_VALUE
。
除了上述两种方式外,还可以通过@Component
注解注册过滤器。如下所示:
@Order(1)
@Component
public class LoggingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response,
final FilterChain filterChain) throws ServletException, IOException {
...
}
}
这种方式注册的过滤器可以通过@Order
来指定过滤器执行顺序,默认值为Integer.MAX_VALUE
。
上面三种方式种,除了第一种方式无法指定优先级而采用默认值之外,其他两种方式都可以指定优先级。那如果都采用默认值呢?此时优先级规则如下:
在这三种过滤器注册方式中,第三种方法无法指定url,也就是说此方式注册的过滤器必定是全局过滤器。第二种方法无法控制顺序,适合没有顺序要求的过滤器。
[1].https://blog.csdn.net/Zong_0915/article/details/126747302
[2].https://blog.csdn.net/zy1992As/article/details/129243604
[3].https://github.com/spring-projects/spring-boot/issues/8276