目录
背景
公司有个项目从SpringBoot 1.5.12升级到2.1.9,POM版本修改完成访问项目域名http://ww.xxxx.com/ 直接500。后台日志显示无法Shiro无法从上下文中获取SecurityManager。后来试试别的地址http://www.xxxx.com/admin/test 居然访问正常。查看ShiroFilter的配置: url-pattern /* 。后来发现有个Filter,存在如下代码
request.getDispatcher("/").forward(request,response);
注释掉该行,发现很多功能居然是正常的。也就意味着forward() 时,并未通过ShiroFilter处理。
问题定位
(1)比对升级前后,FilterChain中Filter的数量、类型、顺序,没有问题;
(2)比对实际走过的Filter,发现运行上面那行代码后,实际走过的Filter数量从20个锐减为3个,当时有种见鬼了的感觉。
(3)有个细心的小伙伴发现,Forward后的请求在获取FilterChain时,居然要考虑dispatcher属性是否匹配;排查发现SB 2.1.9中在Filter注册时,设置了Filter的dispatcherType为REQUEST。而在SB 1.5.12中居然是一组值。抱着试试的态度,把除了DisatcherType.ASYNC外的其他值全部设置给ShiroFilter,功能全部正常了。
源码分析
比较两个版本中Filter的注册类AbstractFilterRegistrationBean,SpringBoot 2.1.9实现如下。
/**
* SpringBoot 2.1.9
* Configure registration settings. Subclasses can override this method to perform
* additional configuration if required.
* @param registration the registration
*/
@Override
protected void configure(FilterRegistration.Dynamic registration) {
super.configure(registration);
// dispatcherType赋值
EnumSet<DispatcherType> dispatcherTypes = this.dispatcherTypes;
if (dispatcherTypes == null) {
dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
}
Set<String> servletNames = new LinkedHashSet<>();
for (ServletRegistrationBean<?> servletRegistrationBean : this.servletRegistrationBeans) {
servletNames.add(servletRegistrationBean.getServletName());
}
servletNames.addAll(this.servletNames);
if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS);
}
else {
if (!servletNames.isEmpty()) {
registration.addMappingForServletNames(dispatcherTypes, this.matchAfter,
StringUtils.toStringArray(servletNames));
}
if (!this.urlPatterns.isEmpty()) {
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter,
StringUtils.toStringArray(this.urlPatterns));
}
}
}
SB 1.5.12实现如下
/**
* SpringBoot 1.5.12-RELEASE
*/
protected void configure(Dynamic registration) {
super.configure(registration);
// 关键差异
EnumSet<DispatcherType> dispatcherTypes = this.dispatcherTypes;
if (dispatcherTypes == null) {
dispatcherTypes = this.isAsyncSupported() ? ASYNC_DISPATCHER_TYPES : NON_ASYNC_DISPATCHER_TYPES;
}
Set<String> servletNames = new LinkedHashSet();
Iterator var4 = this.servletRegistrationBeans.iterator();
while(var4.hasNext()) {
ServletRegistrationBean servletRegistrationBean = (ServletRegistrationBean)var4.next();
servletNames.add(servletRegistrationBean.getServletName());
}
servletNames.addAll(this.servletNames);
if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
this.logger.info("Mapping filter: '" + registration.getName() + "' to: " + Arrays.asList(DEFAULT_URL_MAPPINGS));
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS);
} else {
if (!servletNames.isEmpty()) {
this.logger.info("Mapping filter: '" + registration.getName() + "' to servlets: " + servletNames);
registration.addMappingForServletNames(dispatcherTypes, this.matchAfter, (String[])servletNames.toArray(new String[servletNames.size()]));
}
if (!this.urlPatterns.isEmpty()) {
this.logger.info("Mapping filter: '" + registration.getName() + "' to urls: " + this.urlPatterns);
registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, (String[])this.urlPatterns.toArray(new String[this.urlPatterns.size()]));
}
}
}
}
正是由于SB1.5.12支持的dispatcherType中包含FORWARD,因此在forward("/")后通过ShiroFilter,所以没有问题。
官方文档说明
在SpringBoot2.1.9的文档中特别说明,如下图所示:
但是在SpringBoot2.1.9之前的文档中相关章节并未特别说明,以2.1.8为例仅说明此种Filter的顺序是如何设置的如下图所示:
Dispatcher的作用和影响
想到此种调整必是规范要求,于是阅读了Java Servlet 3.1 规范中关于DispatcherType的说明,比较精华的一段摘录如下
public DispatcherType getDispatcherType() -// 返回request的DispatcherTypeReturns the dispatcher type of a request.// 容器使用dispatcher type来判断需要应用的过滤器。The dispatcher type of a request is used by the container to select the filters that need to be applied to the request.// 只有dispatcherType和url pattern都匹配的filter才会被应用;Only filters with the matching dispatcher type and url patterns will be applied.// 考虑到Filter可以配置为支持多个dispatcher type,这样就可以通过dipatcher type来差异化处理请求Allowing a filter that has been configured for multiple dispatcher types to query a request forit’s dispatcher type allows the filter to process the request differently depending on it’s dispatcher type.// 一个请求的dispatcher type 初始值为 REQUESTThe initial dispatcher type of a request is defined as DispatcherType.REQUEST .// forward() 方法产生请求的dispatcher type 为 FORWARD, include() 方法产生请求的dispatcher type为INCLUDE,基于AsyncContext 产生请求的dispatcher type 为 ASYNCThe dispatcher type of a request dispatched via RequestDispatcher.forward(ServletRequest, ServletResponse) or RequestDispatcher.include(ServletRequest, ServletResponse) is given as DispatcherType.FORWARD or DispatcherType.INCLUDE respectively, while a dispatcher type of an asynchronous request dispatched via one of the AsyncContext.dispatch methods is given as DispatcherType.ASYNC .// 最终被dispatch到容器错误处理机制请求的dispatcher type为 ERRORFinally the dispatcher type of a request dispatched to an error page by the container’s error handling mechanism is given as DispatcherType.ERROR .
简单说就是Filter的选择是基于Filter的url-pattern和dispatcherType 与 request的url和dispatcher是否匹配来选择。一个request在dispatcher过程中其dispatcher type会发生变化。
再看FilterChain
(1)首先FilterChain是一个典型的责任链模式,但在实际的工程中却大不相同;比如责任链中增加类似suppor的函数,实现遍历检查,但实际起作用的仅有1个;再比如Java Web中的filter中sessionFilter和ShiroFilter需要两者组合生效;
(2)以往理解的FilterChain是一个静态的,当url-pattern=/*时,所有请求要面对的FilterChain是一样的。但是现在有个一个二次筛选机制,实际每个请求的FilterChain是动态的;
(3)阅读SpringBoot 2.1.9内嵌Tomcat源码对应的一次forward()处理过程。
场景设定:
Servlet A的url为/a,
Servlet B的url为/b,
图中假设用户请求/a,并在Servlet A中调用 request.getDispatcher("/b").forward(request,response),过程涵盖从ApplicationDispatcher对象创建FilterChain并执行,直到请求响应完成。
小结
本文以DispatcherType为切入点,介绍了Dispatcher的作用场景,以及由此发现FilterChain的动态性。与个人而言,解决了一个工作中的问题,在此记录希望对其他人能够有所帮助。
参考文档
1. SpringBoot各版本参考文档
https://docs.spring.io/spring-boot/docs/
2. SpringBoot2.1.9 基于SpringBean添加Servlet,Filter,Listener
3. Java Servlet 3.1规范
https://download.oracle.com/otn-pub/jcp/servlet-3_1-fr-eval-spec/servlet-3_1-final.pdf
4. 时序图基于markdown语言编写,通过Visual Studio Code+Markdown Preview Enchener插件预览后截图而成;