OncePerRequestFilter 的用法

1、概览

本文将带你了解 Spring 中一种特殊类型的 Filter(过滤器)OncePerRequestFilter

通过实例了解它的功能和用法。

2、OncePerRequestFilter 是什么?

回顾一下 Filter 的工作原理。Filter 可以在 Servlet 执行之前或之后调用。当请求被调度给一个 Servlet 时,RequestDispatcher 可能会将其转发给另一个 Servlet。另一个 Servlet 也有可能使用相同的 Filter。在这种情况下,同一个 Filter 会被调用多次

但是,有时需要确保每个请求只调用一次特定的 Filter。一个常见的用例是在使用 Spring Security 时。当请求通过过滤器链(Filter Chain)时,对请求的身份证认证应该只执行一次。

在这种情况下,可以继承 OncePerRequestFilter。Spring 保证 OncePerRequestFilter 只对指定请求执行一次。

3、使用 OncePerRequestFilter 处理同步请求

定义一个继承了 OncePerRequestFilterAuthenticationFilter Filter 类,并覆写 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  " +
                userName);
        filterChain.doFilter(request, response);
    }
}

由于 OncePerRequestFilter 仅支持 HTTP 请求,因此无需像直接实现 Filter 接口时那样对 requestresponse 对象进行强制转换。

4、使用 OncePerRequestFilter 处理异步请求

对于异步请求,默认情况下不会应用 OncePerRequestFilter

需要覆写 shouldNotFilterAsyncDispatch()shouldNotFilterErrorDispatch() 方法来支持这一点。

有时,只需要在初始请求线程中应用 Filter,而不需要在异步调度中创建的其他线程中应用 Filter。其他时候,可能需要在每个额外线程中至少调用一次 Filter。在这种情况下,需要覆写 shouldNotFilterAsyncDispatch() 方法。

如果 shouldNotFilterAsyncDispatch() 方法返回 true,那么后续的异步调度不会调用 Filter。但是,如果该方法返回 false,则 Filter 会在每次异步调度中调用,每个线程正好调用一次。

类似地,可以覆写 shouldNotFilterErrorDispatch() 方法,并根据是否希望过滤 Error Dispatch 来返回 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() 方法,让 Filter 有条件地只适用于某些特定请求,而跳过其他请求:

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

6、快速示例

通过一个快速示例来了解 OncePerRequestFilter

首先,定义一个 Controller,使用 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");
    }
}

异步处理请求时,两个线程都会经过相同的过滤器链(Filter Chain)。因此,Filter 会被调用两次:第一次是容器线程处理请求时,第二次是异步调度器(Async Dispatcher)完成后。异步处理完成后,响应将返回给客户端。

现在,定义一个继承 OncePerRequestFilter 的 Filter:

@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

现在,来看看请求和异步调度都调用 Filter 的情况。只需覆写 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

从上面的输出可以看出,Filter 被调用了两次 - 首先是容器线程,然后是另一个线程。

7、总结

本文介绍了 OncePerRequestFilter 的功能以及在实际中的用法。


Ref:https://www.baeldung.com/spring-onceperrequestfilter