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

本文涉及的产品
容器镜像服务 ACR,镜像仓库100个 不限时长
简介: 【小家Spring】Spring环境中(含Boot环境),web组件(Servlet、Filter)内注入使用Spring容器里的Bean

前言


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

相关文章
|
1月前
|
Java 应用服务中间件 Maven
SpringBoot 项目瘦身指南
SpringBoot 项目瘦身指南
46 0
|
1月前
|
XML Java 开发者
Spring Boot中的bean注入方式和原理
Spring Boot中的bean注入方式和原理
80 0
|
3月前
|
Java 程序员 Spring
Spring5深入浅出篇:Spring对象属性注入详解
Spring5深入浅出篇:Spring对象属性注入详解
|
5天前
|
Java Spring 容器
Spring注入
Spring注入
24 13
|
12天前
|
安全 Java 应用服务中间件
江帅帅:Spring Boot 底层级探索系列 03 - 简单配置
江帅帅:Spring Boot 底层级探索系列 03 - 简单配置
26 0
江帅帅:Spring Boot 底层级探索系列 03 - 简单配置
|
14天前
|
XML Java C++
【Spring系列】Sping VS Sping Boot区别与联系
【4月更文挑战第2天】Spring系列第一课:Spring Boot 能力介绍及简单实践
【Spring系列】Sping VS Sping Boot区别与联系
|
17天前
|
Unix Java 编译器
Servlet 教程 之 Servlet 环境设置 3
在Windows和Unix上配置Servlet环境涉及设置CLASSPATH。在Windows的`autoexec.bat`或&quot;环境变量&quot;中添加CATALINA路径和`servlet-api.jar`。在Unix的`.cshrc`文件中设置相同环境变量。别忘了将开发目录也添加到CLASSPATH。例如:`C:\ServletDevel`(Windows)或`/user/ServletDevel`(UNIX)。
21 5
|
17天前
|
JSON Java 数据库连接
属性注入掌握:Spring Boot配置属性的高级技巧与最佳实践
属性注入掌握:Spring Boot配置属性的高级技巧与最佳实践
27 1
|
1月前
|
XML Java 数据格式
解释Spring中一个bean的注入过程
解释Spring中一个bean的注入过程
|
2月前
|
Java Spring
Spring 使用注解注入失败,调用内容时提示空指针
Spring 使用注解注入失败,调用内容时提示空指针
15 0