jtlgb

导航

统计
 

主要了解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;
    }
}

  

posted on 2020-07-28 16:10  jtlgb  阅读(88)  评论(0编辑  收藏