Day613.SpringWebFilter常见错误① -Spring编程常见错误

Posted 阿昌喜欢吃黄桃

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Day613.SpringWebFilter常见错误① -Spring编程常见错误相关的知识,希望对你有一定的参考价值。

SpringWebFilter常见错误①

在SpringWeb开发中,Filter必然是一个十分重要的角色。

他可以对于一个请求进行鉴权、日志等操作。

那如下记录了你可能在Filter编程中会出现的常见错误。


一、@WebFilter 过滤器无法被自动注入

我们为一个系统定义一个Filter,其功能为了统计接口耗时代码如下:

@WebFilter
@Slf4j
public class TimeCostFilter implements Filter 
    public TimeCostFilter()
        System.out.println("construct");
    
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
        log.info("开始计算接口耗时");
        long start = System.currentTimeMillis();
        chain.doFilter(request, response);
        long end = System.currentTimeMillis();
        long time = end - start;
        System.out.println("执行时间(ms):" + time);
    

这个过滤器标记了 @WebFilter。所以在启动程序中,我们需要加上扫描注解(即 @ServletComponentScan)让其生效,启动程序如下:

@SpringBootApplication
@ServletComponentScan
@Slf4j
public class Application 
    public static void main(String[] args) 
        SpringApplication.run(Application.class, args);
        log.info("启动成功");
    

然后,我们提供了一个 StudentController 接口来供学生注册:

@Controller
@Slf4j
public class StudentController 
   
    @PostMapping("/regStudent/name")
    @ResponseBody
    public String saveUser(String name) throws Exception 
        System.out.println("用户注册成功");
        return "success";
    

上述程序完成后,你会发现一切按预期执行。

但是假设有一天,我们可能需要把 TimeCostFilter 记录的统计数据输出到专业的度量系统(ElasticeSearch/InfluxDB 等)里面去,我们可能会添加这样一个 Service 类:

@Service
public class MetricsService 
    @Autowired
    public TimeCostFilter timeCostFilter;
    //省略其他非关键代码

完成后你会发现,Spring Boot 都无法启动了:


APPLICATION FAILED TO START


Description:
Field timeCostFilter in com.spring.puzzle.web.filter.example1.MetricsService required a bean of type ‘com.spring.puzzle.web.filter.example1.TimeCostFilter’ that could not be found.

那为什么针对一个Filter注入到对应Bean中会找不到呢??


本质上,过滤器被 @WebFilter 修饰后,TimeCostFilter 只会被包装为 FilterRegistrationBean,而 TimeCostFilter 自身,只会作为一个 InnerBean 被实例化,这意味着 TimeCostFilter 实例并不会作为 Bean 注册到 Spring 容器

知道这个后,我们可以带着两个问题去理清一些关键的逻辑:

FilterRegistrationBean 是什么?它是如何被定义的?TimeCostFilter 是怎么实例化,并和 FilterRegistrationBean 关联起来的?


我们先来看第一个问题:FilterRegistrationBean 是什么?它是如何定义的?实际上,WebFilter 的全名是 javax.servlet.annotation.WebFilter,很明显,它并不属于 Spring,而是 Servlet 的规范。

当 Spring Boot 项目中使用它时,Spring Boot 使用了 org.springframework.boot.web.servlet.FilterRegistrationBean 来包装 @WebFilter 标记的实例。

从实现上来说,即 FilterRegistrationBean#Filter 属性就是 @WebFilter 标记的实例。

这点我们可以从之前给出的截图中看出端倪。另外,当我们定义一个 Filter 类时,我们可能想的是,我们会自动生成它的实例,然后以 Filter 的名称作为 Bean 的名字来指向它。但是调试下你会发现,在 Spring Boot 中,Bean 名字确实是对的,只是 Bean 实例其实是 FilterRegistrationBean

那么这个 FilterRegistrationBean 最早是如何获取的呢?这还得追溯到 @WebFilter 这个注解是如何被处理的。在具体解析之前,我们先看下 @WebFilter 是如何工作起来的。使用 @WebFilter 时,Filter 被加载有两个条件:

  • 声明了 @WebFilter
  • 在能被 @ServletComponentScan 扫到的路径之下。

这里我们直接检索对 @WebFilter 的使用,可以发现 WebFilterHandler 类使用了它,直接在 doHandle() 中加入断点,开始调试,执行调用栈如下:

从堆栈上,我们可以看出对 @WebFilter 的处理是在 Spring Boot 启动时,而处理的触发点是 ServletComponentRegisteringPostProcessor 这个类。

