深入剖析自定义Filter中使用@Autowired无法注入依赖

一、发现问题

在项目中,自定义了一个Filter,想要做一些权限控制。本来是一个很常规的操作,但是奇怪的事情发生了,使用@Autowired注入依赖的时候,报了空指针异常,那就说明依赖没注入。

二、分析问题

1.如何使用自定义Filter

在分析问题之前,先复习下自定义Filter的使用:

  • 创建类实现Filter接口
  • 在web.xml文件中配置过滤器

2.依赖为什么没有注入

通过@Autowired无法注入依赖,于是我想到了在代码中通过spring上下文context获取,代码如下:

	<filter>
        <filter-name>authFilter2</filter-name>
        <filter-class>com.tyd.filter.AuthFilter2</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>authFilter2</filter-name>
        <url-pattern>*.do</url-pattern>
    </filter-mapping>
package com.tyd.filter;

import com.tyd.bean.UserBean;
import com.tyd.utils.ApplicationContextUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import java.io.IOException;

@Component
public class AuthFilter2 implements Filter {

    @Autowired
    private UserBean userBean;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("###authFilter2 userBean: " + userBean);
        ApplicationContext context = ApplicationContextUtils.getContext();

        UserBean userBean2 = context.getBean("userBean", UserBean.class);
        System.out.println("###authFilter2 userBean2: " + userBean2);
        
        filterChain.doFilter(servletRequest, servletResponse);
    }

    @Override
    public void destroy() {

    }
}

图一
结果显示,通过spring上下文是可以获取到依赖对象的,但是通过注解却不行,这是为什么呢?
网上搜索了一些答案,说是注入这个依赖的时候,spring容器还没初始化好,所以就注入了个null,听起来好像是有那么些道理。
我们暂且不管这个结论的对错,不妨换个思路:假设浏览器发起了请求,请求顺利进入到我们的过滤器,那么这个时候,我们应用里的所有初始化工作,包括spring容器的初始化是不是都已经完成了,那肯定是的。那么我们是不是可以获取到spring容器中的当前AuthFilter2对象,用它来跟当前拦截的这个AuthFilter2过滤器对象做比较,判断一下是否是同一个对象。
在这里插入图片描述
结果显示,拦截请求的AuthFilter2对象跟spring容器中的对象不是同一个对象。那这就很好解释了,并不是依赖没有注入,而是拦截请求的这个过滤器对象不是我们代码中用@Component注解通过spring容器创建的。
反复调了几次接口,发现一个问题,就是上面提到的这个拦截请求的过滤器对象,它的引用地址并没有发生变化,即不管什么请求多少次,都是同一个对象拦截。那么这个对象是怎么来的呢?

3.使用DelegatingFilterProxy,调用的就是同一个对象

项目中还用到了这个东西:org.springframework.web.filter.DelegatingFilterProxy
我们测试一下使用DelegatingFilterProxy的效果:
更改web.xml配置:

	<filter>
        <filter-name>authFilter2</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>authFilter2</filter-name>
        <url-pattern>*.do</url-pattern>
    </filter-mapping>

java代码保持不变,重新启动项目。
在这里插入图片描述
结果显示,使用了DelegatingFilterProxy之后,调用的就是同一个对象了,即调用的都是spring容器创建的对象,并且依赖也都有了。那么这个DelegatingFilterProxy里面做了什么事情呢?

4.分析DelegatingFilterProxy原理

分析发现DelegatingFilterProxy继承了GenericFilterBean,而GenericFilterBean实现了Filter,那说明DelegatingFilterProxy其实是一个过滤器,那就看它的doFilter方法:
在这里插入图片描述
在这里插入图片描述
我们调试下代码发现,这个delegate其实就是AuthFilter2,所以最终调用的就是AuthFilter2里面的doFilter方法,这里其实是一个代理模式的应用。
在这里插入图片描述
那么问题来了,这个delegate是什么时候初始化的呢?

5.深入分析(全篇之精华)

通过以上分析,我们知道了以下结论:

  • 在web.xml中配置的自定义Filter跟通过注解申明的Filter,不是同一个对象
  • 使用DelegatingFilterProxy代理之后,获取到的Filter delegate就是spring容器中的对象

这里再说一下DelegatingFilterProxy,如果抽去里面具体的逻辑实现,这个其实就是一个普通的过滤器,具体使用的话,过滤器名称要跟代理的过滤器在spring容器中的名称一致(此处为authFilter2),或者配置上 targetBeanName

	<filter>
        <filter-name>authFilter3</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>authFilter2</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>authFilter3</filter-name>
        <url-pattern>*.do</url-pattern>
    </filter-mapping>

