1. 概述

在这个教程中,我们将学习Spring中的特殊过滤器OncePerRequestFilter。我们将了解它解决的问题,并通过一个快速示例理解如何使用它。

2. 什么是OncePerRequestFilter

首先,让我们了解一下过滤器的工作原理。一个Filter(过滤器)可以在Servlet执行前后被调用。当请求被转发到Servlet时,RequestDispatcher可能会将其转发到另一个Servlet。在这种情况下,同一个过滤器可能会被多次调用。

然而,我们可能希望确保某个特定的过滤器对每个请求只执行一次。在使用Spring Security时,这是一个常见的用例:当请求经过过滤器链时,我们可能希望某些身份验证操作只为请求执行一次。

在这样的情况下,我们可以扩展OncePerRequestFilter。Spring保证对于给定的请求,OncePerRequestFilter只执行一次。

3. 使用OncePerRequestFilter处理同步请求

让我们通过一个例子来理解如何使用这个过滤器。我们将定义一个名为AuthenticationFilter的类,它继承自OncePerRequestFilter,并重写doFilterInternal()方法:

public class AuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        String usrName = request.getHeader(“userName”);
        logger.info("Successfully authenticated user  " +
                usrName);
        filterChain.doFilter(request, response);
    }
}

由于OncePerRequestFilter仅支持HTTP请求,因此我们不需要像实现Filter接口时那样将requestresponse对象强制转换。

4. 使用OncePerRequestFilter处理异步请求

对于异步请求,OncePerRequestFilter默认不应用。我们需要重写shouldNotFilterAsyncDispatch()shouldNotFilterErrorDispatch()方法来支持这种需求。

有时,我们可能希望过滤器仅在初始请求线程中应用,而不是在异步分派中创建的额外线程中。其他时候,我们可能需要在每个额外线程中至少调用一次过滤器。在这种情况下,我们需要重写shouldNotFilterAsyncDispatch()方法。

如果shouldNotFilterAsyncDispatch()方法返回true,那么过滤器将不会在后续的异步分派中被调用。然而,如果返回false,则过滤器将在每个异步分派中精确地执行一次,每次在一个线程中。

同样地,我们会重写shouldNotFilterErrorDispatch()方法,并根据是否要过滤错误分派返回truefalse

@Component
public class AuthenticationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        String usrName = request.getHeader("userName");
        logger.info("Successfully authenticated user  " +
          usrName);
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return false;
    }

    @Override
    protected boolean shouldNotFilterErrorDispatch() {
        return false;
    }
}

5. 条件性跳过请求

我们可以通过重写shouldNotFilter()方法,使过滤器仅对某些特定请求有条件应用,而对其他请求跳过:

@Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
    return Boolean.TRUE.equals(request.getAttribute(SHOULD_NOT_FILTER));
}

6. 快速示例

让我们通过一个快速示例来理解OncePerRequestFilter的行为。首先,我们将定义一个控制器,使用Spring的DeferredResult异步处理请求:

@Controller
public class HelloController  {
    @GetMapping(path = "/greeting")
    public DeferredResult<String> hello(HttpServletResponse response) throws Exception {
        DeferredResult<String> deferredResult = new DeferredResult<>();
        executorService.submit(() -> perform(deferredResult));
        return deferredResult;
    }
    private void perform(DeferredResult<String> dr) {
        // some processing 
        dr.setResult("OK");
    }
}

在异步处理请求时,两个线程都会经过相同的过滤器链。因此,过滤器会被调用两次:第一次是容器线程处理请求,然后是异步分派完成后。一旦异步处理完成,响应将返回给客户端。

现在,让我们定义一个实现OncePerRequestFilter的过滤器:

@Component
public class MyOncePerRequestFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
      FilterChain filterChain) throws ServletException, IOException {
        logger.info("Inside Once Per Request Filter originated by request {}", request.getRequestURI());
        filterChain.doFilter(request, response);
    }

    @Override
    protected boolean shouldNotFilterAsyncDispatch() {
        return true;
    }
}

在上述代码中,我们故意从shouldNotFilterAsyncDispatch()方法返回true。这是为了演示我们的过滤器只在容器线程中被调用,而在后续的异步线程中不会被调用。

让我们调用我们的端点来演示这一点:

curl -X GET http://localhost:8082/greeting 

输出:

10:23:24.175 [http-nio-8082-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
10:23:24.175 [http-nio-8082-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
10:23:24.176 [http-nio-8082-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
10:23:26.814 [http-nio-8082-exec-1] INFO  c.b.O.MyOncePerRequestFilter - Inside OncePer Request Filter originated by request /greeting

现在,让我们看看一个案例,其中我们希望请求和异步分派都调用我们的过滤器。我们只需要将shouldNotFilterAsyncDispatch()重写为返回false即可实现这一点:

@Override
protected boolean shouldNotFilterAsyncDispatch() {
    return false;
}

输出:

2:53.616 [http-nio-8082-exec-1] INFO  o.a.c.c.C.[Tomcat].[localhost].[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
10:32:53.616 [http-nio-8082-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
10:32:53.617 [http-nio-8082-exec-1] INFO  o.s.web.servlet.DispatcherServlet - Completed initialization in 1 ms
10:32:53.633 [http-nio-8082-exec-1] INFO  c.b.O.MyOncePerRequestFilter - Inside OncePer Request Filter originated by request /greeting
10:32:53.663 [http-nio-8082-exec-2] INFO  c.b.O.MyOncePerRequestFilter - Inside OncePer Request Filter originated by request /greeting

从上述输出中,我们可以看到我们的过滤器被调用了两次:第一次由容器线程,然后由另一个线程。

7. 总结

在这篇文章中,我们探讨了OncePerRequestFilter,它解决了什么问题,以及如何通过一些实际示例来实现它。

如往常一样,完整的源代码可以在GitHub上找到。