它继承了 BeanFactoryPostProcessor 接口,实现对 @WebFilter@WebListener@WebServlet 的扫描和处理。

其中对于 @WebFilter 的处理使用的就是上文中提到的 WebFilterHandler。这个逻辑可以参考下面的关键代码:

class ServletComponentRegisteringPostProcessor implements BeanFactoryPostProcessor, ApplicationContextAware 
   private static final List<ServletComponentHandler> HANDLERS;
   static 
      List<ServletComponentHandler> servletComponentHandlers = new ArrayList<>();
      servletComponentHandlers.add(new WebServletHandler());
      servletComponentHandlers.add(new WebFilterHandler());
      servletComponentHandlers.add(new WebListenerHandler());
      HANDLERS = Collections.unmodifiableList(servletComponentHandlers);
   
   // 省略非关键代码
   @Override
   public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException 
      if (isRunningInEmbeddedWebServer()) 
         ClassPathScanningCandidateComponentProvider componentProvider = createComponentProvider();
         for (String packageToScan : this.packagesToScan) 
            scanPackage(componentProvider, packageToScan);
         
      
   
   
  private void scanPackage(ClassPathScanningCandidateComponentProvider componentProvider, String packageToScan) 
     // 扫描注解
     for (BeanDefinition candidate : componentProvider.findCandidateComponents(packageToScan)) 
        if (candidate instanceof AnnotatedBeanDefinition) 
           // 使用 WebFilterHandler 等进行处理
           for (ServletComponentHandler handler : HANDLERS) 
              handler.handle(((AnnotatedBeanDefinition) candidate),
                    (BeanDefinitionRegistry) this.applicationContext);
           
        
     
  

最终,WebServletHandler 通过父类 ServletComponentHandler 的模版方法模式,处理了所有被 @WebFilter 注解的类,关键代码如下:

public void doHandle(Map<String, Object> attributes, AnnotatedBeanDefinition beanDefinition,
      BeanDefinitionRegistry registry) 
   BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(FilterRegistrationBean.class);
   builder.addPropertyValue("asyncSupported", attributes.get("asyncSupported"));
   builder.addPropertyValue("dispatcherTypes", extractDispatcherTypes(attributes));
   builder.addPropertyValue("filter", beanDefinition);
   //省略其他非关键代码
   builder.addPropertyValue("urlPatterns", extractUrlPatterns(attributes));
   registry.registerBeanDefinition(name, builder.getBeanDefinition());

从这里,我们第一次看到了 FilterRegistrationBean

通过调试上述代码的最后一行,可以看到,最终我们注册的 FilterRegistrationBean,其名字就是我们定义的 WebFilter 的名字:

现在,我们接着看第二个问题:TimeCostFilter 何时被实例化?

此时,我们想要的 Bean 被“张冠李戴”成 FilterRegistrationBean,但是 TimeCostFilter 是何时实例化的呢?

为什么它没有成为一个普通的 Bean?

关于这点,我们可以在 TimeCostFilter 的构造器中加个断点,然后使用调试的方式快速定位到它的初始化时机,这里我直接给出了调试截图:

在上述的关键调用栈中,结合源码,你可以找出一些关键信息:

  • Tomcat 等容器启动时,才会创建 FilterRegistrationBean;
  • FilterRegistrationBean 在被创建时(createBean)会创建 TimeCostFilter 来装配自身,TimeCostFilter 是通过 ResolveInnerBean 来创建的;
  • TimeCostFilter 实例最终是一种 InnerBean,我们可以通过下面的调试视图看到它的一些关键信息:

当使用 @WebFilter 修饰过滤器时,TimeCostFilter 类型的 Bean 并没有注册到 Spring 容器中,真正注册的是 FilterRegistrationBean

这里考虑到可能存在多个 Filter,所以我们可以这样修改下案例代码:

@Controller
@Slf4j
public class StudentController 
    @Autowired
    @Qualifier("com.spring.puzzle.filter.TimeCostFilter")FilterRegistrationBean timeCostFilter;
 

  • 注入的类型是 FilterRegistrationBean 类型,而不是 TimeCostFilter 类型;
  • 注入的名称是包含包名的长名称,即 com.spring.puzzle.filter.TimeCostFilter(不能用 TimeCostFilter),以便于存在多个过滤器时进行精确匹配。

经过上述修改后,代码成功运行无任何报错,符合我们的预期。


二、doFilter()方法多次执行

首先,还是需要通过 Spring Boot 创建一个 Web 项目,不过已经不需要 @ServletComponentScan:

