Перейти к основному содержимому

Что такое OncePerRequestFilter?

· 5 мин. чтения

1. Обзор

В этом руководстве мы узнаем об OncePerRequestFilter , специальном типе фильтра в Spring. Мы увидим, какую проблему он решает, и поймем, как его использовать на быстром примере.

2. Что такое OncePerRequestFilter ?

Давайте сначала разберемся, как работают фильтры. Фильтр можно вызывать либо до, либо после выполнения сервлета . Когда запрос отправляется сервлету, RequestDispatcher может перенаправить его другому сервлету. Есть вероятность, что другой сервлет также имеет такой же фильтр. В таких сценариях один и тот же фильтр вызывается несколько раз.

Но мы можем захотеть, чтобы конкретный фильтр вызывался только один раз для каждого запроса. Обычный вариант использования — при работе с 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 " +
userName);
filterChain.doFilter(request, response);
}
}

Поскольку OncePerRequestFilter поддерживает только HTTP-запросы , нет необходимости приводить объекты запроса и ответа , как это делается при реализации интерфейса Filter .

4. Использование OncePerRequestFilter для асинхронных запросов

Для асинхронных запросов OncePerRequestFilter по умолчанию не применяется. Нам нужно переопределить методы shouldNotFilterAsyncDispatch() и shouldNotFilterErrorDispatch() , чтобы поддерживать это.

Иногда нам нужно, чтобы фильтр применялся только в начальном потоке запроса, а не в дополнительных потоках, созданных при асинхронной отправке. В других случаях нам может понадобиться вызывать фильтр по крайней мере один раз в каждом дополнительном потоке. В таких случаях нам нужно переопределить метод shouldNotFilterAsyncDispatch() .

Если метод shouldNotFilterAsyncDispatch() возвращает true , то фильтр не будет вызываться для последующей асинхронной отправки. Однако, если он возвращает false , фильтр будет вызываться для каждой асинхронной отправки ровно один раз для каждого потока.

Точно так же мы переопределяем метод shouldNotFilterErrorDispatch() и возвращаем true или false в зависимости от того, хотим ли мы фильтровать отправку ошибок или нет :

@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;
}
}

В приведенном выше коде мы намеренно вернули значение true из метода shouldNotFilterAsyncDispatch() . Это сделано для того, чтобы продемонстрировать, что наш фильтр вызывается только один раз для потока контейнера, а не для последующих асинхронных потоков.

Давайте вызовем нашу конечную точку, чтобы продемонстрировать это:

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 .