Day614.SpringWebFilter常见错误② -Spring编程常见错误
Posted 阿昌喜欢吃黄桃
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day614.SpringWebFilter常见错误② -Spring编程常见错误相关的知识,希望对你有一定的参考价值。
SpringWebFilter常见错误②
继续上节部分,如下依然使用@ServletComponentScan+@WebFilter的方式
,同样也照样可能会出现如下的问题。
一、@WebFilter 过滤器使用@Order注解失效
首先,创建启动程序的代码如下:
@SpringBootApplication
@ServletComponentScan
@Slf4j
public class Application
public static void main(String[] args)
SpringApplication.run(Application.class, args);
log.info("启动成功");
实现的 Controller 代码如下:
@Controller
@Slf4j
public class StudentController
@PostMapping("/regStudent/name)")
@ResponseBody
public String saveUser(String name) throws Exception
System.out.println("......用户注册成功");
return "success";
上述代码提供了一个 Restful 接口 “/regStudent”。
该接口只有一个参数 name,注册成功会返回"success"。
现在,我们来实现两个新的过滤器,代码如下:AuthFilter
:
例如,限制特定 IP 地址段(例如校园网内)的用户方可注册为新用户,当然这里我们仅仅 Sleep 1 秒来模拟这个过程。
@WebFilter
@Slf4j
@Order(2)
public class AuthFilter implements Filter
@SneakyThrows
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
if(isPassAuth())
System.out.println("通过授权");
chain.doFilter(request, response);
else
System.out.println("未通过授权");
((HttpServletResponse)response).sendError(401);
private boolean isPassAuth() throws InterruptedException
System.out.println("执行检查权限");
Thread.sleep(1000);
return true;
TimeCostFilter:计算注册学生的执行耗时,需要包括授权过程。
@WebFilter
@Slf4j
@Order(1)
public class TimeCostFilter implements Filter
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
System.out.println("#开始计算接口耗时");
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("#执行时间(ms):" + time);
在上述代码中,我们使用了 @Order,期望 TimeCostFilter 先被执行,因为 TimeCostFilter 设计的初衷是统计这个接口的性能,所以是需要统计 AuthFilter 执行的授权过程的。全部代码实现完毕,执行结果如下:
执行检查权限
通过授权
#开始计算接口耗时
…用户注册成功
#执行时间(ms):33
从结果来看,执行时间并不包含授权过程,所以这并不符合我们的预期,毕竟我们是加了 @Order
的。
但是如果我们交换 Order 指定的值,你会发现也不见效果,为什么会如此?难道 Order 不能用来排序 WebFilter 么?
通过上节课的学习,我们得知:当一个请求来临时,会执行到 StandardWrapperValve 的 invoke(),这个方法会创建 ApplicationFilterChain,并通过 ApplicationFilterChain#doFilter() 触发过滤器执行,并最终执行到内部私有方法 internalDoFilter(), 我们可以尝试在 internalDoFilter() 中寻找一些启示:
private void internalDoFilter(ServletRequest request,
ServletResponse response)
throws IOException, ServletException
// Call the next filter if there is one
if (pos < n)
ApplicationFilterConfig filterConfig = filters[pos++];
try
Filter filter = filterConfig.getFilter();
过滤器的执行顺序是由类成员变量 Filters 决定的,而 Filters 变量则是 createFilterChain() 在容器启动时顺序遍历 StandardContext 中的成员变量 FilterMaps 获得的:
public static ApplicationFilterChain createFilterChain(ServletRequest request,
Wrapper wrapper, Servlet servlet)
// 省略非关键代码
// Acquire the filter mappings for this Context
StandardContext context = (StandardContext) wrapper.getParent();
FilterMap filterMaps[] = context.findFilterMaps();
// 省略非关键代码
// Add the relevant path-mapped filters to this filter chain
for (int i = 0; i < filterMaps.length; i++)
if (!matchDispatcher(filterMaps[i] ,dispatcher))
continue;
if (!matchFiltersURL(filterMaps[i], requestPath))
continue;
ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
context.findFilterConfig(filterMaps[i].getFilterName());
if (filterConfig == null)
continue;
filterChain.addFilter(filterConfig);
// 省略非关键代码
// Return the completed filter chain
return filterChain;
继续查找对 StandardContext 成员变量 FilterMaps 的写入引用,我们找到了 addFilterMapBefore():
public void addFilterMapBefore(FilterMap filterMap)
validateFilterMap(filterMap);
// Add this filter mapping to our registered set
filterMaps.addBefore(filterMap);
fireContainerEvent("addFilterMap", filterMap);
现在知道过滤器的执行顺序是由 StandardContext 类成员变量 FilterMaps 的顺序
决定,而 FilterMaps 则是一个包装过的数组,所以我们只要进一步弄清楚 FilterMaps 中各元素的排列顺序即可。
我们继续在 addFilterMapBefore() 中加入断点,尝试从调用栈中找到一些线索:
addFilterMapBefore:2992, StandardContext
addMappingForUrlPatterns:107, ApplicationFilterRegistration
configure:229, AbstractFilterRegistrationBean
configure:44, AbstractFilterRegistrationBean
register:113, DynamicRegistrationBean
onStartup:53, RegistrationBean
selfInitialize:228, ServletWebServerApplicationContext
// 省略非关键代码
可知,Spring 从 selfInitialize() 一直依次调用到 addFilterMapBefore(),稍微分析下 selfInitialize(),我们可以了解到,这里是通过调用 getServletContextInitializerBeans(),获取所有的 ServletContextInitializer 类型的 Bean,并调用该 Bean 的 onStartup(),从而一步步以调用栈显示的顺序,最终调用到 addFilterMapBefore()。
private void selfInitialize(ServletContext servletContext) throws ServletException
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans())
beans.onStartup(servletContext);
首先,查看上述代码中的 getServletContextInitializerBeans(),因为此方法返回的 ServletContextInitializer 类型的 Bean 集合顺序决定了 addFilterMapBefore() 调用的顺序,从而决定了 FilterMaps 内元素的顺序,最终决定了过滤器的执行顺序。
getServletContextInitializerBeans() 的实现非常简单,只是返回了 ServletContextInitializerBeans 类的一个实例,参考代码如下:
protected Collection<ServletContextInitializer> getServletContextInitializerBeans()
return new ServletContextInitializerBeans(getBeanFactory());
上述方法的返回值是个 Collection,可见 ServletContextInitializerBeans 类是一个集合类,它继承了 AbstractCollection 抽象类。
也因为如此,上述 selfInitialize() 才可以遍历 ServletContextInitializerBeans 的实例对象。既然 ServletContextInitializerBeans 是集合类,那么我们就可以先查看其 iterator(),看看它遍历的是什么。
@Override
public Iterator<ServletContextInitializer> iterator()
return this.sortedList.iterator();
此集合类对外暴露的集合遍历元素为 sortedList 成员变量,也就是说,上述 selfInitialize() 最终遍历的即为 sortedList 成员变量。
sortedList 中的过滤器 Bean 元素顺序,决定了最终过滤器的执行顺序。现在我们继续查看 ServletContextInitializerBeans 的构造方法如下:
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);
类成员变量 this.sortedList,其元素顺序是由类成员变量 this.initializers 的 values 通过比较器 AnnotationAwareOrderComparator 进行排序的。
继续查看 AnnotationAwareOrderComparator 比较器,忽略比较器调用的细节过程,其最终是通过两种方式获取比较器需要的 order 值,来决定 sortedInitializers 的排列顺序:
因为 this.initializers 的 values 类型为 ServletContextInitializer,其实现了 Ordered 接口,所以这里的比较器显然是使用了 getOrder() 获取比较器所需的 order 值,对应的类成员变量即为 order。
继续查看 this.initializers 中的元素在何处被添加,addServletContextInitializerBeans() 以及 addAdaptableBeans() 这两个方法均构建了 ServletContextInitializer 子类的实例,并添加到了 this.initializers 成员变量中。
在这里,我们只研究 addServletContextInitializerBeans,毕竟我们使用的添加过滤器方式(使用 @WebFilter 标记)最终只会通过这个方法生效。在这个方法中,Spring 通过 getOrderedBeansOfType() 实例化了所有 ServletContextInitializer 的子类:
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);
根据其不同类型,调用 addServletContextInitializerBean(),我们可以看出 ServletContextInitializer 的子类包括了 ServletRegistrationBean、FilterRegistrationBean 以及 ServletListenerRegistrationBean,正好对应了 Servlet 的三大要素。
而这里我们只需要关心对应于 Filter 的 FilterRegistrationBean,显然,FilterRegistrationBean 是 ServletContextInitializer 的子类(实现了 Ordered 接口),同样由成员变量 order 的值决定其执行的优先级。
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);
else if (initializer instanceof DelegatingFilterProxyRegistrationBean)
String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName();
addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
else if (initializer instanceof ServletListenerRegistrationBean)
EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener();
addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
else
addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory,
initializer);
最终添加到 this.initializers 成员变量中:
private void addServletContextInitializerBean(Class<?> type, String beanName, ServletContextInitializer initializer,
ListableBeanFactory beanFactory, Object source)
this.initializers.add(type, initializer);
// 省略非关键代码
现在我们来总结下,过滤器的执行顺序是由下面这个串联决定的:
RegistrationBean 中 order 属性的值 ->
ServletContextInitializerBeans 类成员变量 sortedList 中元素的顺序 ->
ServletWebServerApplicationContext 中 selfInitialize() 遍历 FilterRegistrationBean 的顺序 ->
addFilterMapBefore() 调用的顺序 ->
filterMaps 内元素的顺序 ->
过滤器的执行顺序
可见,RegistrationBean 中 order 属性
的值最终可以决定过滤器的执行顺序。但是可惜的是:
当使用 @WebFilter 时,构建的 FilterRegistrationBean 并没有依据 @Order 的值去设置 order 属性,所以 @Order 失效了。
那么对应的解决方案是如下
因为 WebFilterHandler 中 doHandle() 的逻辑(即通过 BeanDefinitionBuilder 动态构建了 FilterRegistrationBean 类型的 BeanDefinition)。然而遗憾的是,此处并没有设置 order 的值,更没有根据 @Order 指定的值去设置。
如此,他不获取根据Order值排序的话,那我们只能不通过这种方式,手动实例化对应的Bean
@Configuration
public class FilterConfiguration
@Bean
public FilterRegistrationBean authFilter()
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new AuthFilter());
registration.addUrlPatterns("/*");
registration.setOrder(2);
return registration;
@Bean
public FilterRegistrationBean timeCostFilter()
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new TimeCostFilter());
registration.addUrlPatterns("/*");
registration.setOrder(1);
return registration;
按照我们查看的源码中的逻辑,虽然 WebFilterHandler 中 doHandle() 构建了 FilterRegistrationBean 类型的 BeanDefinition,但没有设置 order 的值。
所以在这里,我们直接手工实例化了 FilterRegistrationBean 实例,而且设置了其 setOrder()。同时不要忘记去掉 AuthFilter 和 TimeCostFilter 类中的 @WebFilter。
二、过滤器被多次执行
前文,我们记录过一个过滤器被多次执行的案例,就是我们多次调用doFilter()方法。
那这里我们能否在两个过滤器中增加 @Component,从而让 @Order 生效呢?代码如下。
此时@WebFilter/@Component
同时存在。
@WebFilter
@Slf4j
@Order(2)
@Component
public class AuthFilter implements Filter
@SneakyThrows
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
if(isPassAuth())
System.out.println("通过授权");
chain.doFilter(request, response);
else
System.out.println("未通过授权");
((HttpServletResponse)response).sendError(401);
private boolean isPassAuth() throws InterruptedException
System.out.println("执行检查权限");
Thread.sleep(1000);
return true;
@WebFilter
@Slf4j
@Order(1)
@Component
public class TimeCostFilter implements Filter
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
System.out.println("#开始计算接口耗时");
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long end = System.currentTimeMillis();
long time = end - start;
System.out.println("#执行时间(ms):" + time);
最终执行结果如下:
#开始计算接口耗时
执行检查权限
通过授权
执行检查权限
通过授权
#开始计算接口耗时
…用户注册成功
#执行时间(ms):73
#执行时间(ms):2075
更改 AuthFilter 类中的 Order 值为 0,继续测试,得到结果如下:
执行检查权限
通过授权
#开始计算接口耗时
执行检查权限
通过授权
#开始计算接口耗时
…用户注册成功
#执行时间(ms):96
#执行时间(ms):1100
发现修改Order的值并能改变执行顺序,但发现过滤器被执行了2次。
继续查阅 ServletContextInitializerBeans 的构造方法如下
public ServletContextInitializerBeans(ListableBeanFactory beanFactory,
Class<? extends ServletContextInitializer>... initializerTypes)
this.initializers = new LinkedMultiValueMap<>();
this.initializerTypes = (initializerTypes.length != 0) ? Arrays.asList(initializerTypes)
: Collections.singletonList(ServletContextInitializer.class)<以上是关于Day614.SpringWebFilter常见错误② -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章
day.java:5: 错: 编码 GBK 的不可映射字符 (0x88) System.out.println((i+1)+"链?"+"链?"+day[i]+&