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 .