前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >【小家Spring】Spring环境中(含Boot环境),web组件(Servlet、Filter)内注入使用Spring容器里的Bean

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

作者头像
YourBatman
发布2019-09-03 16:41:50
2.2K0
发布2019-09-03 16:41:50
举报
文章被收录于专栏:BAT的乌托邦BAT的乌托邦

前言

在日常web开发中,我们经常会使用到Filter,这个组件最经典的使用场景就是鉴权。比如现在的JWT鉴权模式,所有的请求都应该携带一个Token,然后我们在Filter里去调用Service方法去校验这个Token是否合法,从而达到鉴权的目的。

但是不乏很多人问题就来了:我们使用Spring注入的方式在Filter里注入的时候,竟然是null,从而就悲剧–>空指针~

本文主要从根本原因上讲述,为何这样直接注入不好使已经在Spring Framework环境下的解决方案~

案例构造

为了讲述方便(其实是偷懒),我基于这篇博文的项目哈:【小家Spring】Spring注解驱动开发—Servlet 3.0整合Spring MVC(不使用web.xml部署描述符,全注解驱动)

使用的是最传统的Spring环境(非Boot环境)下,使用Filter

代码语言:javascript
复制
@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

代码语言:javascript
复制
@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里加上这么一句:

代码语言:javascript
复制
    @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拿出来即可~

代码语言:javascript
复制
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        ApplicationContext applicationContext = WebApplicationContextUtils.getWebApplicationContext(filterConfig.getServletContext());
        helloService = applicationContext.getBean(HelloService.class);
    }

缺点:稍微显得有点麻烦累赘,但我个人认为其实还好~

===============================

此处切忌,不要想着直接Filter交给容器管理就行,这么来:

代码语言:javascript
复制
@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 会到 IOC 容器中查找和 <filter-name> 对应的 filter bean. 也可以通过 targetBeanName 的初始化参数来配置 filter bean 的 id(建议指定)

DelegatingFilterProxy类继承GenericFilterBean,间接实现了Filter这个接口,故而该类属于一个过滤器。

使用方式:

若我们使用的仍然是web.xml方式,那我们这么配置就行:

代码语言:javascript
复制
    <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放进去:

代码语言:javascript
复制
@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要对应上):

代码语言:javascript
复制
@Component("helloFilter")
public class HelloFilter implements Filter { ... }
方案三:
代码语言:javascript
复制
@WebFilter(urlPatterns = "/*", initParams = {
        @WebInitParam(name = "targetFilterLifecycle", value = "true"),
        @WebInitParam(name = "targetBeanName", value = "helloFilter")})
public class HelloFilterProxy extends DelegatingFilterProxy {

}

这样也是欧克的。这种方法可以很巧妙的吧jar包里面的类提取出来。

纯个人认为,这个方案还是非常不错的,类级别的一一对应,比较清晰点。

特别注意

当使用DelegatingFilterProxy来代理的时候,请务必保证bean的id是能对应上的,否则会找不到这个Filter,源码如下:

代码语言:javascript
复制
	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会更加的顺畅~

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019年02月19日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 案例构造
  • 原因解读
  • 解决方案:
    • 方案一:
      • 方案二:DelegatingFilterProxy
        • 方案三:
          • 特别注意
            • 方案四:自定义一个ServletContainerInitializer的子类,然后再启动的时候完成注册(也推荐)
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档