用一种方式去向Spring注册对应Filter:@Component + Filter

@SpringBootApplication()
public class LearningApplication 
    public static void main(String[] args) 
        SpringApplication.run(LearningApplication.class, args);
        System.out.println("启动成功");
    

StudentController 保持功能不变,所以你可以直接参考之前的代码。

另外我们定义一个 DemoFilter 用来模拟问题,这个 Filter 标记了 @Component 且实现了 Filter 接口,已经不同于我们上一个案例的方式:

@Component
public class DemoFilter implements Filter 
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
        try 
            //模拟异常
            System.out.println("Filter 处理中时发生异常");
            throw new RuntimeException();
         catch (Exception e) 
            chain.doFilter(request, response);
        
        chain.doFilter(request, response);
    

其实,你会直接发现,我们在try-catch中捕获了异常后执行了一次doFilter(),出了try-catch后就会又再次执行一次对应的doFilter。

就会出现doFilter执行了两次,对应的Controller对应的真实业务方法也会执行两次。


Filter 背后的机制,即责任链设计模式

以 Tomcat 为例,我们先来看下它的 Filter 实现中最重要的类 ApplicationFilterChain。

它采用的是责任(职责)链设计模式,在形式上很像一种递归调用。但区别在于递归调用是同一个对象把子任务交给同一个方法本身去完成,而职责链则是一个对象把子任务交给其他对象的同名方法去完成。

其核心在于上下文 FilterChain 在不同对象 Filter 间的传递与状态的改变,通过这种链式串联,我们就可以对同一种对象资源实现不同业务场景的处理,达到业务解耦。整个 FilterChain 的结构就像这张图一样:


这里我们不妨还是带着两个问题去理解 FilterChain:

  • FilterChain 在何处被创建,又是在何处进行初始化调用,从而激活责任链开始链式调用?
  • FilterChain 为什么能够被链式调用,其内在的调用细节是什么?

直接查看负责请求处理的 StandardWrapperValve#invoke(),快速解决第一个问题:

public final void invoke(Request request, Response response)
    throws IOException, ServletException 
    // 省略非关键代码
    // 创建filterChain 
    ApplicationFilterChain filterChain =
        ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
// 省略非关键代码 
try 
    if ((servlet != null) && (filterChain != null)) 
        // Swallow output if needed
        if (context.getSwallowOutput()) 
             // 省略非关键代码 
             //执行filterChain
             filterChain.doFilter(request.getRequest(),
                            response.getResponse());
             // 省略非关键代码 
         
// 省略非关键代码

通过代码可以看出,Spring 通过 ApplicationFilterFactory.createFilterChain() 创建 FilterChain,然后调用其 doFilter() 执行责任链。

而这些步骤的起始点正是 StandardWrapperValve#invoke()。

接下来,我们来一起研究第二个问题,即 FilterChain 能够被链式调用的原因和内部细节。首先查看 ApplicationFilterFactory.createFilterChain(),来看下 FilterChain 如何被创建,如下所示:

public static ApplicationFilterChain createFilterChain(ServletRequest request,
        Wrapper wrapper, Servlet servlet) 
    // 省略非关键代码
    ApplicationFilterChain filterChain = null;
    if (request instanceof Request) 
        // 省略非关键代码
        // 创建Chain 
        filterChain = new ApplicationFilterChain();
        // 省略非关键代码
    
    // 省略非关键代码
    // Add the relevant path-mapped filters to this filter chain
    for (int i = 0; i < filterMaps.length; i++) 
        // 省略非关键代码
        ApplicationFilterConfig filterConfig = (ApplicationFilterConfig)
            context.findFilterConfig(filterMaps[i].getFilterName());
        if (filterConfig == null) 
            continue;
        
        // 增加filterConfig到Chain
        filterChain.addFilter(filterConfig);
    

    // 省略非关键代码
    return filterChain;

它创建 FilterChain,并将所有 Filter 逐一添加到 FilterChain 中。然后我们继续查看 ApplicationFilterChain 类及其 addFilter():

// 省略非关键代码
private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
private int pos = 0;
private int n = 0// 省略非关键代码
void addFilter(ApplicationFilterConfig filterConfig) 
    for(ApplicationFilterConfig filter:filters)
        if(filter==filterConfig)
            return;

    if (n == filters.length) 
        ApplicationFilterConfig[] newFilters =
            new ApplicationFilterConfig[n + INCREMENT];
        System.arraycopy(filters, 0, newFilters, 0, n);
        filters = newFilters;
    
    filters[n++] = filterConfig;

