前言
在日常web开发中,我们经常会使用到Filter,这个组件最经典的使用场景就是鉴权。比如现在的JWT鉴权模式,所有的请求都应该携带一个Token,然后我们在Filter里去调用Service方法去校验这个Token是否合法,从而达到鉴权的目的。
但是不乏很多人问题就来了:我们使用Spring注入的方式在Filter里注入的时候,竟然是null,从而就悲剧–>空指针~
本文主要从根本原因上讲述,为何这样直接注入不好使已经在Spring Framework环境下的解决方案~
案例构造
为了讲述方便(其实是偷懒),我基于这篇博文的项目哈:【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,全注解驱动)
使用的是最传统的Spring环境(非Boot环境)下,使用Filter:
@WebFilter(urlPatterns = "/hello") public class HelloFilter implements Filter { @Autowired private HelloService helloService; @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("init..."); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.out.println(helloService); chain.doFilter(request, response); } @Override public void destroy() { System.out.println("init..."); } }
其中controller写一个如下,也注入这个service
@Controller public class HelloController { @Autowired private HelloService helloService; @ResponseBody @RequestMapping("/hello") public String hello() { System.out.println(helloService); return "hello..."; } }
我直接请求http://localhost:8080/demowar_war/hello发现:
Filter里输出的为null,Controller输出的为com.fsx.service.HelloServiceImpl@5cd533d9。所以在Filter里我们没有注入进去,怎么回事呢?
在Spring MVC的interceptor拦截器里注入,是没有任何问题的。所以很多小伙伴就直接采用interceptor去实现了。虽然说也能达到效果,但是个人并不建议这么做,因为从职责分析上来看,使用Filter鉴权才是最佳实践~
原因解读
因为Spring bean、filter、interceptor加载顺序与它们在 web.xml 文件中的先后顺序无关。即不会因为 filter 写在 listener 的前面而会先加载 filter。
组件的加载顺序应该是: ServletContext -> Listener -> Filter -> Servlet
由于Spring bean的初始化是在listener中声明的,因此Filter时,Spring bean已经实例。 既然已经实例化了,怎么证明呢?且看我写的下面代码,在Filter里加上这么一句:
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()); HelloService bean = applicationContext.getBean(HelloService.class); System.out.println(bean); //com.fsx.service.HelloServiceImpl@5cd533d9 System.out.println(helloService); //null chain.doFilter(request, response); }
从输出的内容里可以看出,其实doFilter的时候,该Bean已经存在于该容器内了,只是没有@Autowired进来而已。
所以根本原因是:
过滤器是servlet规范中定义的,并不归Spring容器管理,也无法直接注入spring中的Bean
有了这个解释,小伙伴们就很好理解为何你在Spring Boot环境下使用Filter时,都可以直接@Autowired注入Service了,因为Boot环境下,三大组件都是以Spring Bean的形式存在于容器内的~
解决方案:
问题就来了,现在我的项目较老,就是传统的Spring环境,怎么办呢?既然我们都已经知道容器里面有这个bean了,所以方案自然有多种,下面介绍几种:
方案一:
在Filter初始化方法里手动去容器里吧Bean拿出来即可~
@Override public void init(FilterConfig filterConfig) throws ServletException { ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext()); helloService = applicationContext.getBean(HelloService.class); }
缺点:稍微显得有点麻烦累赘,但我个人认为其实还好~
===============================
此处切忌,不要想着直接Filter交给容器管理就行,这么来:
@Component public class HelloFilter implements Filter {} WebApplicationContextUtils.getWebApplicationContext(request.getServletContext()).getBean(HelloFilter.class); //com.fsx.filter.HelloFilter@1b4dcbf5 Spring容器里是存在HelloFilter这个Bean的
这样是不好使的,因为这样子在Spring容器里的Filter和Servlet容器里的不是同一个Bean,没用的。
但下面这样方案是能正常work的,因为它是同一个Bean:
方案二:DelegatingFilterProxy
DelegatingFilterProxy类存在与spring-web包中,其作用就是一个filter的代理,用这个类的好处是可以通过spring容器来管理filter的生命周期(比如shiro里面的Filter用到了它)。这样如果filter中需要一些Spring容器的实例,可以通过spring直接注入
使用它的好处:主要的目的还是我们添加的过滤器,需要使用spring中的某些bean,即委托Spring来管理过滤器的生命周期
默认情况下, Spring 会到 IOC 容器中查找和 <filter-name> 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id(建议指定)
DelegatingFilterProxy类继承GenericFilterBean,间接实现了Filter这个接口,故而该类属于一个过滤器。
使用方式:
若我们使用的仍然是web.xml方式,那我们这么配置就行:
<filter> <filter-name>testFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <!-- 保留Filter原有的init,destroy方法的调用,还需要配置初始化参数targetFilterLifecycle为true,该参数默认为false --> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> <!-- 查找的指定的Bean的id,然后拿出来代理 --> <init-param> <param-name>targetBeanName</param-name> <param-value>helloFilter</param-value> </init-param> </filter> <filter-mapping> <filter-name>testFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
然后吧自定义的Filter加上@Component扫描进容器里面即可~奈何我们现在流行的全注解去驱动,怎么配置Filter呢?(备注:此filter为jar包里面的Filter,配置起来显得不便)
本来想着在SpringMVC启动器里面进行操作,但是人家都放在jar包里面了,你还拿出来改,显然不太合适。所以我依照监听器早于Filter加载的特性,在监听器里面写一个,然后手动把Filter放进去:
@WebListener public class HelloListener implements ServletContextListener { @Override public void contextInitialized(ServletContextEvent sce) { ServletContext servletContext = sce.getServletContext(); //为helloFilter条件一个代理的Filter DelegatingFilterProxy filterProxy = new DelegatingFilterProxy(); filterProxy.setTargetBeanName("helloFilter"); filterProxy.setTargetFilterLifecycle(true); FilterRegistration.Dynamic delegatingFilterProxy = servletContext.addFilter("helloFilterProxy", filterProxy); delegatingFilterProxy.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), true, "/*"); } @Override public void contextDestroyed(ServletContextEvent sce) { } }
这样子,我们的Filter就被代理成功了,我们只需要吧我们的Filter交给Spring管理即可(注意:此处的bean的id和setTargetBeanName
要对应上):
@Component("helloFilter") public class HelloFilter implements Filter { ... }
方案三:
@WebFilter(urlPatterns = "/*", initParams = { @WebInitParam(name = "targetFilterLifecycle", value = "true"), @WebInitParam(name = "targetBeanName", value = "helloFilter")}) public class HelloFilterProxy extends DelegatingFilterProxy { }
这样也是欧克的。这种方法可以很巧妙的吧jar包里面的类提取出来。
纯个人认为,这个方案还是非常不错的,类级别的一一对应,比较清晰点。
特别注意
当使用DelegatingFilterProxy来代理的时候,请务必保证bean的id是能对应上的,否则会找不到这个Filter,源码如下:
protected Filter initDelegate(WebApplicationContext wac) throws ServletException { String targetBeanName = getTargetBeanName(); Assert.state(targetBeanName != null, "No target bean name set"); Filter delegate = wac.getBean(targetBeanName, Filter.class); if (isTargetFilterLifecycle()) { delegate.init(getFilterConfig()); } return delegate; }
当一个Filter都没找到,并且还有代理Filter的时候,那就会出现各种莫名其妙的问题~
方案四:自定义一个ServletContainerInitializer的子类,然后再启动的时候完成注册(也推荐)
总结
上面两种方案均可,至于选取哪种方案,就仁者见仁了。
另外,有小伙伴说没有说在Spring Boot环境下的没有说明,其实这个看这篇博文就够了:
【小家Spring】SpringBoot中使用Servlet、Filter、Listener三大组件的三种方式以及原理剖析
知其然,知其所以然。更好的了解了Spring Framework,对我们使用Spring Boot会更加的顺畅~