zoukankan      html  css  js  c++  java
  • SpringBoot 中拦截器和过滤器的使用

    主要了解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主链中的重复的sessionFiltertestFilter2去除了吗,只有自定义的链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;
        }
    }
    

      

  • 相关阅读:
    深入Spring Security魔幻山谷-获取认证机制核心原理讲解
    灵魂拷问:你真的理解System.out.println()打印原理吗?
    解决分布式React前端在开发环境的跨域问题
    “我以为”与“实际上”
    前端:如何让background背景图片进行CSS自适应
    VSCode如何设置Vue前端的debug调试
    【JDBC】总结
    【JDBC第9章】Apache-DBUtils实现CRUD操作
    【JDBC第8章】数据库连接池
    【JDBC第7章】DAO及相关实现类
  • 原文地址:https://www.cnblogs.com/jtlgb/p/13391687.html
Copyright © 2011-2022 走看看