【小家Spring】Spring环境中(含Boot环境),web组件(Servlet、Filter)内注入使用Spring容器里的Bean

每篇一句

编程不规范,同事两行泪

相关阅读

【小家Spring】Spring IOC容器启动流程 AbstractApplicationContext#refresh()方法源码分析(一)
【小家Spring】Spring IOC容器启动流程 AbstractApplicationContext#refresh()方法源码分析(二)
【小家Spring】AbstractBeanFactory#getBean()、doGetBean完成Bean的初始化、实例化,以及BeanPostProcessor后置处理器源码级详细分析
【小家Spring】AbstractAutowireCapableBeanFactory#populateBean实现Bean的依赖注入(属性赋值)和initializeBean对Bean的初始化
【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)

【小家Spring】细说Spring IOC容器的自动装配(@Autowired),以及Spring4.0新特性之【泛型依赖注入】的源码级解析
【小家Spring】BeanFactory体系和ApplicationContext体系,两大体系各接口分析、区别和联系

前言

在日常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会更加的顺畅~

知识交流

在这里插入图片描述
若群二维码失效,请加微信号(或者扫描下方二维码):fsx641385712。
并且备注:“java入群” 字样,会手动邀请入群

在这里插入图片描述

发布了377 篇原创文章 · 获赞 572 · 访问量 51万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: Age of Ai 设计师: meimeiellie

分享到微信朋友圈

×

扫一扫,手机浏览