主要了解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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | // 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项目一样,启动项目即可,最初启动(即不主动调用接口)的效果如下:
1 2 3 4 5 6 7 8 9 10 | 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 ) |
在调用接口后,添加了如下日志:
1 2 3 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 | // 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可以应用这个拦截器,如下:
1 2 3 | // 使用自动注入的方式注入拦截器,添加应用、或不应用该拦截器的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接口中的常用方法有如下使用示例,可以选择性重写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 | @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中使用过滤器的基本流程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | 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注解):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 | // 过滤器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()
的源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 | @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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // 过滤器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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | // 过滤器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
这个类,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 | 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 | // 过滤器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; } } |
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 字符编码:从基础到乱码解决
2017-07-28 mysql优化连接数
2016-07-28 CentOS中JAVA_HOME的环境变量设置
2016-07-28 用Navicat for MySQL 连接 CentOS 6.5
2016-07-28 CentOS上开启MySQL远程访问权限
2016-07-28 centos7下yum安装mysql
2016-07-28 CentOS7下解决yum install mysql-server没有可用包