在 ApplicationFilterChain 里,声明了 3 个变量,类型为 ApplicationFilterConfig 的数组 Filters过滤器总数计数器 n,以及标识运行过程中被执行过的过滤器个数 pos

每个被初始化的 Filter 都会通过 filterChain.addFilter(),加入到类型为 ApplicationFilterConfig 的类成员数组 Filters 中,并同时更新 Filter 总数计数器 n,使其等于 Filters 数组的长度。

到这,Spring 就完成了 FilterChain 的创建准备工作。接下来,我们继续看 FilterChain 的执行细节,即 ApplicationFilterChain 的 doFilter():

public void doFilter(ServletRequest request, ServletResponse response)
    throws IOException, ServletException 
    if( Globals.IS_SECURITY_ENABLED ) 
        //省略非关键代码
        internalDoFilter(request,response);
        //省略非关键代码
     else 
        internalDoFilter(request,response);
    

这里逻辑被委派到了当前类的私有方法 internalDoFilter,具体实现如下:

private void internalDoFilter(ServletRequest request,
                              ServletResponse response)
    if (pos < n) 
        // pos会递增
        ApplicationFilterConfig filterConfig = filters[pos++];
        try 
            Filter filter = filterConfig.getFilter();
            // 省略非关键代码
            // 执行filter
            filter.doFilter(request, response, this);
            // 省略非关键代码
         
        // 省略非关键代码
        return;
    
        // 执行真正实际业务
        servlet.service(request, response);
     
    // 省略非关键代码

我们可以归纳下核心知识点

  • ApplicationFilterChain 的 internalDoFilter() 是过滤器逻辑的核心;
  • ApplicationFilterChain 的成员变量 Filters 维护了所有用户定义的过滤器;
  • ApplicationFilterChain 的类成员变量 n 为过滤器总数,变量 pos 是运行过程中已经执行的过滤器个数;
  • internalDoFilter() 每被调用一次,pos 变量值自增 1,即从类成员变量 Filters 中取下一个 Filter;
  • filter.doFilter(request, response, this) 会调用过滤器实现的 doFilter(),注意第三个参数值为 this,即为当前 ApplicationFilterChain 实例 ,这意味着:用户需要在过滤器中显式调用一次 javax.servlet.FilterChain#doFilter,才能完成整个链路;
  • pos < n 意味着执行完所有的过滤器,才能通过 servlet.service(request, response) 去执行真正的业务。

执行完所有的过滤器后,代码调用了 servlet.service(request, response) 方法。从下面这张调用栈的截图中,可以看到,经历了一个很长的看似循环的调用栈,我们终于从 internalDoFilter() 执行到了 Controller 层的 saveUser()。


对应的解决方案,就是在代码中需要注意,不要多次执行doFilter()

@Component
public class DemoFilter implements Filter 
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException 
        try 
            //模拟异常
            System.out.println("Filter 处理中时发生异常");
            throw new RuntimeException();
         catch (Exception e) 
            //去掉下面这行调用
            //chain.doFilter(request, response);
        
        chain.doFilter(request, response);
    

重新运行程序和测试,结果符合预期,业务只执行了一次。

回顾这个问题,我想你应该有所警示:在使用过滤器的时候,一定要注意,不管怎么调用,不能多次调用 FilterChain#doFilter()


三、总结

  • 针对Filter注入到别的Bean中时,需要注意对应Filter过滤器如果通过@ServletComponentScan+@WebFilter的方式注册到容器的话,对应生成的类为FilterRegistrationBean,那对应注入获取的时候不能拿Filter类来接收,而是FilterRegistrationBean;同时也要注意多个Filter的情况,精确注入

    @Controller
    @Slf4j
    public class StudentController 
        @Autowired
        @Qualifier("com.spring.puzzle.filter.TimeCostFilter")FilterRegistrationBean timeCostFilter;
     
    
    
  • 在对应Filter中doFilter()方法注意不要多次执行,不然对应的业务方法和FilterChain上的Filter都会被多次执行

以上是关于Day613.SpringWebFilter常见错误① -Spring编程常见错误的主要内容,如果未能解决你的问题,请参考以下文章

NSATP-A学习笔记之Day3-4常见注入类型

每日一题 错选择 及 编程题 周总结

每日一题 错选择 及 编程题 周总结

DAY13 Matlab实现图像错切源代码

每日一题 错选择 及 编程题 周总结

day.java:5: 错: 编码 GBK 的不可映射字符 (0x88) System.out.println((i+1)+"链?"+"链?"+day[i]+&