【小家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会更加的顺畅~

相关文章
|
3月前
|
Java Spring
在使用Spring的`@Value`注解注入属性值时,有一些特殊字符需要注意
【10月更文挑战第9天】在使用Spring的`@Value`注解注入属性值时,需注意一些特殊字符的正确处理方法,包括空格、引号、反斜杠、新行、制表符、逗号、大括号、$、百分号及其他特殊字符。通过适当包裹或转义,确保这些字符能被正确解析和注入。
205 3
|
1月前
|
Java Spring
一键注入 Spring 成员变量,顺序编程
介绍了一款针对Spring框架开发的插件,旨在解决开发中频繁滚动查找成员变量注入位置的问题。通过一键操作(如Ctrl+1),该插件可自动在类顶部添加`@Autowired`注解及其成员变量声明,同时保持光标位置不变,有效提升开发效率和代码编写流畅度。适用于IntelliJ IDEA 2023及以上版本。
一键注入 Spring 成员变量,顺序编程
|
22天前
|
XML Java 数据格式
Spring容器Bean之XML配置方式
通过对以上内容的掌握,开发人员可以灵活地使用Spring的XML配置方式来管理应用程序的Bean,提高代码的模块化和可维护性。
57 6
|
1月前
|
安全 Java 开发者
Spring容器中的bean是线程安全的吗?
Spring容器中的bean默认为单例模式,多线程环境下若操作共享成员变量,易引发线程安全问题。Spring未对单例bean做线程安全处理,需开发者自行解决。通常,Spring bean(如Controller、Service、Dao)无状态变化,故多为线程安全。若涉及线程安全问题,可通过编码或设置bean作用域为prototype解决。
35 1
|
2月前
|
前端开发 Java Docker
使用Docker容器化部署Spring Boot应用程序
使用Docker容器化部署Spring Boot应用程序
|
2月前
|
Java Docker 微服务
利用Docker容器化部署Spring Boot应用
利用Docker容器化部署Spring Boot应用
58 0
|
1月前
|
监控 NoSQL 时序数据库
《docker高级篇(大厂进阶):7.Docker容器监控之CAdvisor+InfluxDB+Granfana》包括:原生命令、是什么、compose容器编排,一套带走
《docker高级篇(大厂进阶):7.Docker容器监控之CAdvisor+InfluxDB+Granfana》包括:原生命令、是什么、compose容器编排,一套带走
232 77
|
2天前
|
Ubuntu NoSQL Linux
《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结
《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结
41 6
《docker基础篇:3.Docker常用命令》包括帮助启动类命令、镜像命令、有镜像才能创建容器,这是根本前提(下载一个CentOS或者ubuntu镜像演示)、容器命令、小总结
|
1月前
|
监控 Docker 容器
在Docker容器中运行打包好的应用程序
在Docker容器中运行打包好的应用程序
|
13天前
|
Ubuntu Linux 开发工具
docker 是什么?docker初认识之如何部署docker-优雅草后续将会把产品发布部署至docker容器中-因此会出相关系列文章-优雅草央千澈
Docker 是一个开源的容器化平台,允许开发者将应用程序及其依赖项打包成标准化单元(容器),确保在任何支持 Docker 的操作系统上一致运行。容器共享主机内核,提供轻量级、高效的执行环境。本文介绍如何在 Ubuntu 上安装 Docker,并通过简单步骤验证安装成功。后续文章将探讨使用 Docker 部署开源项目。优雅草央千澈 源、安装 Docker 包、验证安装 - 适用场景:开发、测试、生产环境 通过以上步骤,您可以在 Ubuntu 系统上成功安装并运行 Docker,为后续的应用部署打下基础。
docker 是什么?docker初认识之如何部署docker-优雅草后续将会把产品发布部署至docker容器中-因此会出相关系列文章-优雅草央千澈