主要了解SpringBoot中使用拦截器和过滤器的使用,关于两者,资料所提及的有:
作用域差异:Filter是Servlet规范中规定的,只能用于WEB中,拦截器既可以用于WEB,也可以用于Application、Swing中(即过滤器是依赖于Servlet容器的,和它类似的还有Servlet中的监听器同样依赖该容器,而拦截器则不依赖它);
规范差异:Filter是Servlet规范中定义的,是Servlet容器支持的,而拦截器是Spring容器内的,是Spring框架支持的;
资源差异:拦截器是Spring的一个组件,归Spring管理配置在Spring的文件中,可以使用Spring内的任何资源、对象(可以粗浅的认为是IOC容器中的Bean对象),而Filter则不能使用访问这些资源;
深度差异:Filter只在Servlet前后起作用,而拦截器可以深入到方法的前后、异常抛出前后等更深层次的程度作处理(这里也在一定程度上论证了拦截器是利用java的反射机制实现的),所以在Spring框架中,优先使用拦截器;
1. 关于ApplicationListener
除此之外,还有监听器,在项目中遇到一个ApplicationListener,在容器初始化完成后,有一些操作需要处理一下,比如数据的加载、初始化缓存、特定任务的注册等,此时可以使用这个监听器,下面是使用方式:
// 1. 定义实现类实现ApplicationListener接口 package com.glodon.tot.listener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import java.net.InetAddress; import java.net.UnknownHostException; /** * @author liuwg-a * @date 2018/11/26 10:48 * @description 容器初始化后要做的数据处理 */ public class StartupListener implements ApplicationListener<ContextRefreshedEvent> { private static final Logger logger = LoggerFactory.getLogger(StartupListener.class); @Override public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) { try { logger.info("get local ip is " + InetAddress.getLocalHost().getHostAddress()); } catch (UnknownHostException e) { e.printStackTrace(); logger.error("occur a exception!"); } } } // 2. 配置上述实现类,返回Bean实例,类似于在xml中配置<bean>标签 package com.glodon.tot.config; import com.glodon.tot.listener.StartupListener; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * @author liuwg-a * @date 2018/11/26 11:10 * @description 配置监听器 */ @Configuration public class ListenerConfig { // 这里会直接注入 @Bean public StartupListener startupListener() { return new StartupListener(); } }
主要就是上述配置,其他和普通SpringBoot项目一样,启动项目即可,最初启动(即不主动调用接口)的效果如下:
2018-11-26 11:23:18.360 INFO 18792 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2018-11-26 11:23:18.360 INFO 18792 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2018-11-26 11:23:18.372 INFO 18792 --- [ main] .m.m.a.ExceptionHandlerExceptionResolver : Detected @ExceptionHandler methods in globalExceptionHandler 2018-11-26 11:23:18.389 INFO 18792 --- [ main] o.s.w.s.handler.SimpleUrlHandlerMapping : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler] 2018-11-26 11:23:18.578 INFO 18792 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup welcome to StartupListener... your ip is 10.4.37.108 2018-11-26 11:23:18.590 INFO 18792 --- [ main] com.glodon.tot.listener.StartupListener : get local ip is 10.4.37.108 2018-11-26 11:23:18.822 INFO 18792 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8081 (http) 2018-11-26 11:23:18.827 INFO 18792 --- [ main] com.glodon.tot.Application : Started Application in 4.616 seconds (JVM running for 11.46)
在调用接口后,添加了如下日志:
2018-11-26 11:25:46.785 INFO 18792 --- [nio-8081-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet' 2018-11-26 11:25:46.785 INFO 18792 --- [nio-8081-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started 2018-11-26 11:25:46.803 INFO 18792 --- [nio-8081-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 18 ms
2. 关于拦截器
在SpringBoot使用拦截器,流程如下:
// 1. 定义拦截器 package com.glodon.tot.interceptor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author liuwg-a * @date 2018/11/26 14:55 * @description 配置拦截器 */ public class UrlInterceptor implements HandlerInterceptor { private static final Logger logger = LoggerFactory.getLogger(UrlInterceptor.class); private static final String GET_ALL = "getAll"; private static final String GET_HEADER = "getHeader"; /** * 进入Controller层之前拦截请求,默认是拦截所有请求 * @param httpServletRequest request * @param httpServletResponse response * @param o object * @return 是否拦截当前请求,true表示拦截当前请求,false表示不拦截当前请求 * @throws Exception 可能出现的异常 */ @Override public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception { logger.info("go into preHandle method ... "); String requestURI = httpServletRequest.getRequestURI(); if (requestURI.contains(GET_ALL)) { return true; } if (requestURI.contains(GET_HEADER)) { httpServletResponse.sendRedirect("/user/redirect"); } return true; } /** * 处理完请求后但还未渲染试图之前进行的操作 * @param httpServletRequest request * @param httpServletResponse response * @param o object * @param modelAndView mv * @throws Exception E */ @Override public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception { logger.info("go into postHandle ... "); } /** * 视图渲染后但还未返回到客户端时的操作 * @param httpServletRequest request * @param httpServletResponse response * @param o object * @param e exception * @throws Exception */ @Override public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception { logger.info("go into afterCompletion ... "); } } // 2. 注册拦截器 package com.glodon.tot.config; import com.glodon.tot.interceptor.UrlInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; /** * @author liuwg-a * @date 2018/11/26 15:30 * @description 配置MVC */ @Configuration public class MvcConfig implements WebMvcConfigurer { /** * 注册配置的拦截器 * @param registry 拦截器注册器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // 这里的拦截器是new出来的,在Spring框架中可以交给IOC进行依赖注入,直接使用@Autowired注入 registry.addInterceptor(new UrlInterceptor()); } }
主要就是上述的两个步骤,需要注意的是preHandle()方法只有返回true,Controller中接口方法才能执行,否则不能执行,直接在preHandle()返回后false结束流程。上述在配置WebMvcConfigurer实现类中注册拦截器时除了使用registry.addInterceptor(new UrlInterceptor())注册外,还可以指定哪些URL可以应用这个拦截器,如下:
// 使用自动注入的方式注入拦截器,添加应用、或不应用该拦截器的URI(addPathPatterns/excludePathPatterns) // addPathPatterns 用于添加拦截的规则,excludePathPatterns 用于排除拦截的规则 registry.addInterceptor(urlInterceptor).addPathPatterns(new UrlInterceptor()).excludePathPatterns("/login");
上述注册拦截器路径时(即addPathPatterns和excludePathPatterns的参数),是支持通配符的,写法如下:
通配符 说明
* 匹配单个字符,如/user/*匹配到/user/a等,又如/user/*/ab匹配到/user/p/ab;
** 匹配任意多字符(包括多级路径),如/user/**匹配到user/a、/user/abs/po等;
上述也可以混合使用,如/user/po*/**、/user/{userId}/*(pathValue是可以和通配符共存的);
注:
Spring boot 2.0 后WebMvcConfigurerAdapter已经过时,所以这里并不是继承它,而是继承WebMvcConfigurer;
这里在实操时,使用IDEA工具继承WebMvcConfigurer接口时,使用快捷键Alt+Enter已经无论如何没有提示,进入查看发现这个接口中所有的方法变成了default方法(JDK8新特性,这个修饰符修饰的方法必须要有方法体,此时接口中允许有具体的方法,在实现该接口时,用户可以选择是否重写该方法,而不是必须重写了),所以没有提示,可以手动进入接口中复制对应的方法名(不包括default修饰符)。
对于WebMvcConfigurer接口中的常用方法有如下使用示例,可以选择性重写:
@Configuration public class MvcConfig implements WebMvcConfigurer { @Autowired private HandlerInterceptor urlInterceptor; private static List<String> myPathPatterns = new ArrayList<>(); /** * 在初始化Servlet服务时(在Servlet构造函数执行之后、init()之前执行),@PostConstruct注解的方法被调用 */ @PostConstruct void init() { System.out.println("Servlet init ... "); // 添加匹配的规则, /** 表示匹配所有规则,任意路径 myPathPatterns.add("/**"); } /** * 在卸载Servlet服务时(在Servlet的destroy()方法之前执行),@PreDestroy注解的方法被调用 */ @PreDestroy void destroy() { System.out.println("Servlet destory ... "); } /** * 注册配置的拦截器 * @param registry 拦截器注册器 */ @Override public void addInterceptors(InterceptorRegistry registry) { // addPathPatterns 用于添加拦截规则 // excludePathPatterns 用户排除拦截 registry.addInterceptor(urlInterceptor).addPathPatterns(myPathPatterns).excludePathPatterns("/user/login"); } // 下面的方法可以选择性重写 /** * 添加类型转换器和格式化器 * @param registry */ @Override public void addFormatters(FormatterRegistry registry) { // registry.addFormatterForFieldType(LocalDate.class, new USLocalDateFormatter()); } /** * 跨域支持 * @param registry */ @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("*") .allowCredentials(true) .allowedMethods("GET", "POST", "DELETE", "PUT") .maxAge(3600 * 24); } /** * 添加静态资源映射--过滤swagger-api * @param registry */ @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { //过滤swagger registry.addResourceHandler("swagger-ui.html") .addResourceLocations("classpath:/META-INF/resources/"); registry.addResourceHandler("/webjars/**") .addResourceLocations("classpath:/META-INF/resources/webjars/"); registry.addResourceHandler("/swagger-resources/**") .addResourceLocations("classpath:/META-INF/resources/swagger-resources/"); registry.addResourceHandler("/swagger/**") .addResourceLocations("classpath:/META-INF/resources/swagger*"); registry.addResourceHandler("/v2/api-docs/**") .addResourceLocations("classpath:/META-INF/resources/v2/api-docs/"); } /** * 配置消息转换器--这里用的是ali的FastJson * @param converters */ @Override public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { //1. 定义一个convert转换消息的对象; FastJsonHttpMessageConverter fastJsonHttpMessageConverter = new FastJsonHttpMessageConverter(); //2. 添加fastJson的配置信息,比如:是否要格式化返回的json数据; FastJsonConfig fastJsonConfig = new FastJsonConfig(); fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat, SerializerFeature.WriteMapNullValue, SerializerFeature.WriteNullStringAsEmpty, SerializerFeature.DisableCircularReferenceDetect, SerializerFeature.WriteNullListAsEmpty, SerializerFeature.WriteDateUseDateFormat); //3处理中文乱码问题 List<MediaType> fastMediaTypes = new ArrayList<>(); fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8); //4.在convert中添加配置信息. fastJsonHttpMessageConverter.setSupportedMediaTypes(fastMediaTypes); fastJsonHttpMessageConverter.setFastJsonConfig(fastJsonConfig); //5.将convert添加到converters当中. converters.add(fastJsonHttpMessageConverter); } /** * 访问页面需要先创建个Controller控制类,再写方法跳转到页面 * 这里的配置可实现直接访问http://localhost:8080/toLogin就跳转到login.jsp页面了 * @param registry */ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/toLogin").setViewName("login"); } /** * 开启默认拦截器可用并指定一个默认拦截器DefaultServletHttpRequestHandler,比如在webroot目录下的图片:xx.png, * Servelt规范中web根目录(webroot)下的文件可以直接访问的,但DispatcherServlet配置了映射路径是/ , * 几乎把所有的请求都拦截了,从而导致xx.png访问不到,这时注册一个DefaultServletHttpRequestHandler可以解决这个问题。 * 其实可以理解为DispatcherServlet破坏了Servlet的一个特性(根目录下的文件可以直接访问),DefaultServletHttpRequestHandler * 可以帮助回归这个特性的 * @param configurer */ @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); // 这里可以自己指定默认的拦截器 configurer.enable("DefaultServletHttpRequestHandler"); } /** * 在该方法中可以启用内容裁决解析器,configureContentNegotiation()方法是专门用来配置内容裁决参数的 * @param configurer */ @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 表示是否通过请求的Url的扩展名来决定media type configurer.favorPathExtension(true) // 忽略Accept请求头 .ignoreAcceptHeader(true) .parameterName("mediaType") // 设置默认的mediaType .defaultContentType(MediaType.TEXT_HTML) // 以.html结尾的请求会被当成MediaType.TEXT_HTML .mediaType("html", MediaType.TEXT_HTML) // 以.json结尾的请求会被当成MediaType.APPLICATION_JSON .mediaType("json", MediaType.APPLICATION_JSON); } }
3. 关于过滤器
过滤器是依赖于Servlet的,不依赖于Spring,下面使在SpringBoot中使用过滤器的基本流程:
package com.glodon.tot.filter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.core.annotation.Order; import org.springframework.stereotype.Component; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import java.io.IOException; /** * @author liuwg-a * @date 2018/11/28 9:13 * @description 检验缓存中是否有用户信息 */ @Order(2) @WebFilter(urlPatterns = {"/user/*"}, filterName = "loginFilter") public class SessionFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(SessionFilter.class); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("come into SessionFilter init..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { logger.info("come into SessionFilter and do processes..."); // 实际业务处理,这里就是下面图中的before doFilter逻辑 HttpServletRequest HRrequest = (HttpServletRequest) request; Cookie[] cookies = HRrequest.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals("loginUser")) { logger.info("find loginUser: " + cookie.getValue()); break; } } } // 当前过滤器处理完了交给下一个过滤器处理 chain.doFilter(request, response); logger.info("SessionFilter's process has completed!"); } @Override public void destroy() { logger.info("come into SessionFilter destroy..."); } }
上述的配置中,头部的有两个注解:
@Order注解: 用于标注优先级,数值越小优先级越大;
@WebFilter注解: 用于标注过滤器,urlPatterns指定过滤的URI(多个URI之间用逗号分隔),filterName指定名字;
注: 使用@WebFilter注解,必须在Springboot启动类上加@ServletComponentScan注解,否则该注解不生效,过滤器无效!
【问题】
实操时,发现@Order(2)注解未生效,即过滤器的执行顺序没有被指定,而是按照默认过滤器类名的排列的顺序执行(即TestFilter.java在AbcFilter.java之后执行),然后发现,如果使用Spring组件注解标注过滤器,比如@Component(@Service等注解也是一样的,此时Springboot启动类上无须加@ServletComponentScan注解过滤器即可被扫描),@Order(2)注解生效,多个过滤器按指定顺序执行,但此时又出现一个问题,过滤器上@WebFilter注解设置的过滤URI和名字无效(即urlPatterns和filterName无效,其实此时整个@WebFilter注解都是无效的),在随后排查时,发现控制台有如下日志:
发现先初始化了sessionFilter,然后又初始化了一个loginFilter,但上述两个Filter实际就是上面定义的同一个Filter,即同一个Filter初始化了2次,按结果来看使用@Component注解初始化(过滤URI为/*)的内容覆盖了@WebFilter注解初始化(过滤URI为/school/*)的内容,所以导致@WebFilter注解不生效。
【解决方案】
最后发现是对@Order注解认识存在误区,这个注解是用于控制Spring组件被加载的顺序,但并不能决定过滤器的执行顺序,应该使用一个配置类来管理各个过滤器的执行顺序和过滤URI、名字等属性(此时,过滤器上不需要加任何注解,springboot启动类上也无需加@ServletComponentScan注解):
package com.glodon.tot.config; import com.glodon.tot.filter.SessionFilter; import com.glodon.tot.filter.TestFilter2; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import javax.servlet.Filter; import java.util.Arrays; import java.util.List; /** * @author liuwg-a * @date 2018/11/28 17:59 * @description 配置过滤器 */ @Configuration public class FilterConfig { // 这种方式过滤器上不需要加任何注解 @Bean public Filter sessionFilter() { System.out.println("create sessionFilter..."); return new SessionFilter(); } @Bean public Filter testFilter() { System.out.println("create testFilter2..."); return new TestFilter2(); } // 下面这种方式需要在过滤器上加@Component这类注解,然后完成自动注入 // @Autowired // private Filter sessionFilter; // @Autowired // private Filter testFilter; // 有多少个过滤器要配置就写多少,没特殊要求也可以不写 @Bean public FilterRegistrationBean loginFilterRegistration() { String[] arr = {"/user/go", "/user/login"}; List patternList = Arrays.asList(arr); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(sessionFilter()); // filterRegistrationBean.setFilter(sessionFilter); filterRegistrationBean.setUrlPatterns(patternList); filterRegistrationBean.setOrder(2); return filterRegistrationBean; } @Bean public FilterRegistrationBean testFilterRegistration() { // String[] arr = {"/school/all"}; String[] arr = {"/user/go", "/user/login"}; List patternList = Arrays.asList(arr); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(testFilter()); // filterRegistrationBean.setFilter(testFilter); filterRegistrationBean.setUrlPatterns(patternList); filterRegistrationBean.setOrder(1); return filterRegistrationBean; } }
综合来看拦截器和过滤器,如果过滤器和拦截器有且仅各一个的情况下,运行的流程如下:
登录流程可以使用过滤器过滤器所有的URI,在里面检测当前用户是否已经登录,从而判定有无权限访问。
3. 1 关于过滤器链
这一块主要是将上述定义的过滤器封装成一个自定义链,暴露的问题还比较多,下面是自定义链的过程:
// 过滤器1 @Component("sessionFilter") public class SessionFilter implements Filter { private static final Logger logger = LoggerFactory.getLogger(SessionFilter.class); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("come into SessionFilter init..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { logger.info("come into SessionFilter and do processes..."); // 实际业务处理... chain.doFilter(request, response); logger.info("SessionFilter's process has completed!"); } @Override public void destroy() { logger.info("come into SessionFilter destroy..."); } } // 过滤器2 @Component("testFilter") public class TestFilter2 implements Filter { private static final Logger logger = LoggerFactory.getLogger(TestFilter2.class); @Override public void init(FilterConfig filterConfig) throws ServletException { logger.info("come into TestFilter2's init..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { logger.info("come into TestFilter2 and do processes..."); // 实际业务处理... chain.doFilter(request, response); logger.info("TestFilter2's process has completed!"); } @Override public void destroy() { logger.info("come into TestFilter2's destroy..."); } } // 封装过滤器List @Configuration public class FilterChainBean { @Autowired private Filter sessionFilter; @Autowired private Filter testFilter; @Bean(name = "allMyFilter") public List<Filter> registerFilter() { List<Filter> allMyFilter = new ArrayList<>(); allMyFilter.add(sessionFilter); allMyFilter.add(testFilter); return allMyFilter; } } // 装链 @Service("myFilterChain") @Order(1) public class MyChain implements Filter { @Autowired private List<Filter> allMyFilter; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (allMyFilter == null || allMyFilter.size() == 0) { chain.doFilter(request, response); return; } VirtualFilterChain virtualFilterChain = new VirtualFilterChain(chain, allMyFilter); virtualFilterChain.doFilter(request, response); } // 封装过滤器链,参照CompositeFilter中的VirtualFilterChain类代码编写 private class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List<Filter> additionalFilters; private final int n; private int pos = 0; private VirtualFilterChain(FilterChain chain, List<Filter> additionalFilters) { this.originalChain = chain; this.additionalFilters = additionalFilters; this.n = additionalFilters.size(); } @Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if (pos >= n) { originalChain.doFilter(request, response); } else { Filter nextFilter = additionalFilters.get(pos++); nextFilter.doFilter(request, response, this); } } } }
链上没有指定过滤器的URI,默认是拦截所有URI,测试上述链的工作流程,发现结果如下:
可以发现两个过滤器执行了2次,重复执行并不是想要的,开始着手追踪原因,在追踪到MyChain
中的doFilter()
方法时,发现上述自定义链中的内容如下:
猜测问题就是出现在这里,结合debug查看,过滤器链是沿着ApplicationFilterChain
不断调用的,内部涉及到链操作的执行函数doFilter()
的源码如下:
@Override public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; try { java.security.AccessController.doPrivileged( new java.security.PrivilegedExceptionAction<Void>() { @Override public Void run() throws ServletException, IOException { internalDoFilter(req,res); return null; } } ); } catch( PrivilegedActionException pe) { Exception e = pe.getException(); if (e instanceof ServletException) throw (ServletException) e; else if (e instanceof IOException) throw (IOException) e; else if (e instanceof RuntimeException) throw (RuntimeException) e; else throw new ServletException(e.getMessage(), e); } } else { internalDoFilter(request,response); } } private void internalDoFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException { // 调用链中下一个过滤器(如果存在的话),可以在这里打断点查看 if (pos < n) { ApplicationFilterConfig filterConfig = filters[pos++]; try { Filter filter = filterConfig.getFilter(); if (request.isAsyncSupported() && "false".equalsIgnoreCase( filterConfig.getFilterDef().getAsyncSupported())) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } if( Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res, this}; SecurityUtil.doAsPrivilege ("doFilter", filter, classType, args, principal); } else { filter.doFilter(request, response, this); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.filter"), e); } return; } // We fell off the end of the chain -- call the servlet instance try { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(request); lastServicedResponse.set(response); } if (request.isAsyncSupported() && !servletSupportsAsync) { request.setAttribute(Globals.ASYNC_SUPPORTED_ATTR, Boolean.FALSE); } // Use potentially wrapped request from this point if ((request instanceof HttpServletRequest) && (response instanceof HttpServletResponse) && Globals.IS_SECURITY_ENABLED ) { final ServletRequest req = request; final ServletResponse res = response; Principal principal = ((HttpServletRequest) req).getUserPrincipal(); Object[] args = new Object[]{req, res}; SecurityUtil.doAsPrivilege("service", servlet, classTypeUsedInService, args, principal); } else { servlet.service(request, response); } } catch (IOException | ServletException | RuntimeException e) { throw e; } catch (Throwable e) { e = ExceptionUtils.unwrapInvocationTargetException(e); ExceptionUtils.handleThrowable(e); throw new ServletException(sm.getString("filterChain.servlet"), e); } finally { if (ApplicationDispatcher.WRAP_SAME_OBJECT) { lastServicedRequest.set(null); lastServicedResponse.set(null); } } }
上述Demo中没有使用Spring security内容,所以关注点主要集中在internalDoFilter()
函数上,说的简单点就是不断调用数组中的过滤器,通过debug查看链的执行流程如下:
其中右侧的过滤器是MyChain自定义链,其他的characterEncodingFilter、hiddenHttpMethodFilter、formContentFilter、requestContextFilter过滤器是Spring框架内部帮我们自动实现的几个过滤器,不管Spring帮我们定义多少个过滤器,因为过滤器是Servlet规范中的,所以这些过滤器最终还有要汇总到Servelet容器中,对于上述情况具体来说,就是要汇总到ApplicationFilterChain(包位置:org.apache.catalina.core,嗯,更明白了),而Servlet把外部定义的过滤器(包括Spring框架定义的一些必要的过滤器)全部放到我们手动定义的过滤器链中,所以执行了2次,而且不仅仅是我们上面手写的SessionFilter和TestFilter2,还有Spring提供的过滤器实际都执行了2次。
解决方案:原因找了,理论上可以想到两种方案:一种是不要让Spring帮我们自定过滤器了,所有的过滤器都由自己实现管理,最后交给Servlet的过滤器链;第二种就是我们所有的过滤器都交给Spring管理,不直接和Filter发生联系,而是通过Spring间接和Filter联系,包括最后链的交接也由Spring和Filter去搞。
方案1
过滤器由自己管理,不通过IOC自动注入,手动new:
// 过滤器1,不加Spring注解 public class SessionFilter implements Filter { // 内容不变,省去... } // 过滤器2 public class TestFilter2 implements Filter { // 内容不变,省去... } // 封装过滤器List @Configuration public class FilterChainBean { @Bean(name = "allMyFilter") public List<Filter> registerFilter() { List<Filter> allMyFilter = new ArrayList<>(); // 通过 new 的方式加入 allMyFilter.add(new SessionFilter()); allMyFilter.add(new TestFilter2()); return allMyFilter; } } // 装链 @Service("myFilterChain") @Order(1) public class MyChain implements Filter { // 内容不变... }
捉摸着这样不就把ApplicationFilterChain
主链中的重复的sessionFilter
和testFilter2
去除了吗,只有自定义的链MyChain
中有这两个过滤器,实际发现,并没有那么简单,debug和结果如下:
我去,发现我自己写的2个过滤器没有起作用,然后找了一下,最后的发现:
主链中确实没有sessionFilter和testFilter2了,但期望发现List中的additionalFilters也没有这两个过滤器,搞得有点懵,为什么通过new的方式没有将上述两个过滤器放进List中,答案未知。。。
方案1:设置标志位
结合account的代码和网络资料以及OncePerRequestFilter源码发现,可以通过在过滤器中设置标志位来解决问题(GenericFilterBean是Spring框架对Filter的实现),代码如下:
// 过滤器1 @Component("sessionFilter") public class SessionFilter extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(SessionFilter.class); /** * 标识位,存入request中 */ private static final String FILTER_APPLIED = SessionFilter.class.getName() + ".FILTERED"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getAttribute(FILTER_APPLIED) != null) { // 如果已经执行过了就啥也不干,直接执行下一个过滤器,执行完直接返回 System.out.println("SessionFilter:非第一次执行,啥也不干。。。"); chain.doFilter(request, response); System.out.println("SessionFilter: 非第一次执行,下一个Filter执行完毕,即将结束扫尾工作。。。"); return; } else { // 设置已执行的标识位,放入request中 request.setAttribute(FILTER_APPLIED, true); // 实际业务处理... System.out.println("SessionFilter:第一次开始执行,可以在这里进行业务处理"); Cookie[] cookies = ((HttpServletRequest) request).getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals("loginUser")) { logger.info("find loginUser: " + cookie.getValue()); break; } } } chain.doFilter(request, response); System.out.println("SessionFilter: 第一次执行成功"); } } @Override public void destroy() { logger.info("come into SessionFilter destroy..."); } } // 过滤器2 @Component("testFilter") public class TestFilter2 extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(TestFilter2.class); /** * 标识符 */ private static final String FILTER_APPLIED = TestFilter2.class.getName() + ".FILTERED"; @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request.getAttribute(FILTER_APPLIED) != null) { // 如果已经执行过了就啥也不干,直接执行下一个过滤器,执行完直接返回 System.out.println("TestFilter2:非第一次执行,我啥也不干。。。"); chain.doFilter(request, response); System.out.println("TestFilter2:非第一次执行,下一个Filter执行完毕,即将结束扫尾工作。。。"); return; } else { // 设置已执行的标识位,放入request中 request.setAttribute(FILTER_APPLIED, true); // 实际业务处理... System.out.println("TestFilter2:第一次开始执行,可以在这里进行逻辑业务处理"); chain.doFilter(request, response); System.out.println("TestFilter2:初次执行成功"); } } @Override public void destroy() { logger.info("come into TestFilter2's destroy..."); } }
方案2:交给CompositeFilter
管理
在方案1中,无意间发现CompositeFilter
这个类,源码如下:
public class CompositeFilter implements Filter { private List<? extends Filter> filters = new ArrayList<>(); public void setFilters(List<? extends Filter> filters) { this.filters = new ArrayList<>(filters); } @Override public void init(FilterConfig config) throws ServletException { for (Filter filter : this.filters) { filter.init(config); } } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { new VirtualFilterChain(chain, this.filters).doFilter(request, response); } @Override public void destroy() { for (int i = this.filters.size(); i-- > 0;) { Filter filter = this.filters.get(i); filter.destroy(); } } private static class VirtualFilterChain implements FilterChain { private final FilterChain originalChain; private final List<? extends Filter> additionalFilters; private int currentPosition = 0; public VirtualFilterChain(FilterChain chain, List<? extends Filter> additionalFilters) { this.originalChain = chain; this.additionalFilters = additionalFilters; } @Override public void doFilter(final ServletRequest request, final ServletResponse response) throws IOException, ServletException { if (this.currentPosition == this.additionalFilters.size()) { this.originalChain.doFilter(request, response); } else { this.currentPosition++; Filter nextFilter = this.additionalFilters.get(this.currentPosition - 1); nextFilter.doFilter(request, response, this); } } } }
发现猜测它可以帮我们管理手写的过滤器,唯一要做的就是将过滤器通过setFilters()
方法塞进去就好了,会自动封装一个虚拟链(之前自定义的封装连代码可以直接丢弃),详细代码如下:
// 过滤器1 public class SessionFilter extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(SessionFilter.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 实际业务处理... System.out.println("SessionFilter:开始执行,可以在这里进行业务处理"); Cookie[] cookies = ((HttpServletRequest) request).getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookie.getName().equals("loginUser")) { logger.info("find loginUser: " + cookie.getValue()); break; } } } chain.doFilter(request, response); System.out.println("SessionFilter: 执行成功"); } @Override public void destroy() { logger.info("come into SessionFilter destroy..."); } } // 过滤器2 public class TestFilter2 extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(TestFilter2.class); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 实际业务处理... System.out.println("TestFilter2:开始执行,可以在这里进行逻辑业务处理"); chain.doFilter(request, response); System.out.println("TestFilter2:执行成功"); } @Override public void destroy() { logger.info("come into TestFilter2's destroy..."); } } // 配置符合过滤器(无需手写虚拟链,全部塞进CompositeFilter即可自动封装,无需再写MyChain) @Configuration public class FilterChainBean { @Bean("myChain") public CompositeFilter addFilterInChain() { List<Filter> allMyFilter = new ArrayList<>(); allMyFilter.add(new SessionFilter()); allMyFilter.add(new TestFilter2()); CompositeFilter compositeFilter = new CompositeFilter(); compositeFilter.setFilters(allMyFilter); return compositeFilter; } }