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