接下来,我们还需要解决这些问题:

  • DelegatingFilterProxy什么时候初始化的?
  • DelegatingFilterProxy里面的delegate属性什么时候赋值的?

我们通过嵌入式tomcat去跟踪Filter的创建,spring容器的启动,以及DelegatingFilterProxy里面的delegate属性的赋值。具体tomcat的使用请参考 嵌入式tomcat的使用.

import org.apache.catalina.startup.Tomcat;

import java.io.File;

public class ApplicationStart {

    private static void startSimpleTomcat() throws Exception {
        Tomcat tomcat = new Tomcat();
        tomcat.setPort(8090);
        String contextPath = "/myTest";
        String docBase = new File("").getAbsolutePath() + "/src/main/webapp";
        tomcat.addWebapp(contextPath, docBase);
        tomcat.start();
        tomcat.getServer().await();
    }

    public static void main(String[] args) throws Exception {
        ApplicationStart.startSimpleTomcat();
    }
}

  • 解析web.xml配置文件,把filter、listener、servlet等定义信息赋值给StandardContext context
    filter --> FilterDef
    listener --> String
    servlet --> ServletDef
org.apache.catalina.startup.ContextConfig#configureStart
org.apache.catalina.startup.ContextConfig#webConfig
org.apache.catalina.startup.ContextConfig#configureContext
  • 调用context的listenerStart,触发web.xml里面配置的监听器,然后启动spring
org.apache.catalina.core.StandardContext#listenerStart
org.springframework.web.context.ContextLoaderListener#contextInitialized
org.springframework.web.context.ContextLoader#configureAndRefreshWebApplicationContext
org.springframework.context.ConfigurableApplicationContext#refresh
  • 调用context的filterStart,创建ApplicationFilterConfig,如果filterDef里面的filter为空(第一次是为空的),则通过反射创建,DelegatingFilterProxy就是这个时候创建的
    然后调用过滤器的init方法初始化,delegate就是这个时候赋值的,且是从spring容器中获取的
org.apache.catalina.core.StandardContext#filterStart
org.apache.catalina.core.ApplicationFilterConfig#ApplicationFilterConfig
org.apache.catalina.core.ApplicationFilterConfig#getFilter
org.apache.catalina.core.ApplicationFilterConfig#initFilter
org.springframework.web.filter.DelegatingFilterProxy#initFilterBean

在这里插入图片描述

6.插曲(尴尬)

本来到这里应该是要完结了,但是意外发生了,再次启动项目的时候,发现spring容器没有启动,并且在进行代理的filter获取的时候也还是没有获取到。
在这里插入图片描述
这时候注意到几行注释:
Fetch Spring root application context and initialize the delegate early, if possible. If the root application context will be started after this filter proxy, we’ll have to resort to lazy initialization.
如果可能的话,获取Spring根应用程序上下文并早一些初始化delegate。如果根应用程序上下文将在这个过滤器代理之后启动,我们将不得不采用延迟初始化。

最后,再梳理一下整个流程:
浏览器发起请求,tomcat开启一个线程处理请求,然后tomcat从上下文取到filter配置信息ApplicationFilterConfig,
从而拿到DelegatingFilterProxy(但是此时delegate有可能是空的,也有可能是初始化好的,这取决于之前创建过滤器的时候spring容器是否已经启动),调用delegate.doFilter

org.apache.catalina.core.ApplicationFilterChain#internalDoFilter
org.springframework.web.filter.DelegatingFilterProxy#doFilter

三、解决问题

通过以上分析,解决方法有两种:

  1. 使用自定义Filter时,不能通过注解注入依赖,只能获取spring上下文,通过getBean方法获取
  2. 使用DelegatingFilterProxy代理,此时可以正常使用注解注入依赖

遗留问题

spring容器启动跟tomcat中过滤器的初始化是并发执行的?

补充

这部分是对于插曲部分,和遗留问题的解答。很尴尬,
不得不承认自己的粗心,之所以出现了插曲,是因为监听器类配置错了,所以没有触发监听器里面的方法,就没有在Filter初始化之前启动spring容器。

于是spring容器就在springmvc的DispatcherServlet初始化的时候启动了,servlet初始化是在filter之后的。这种情况下,delegate就不是在tomcat启动过程中赋值的,而是在第一次请求的时候。更极端的,如果DispatcherServlet没有配置<load-on-startup>1</load-on-startup>,那么spring容器将在第一次请求的时候创建。

所以spring容器启动跟tomcat中过滤器的初始化并不是并发执行的,可以是同步执行,也可以是延迟创建。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值