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

Введение в шаблон перехвата фильтра в Java

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

1. Обзор

В этом руководстве мы собираемся представить базовый шаблон J2EE уровня представления Intercepting Filter Pattern.

Это второй учебник в нашей серии паттернов и продолжение руководства по паттернам Front Controller , которое можно найти здесь .

Перехватывающие фильтры — это фильтры, запускающие действия до или после обработки входящего запроса обработчиком.

Перехватывающие фильтры представляют собой централизованные компоненты веб-приложения, общие для всех запросов и расширяемые, не затрагивающие существующие обработчики.

2. Варианты использования

Давайте расширим пример из предыдущего руководства и реализуем механизм аутентификации, ведение журнала запросов и счетчик посетителей . Кроме того, нам нужна возможность доставки наших страниц в различных кодировках .

Все это варианты использования для перехвата фильтров, поскольку они являются общими для всех запросов и не должны зависеть от обработчиков.

3. Стратегии фильтрации

Давайте представим различные стратегии фильтрации и примеры использования. Чтобы запустить код с контейнером Jetty Servlet, просто выполните:

$> mvn install jetty:run

3.1. Пользовательская стратегия фильтрации

Стратегия пользовательского фильтра используется в каждом случае использования, требующем упорядоченной обработки запросов, в том смысле, что один фильтр основан на результатах предыдущего фильтра в цепочке выполнения .

Эти цепочки будут созданы путем реализации интерфейса FilterChain и регистрации в нем различных классов Filter .

При использовании нескольких цепочек фильтров с разными задачами вы можете объединить их вместе в диспетчере фильтров:

./e3ad61ea88fbd772061a83859b997993.png

В нашем примере счетчик посетителей работает, подсчитывая уникальные имена пользователей от вошедших в систему пользователей, что означает, что он основан на результате фильтра аутентификации, поэтому оба фильтра должны быть связаны.

Давайте реализуем эту цепочку фильтров.

Во-первых, мы создадим фильтр аутентификации, который проверяет, существует ли сеанс для заданного атрибута «имя пользователя», и запускает процедуру входа, если нет:

public class AuthenticationFilter implements Filter {
...
@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;

HttpSession session = httpServletRequest.getSession(false);
if (session == null || session.getAttribute("username") == null) {
FrontCommand command = new LoginCommand();
command.init(httpServletRequest, httpServletResponse);
command.process();
} else {
chain.doFilter(request, response);
}
}

...
}

Теперь давайте создадим счетчик посетителей. Этот фильтр поддерживает HashSet уникальных имен пользователей и добавляет к запросу атрибут counter:

public class VisitorCounterFilter implements Filter {
private static Set<String> users = new HashSet<>();

...
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) {
HttpSession session = ((HttpServletRequest) request).getSession(false);
Optional.ofNullable(session.getAttribute("username"))
.map(Object::toString)
.ifPresent(users::add);
request.setAttribute("counter", users.size());
chain.doFilter(request, response);
}

...
}

Далее мы реализуем FilterChain , который перебирает зарегистрированные фильтры и выполняет метод doFilter :

public class FilterChainImpl implements FilterChain {
private Iterator<Filter> filters;

public FilterChainImpl(Filter... filters) {
this.filters = Arrays.asList(filters).iterator();
}

@Override
public void doFilter(ServletRequest request, ServletResponse response) {
if (filters.hasNext()) {
Filter filter = filters.next();
filter.doFilter(request, response, this);
}
}
}

Чтобы связать наши компоненты вместе, давайте создадим простой статический менеджер, который отвечает за создание экземпляров цепочек фильтров, регистрацию его фильтров и инициирует его:

public class FilterManager {
public static void process(HttpServletRequest request,
HttpServletResponse response, OnIntercept callback) {
FilterChain filterChain = new FilterChainImpl(
new AuthenticationFilter(callback), new VisitorCounterFilter());
filterChain.doFilter(request, response);
}
}

В качестве последнего шага нам нужно будет вызвать наш FilterManager как общую часть последовательности обработки запросов из нашей FrontCommand :

public abstract class FrontCommand {
...

public void process() {
FilterManager.process(request, response);
}

...
}

3.2. Базовая стратегия фильтрации

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

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

Абстрактный базовый класс можно использовать для применения пользовательского поведения, принадлежащего цепочке фильтров. Мы будем использовать его в нашем примере, чтобы сократить шаблонный код, связанный с настройкой фильтра и ведением журнала отладки:

public abstract class BaseFilter implements Filter {
private Logger log = LoggerFactory.getLogger(BaseFilter.class);

protected FilterConfig filterConfig;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Initialize filter: {}", getClass().getSimpleName());
this.filterConfig = filterConfig;
}

@Override
public void destroy() {
log.info("Destroy filter: {}", getClass().getSimpleName());
}
}

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

public class LoggingFilter extends BaseFilter {
private static final Logger log = LoggerFactory.getLogger(LoggingFilter.class);

@Override
public void doFilter(
ServletRequest request,
ServletResponse response,
FilterChain chain) {
chain.doFilter(request, response);
HttpServletRequest httpServletRequest = (HttpServletRequest) request;

String username = Optional
.ofNullable(httpServletRequest.getAttribute("username"))
.map(Object::toString)
.orElse("guest");

log.info(
"Request from '{}@{}': {}?{}",
username,
request.getRemoteAddr(),
httpServletRequest.getRequestURI(),
request.getParameterMap());
}
}

3.3. Стандартная стратегия фильтрации

Более гибким способом применения фильтров является реализация стандартной стратегии фильтрации . Это можно сделать, объявив фильтры в дескрипторе развертывания или, начиная со спецификации сервлета 3.0, с помощью аннотации.

Стандартная стратегия фильтрации `` позволяет подключать новые фильтры к цепочке по умолчанию, не имея явно определенного менеджера фильтров:

./5347c73e8b1fca57680101c8576eb9e9.png

Обратите внимание, что порядок применения фильтров нельзя указать с помощью аннотации. Если вам нужно упорядоченное выполнение, вы должны придерживаться дескриптора развертывания или реализовать собственную стратегию фильтрации.

Давайте реализуем фильтр кодирования, управляемый аннотациями, который также использует стратегию базового фильтра:

@WebFilter(servletNames = {"intercepting-filter"}, 
initParams = {@WebInitParam(name = "encoding", value = "UTF-8")})
public class EncodingFilter extends BaseFilter {
private String encoding;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
super.init(filterConfig);
this.encoding = filterConfig.getInitParameter("encoding");
}

@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain) {
String encoding = Optional
.ofNullable(request.getParameter("encoding"))
.orElse(this.encoding);
response.setCharacterEncoding(encoding);

chain.doFilter(request, response);
}
}

В сценарии сервлета с дескриптором развертывания наш файл web.xml будет содержать следующие дополнительные объявления:

<filter>
<filter-name>encoding-filter</filter-name>
<filter-class>
com.foreach.patterns.intercepting.filter.filters.EncodingFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>encoding-filter</filter-name>
<servlet-name>intercepting-filter</servlet-name>
</filter-mapping>

Давайте возьмем наш фильтр ведения журнала и аннотируем его, чтобы сервлет мог его использовать:

@WebFilter(servletNames = "intercepting-filter")
public class LoggingFilter extends BaseFilter {
...
}

3.4. Стратегия фильтра шаблонов

Стратегия шаблонного фильтра во многом аналогична базовой стратегии фильтра, за исключением того, что она использует методы шаблона, объявленные в базовом классе, которые должны быть переопределены в реализациях:

./25c7b412086160adb3258d3459768efb.png

Давайте создадим базовый класс фильтра с двумя абстрактными методами фильтра, которые вызываются до и после дальнейшей обработки.

Поскольку эта стратегия менее распространена и мы не используем ее в нашем примере, конкретная реализация и вариант использования зависят от вашего воображения:

public abstract class TemplateFilter extends BaseFilter {
protected abstract void preFilter(HttpServletRequest request,
HttpServletResponse response);

protected abstract void postFilter(HttpServletRequest request,
HttpServletResponse response);

@Override
public void doFilter(ServletRequest request,
ServletResponse response, FilterChain chain) {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;

preFilter(httpServletRequest, httpServletResponse);
chain.doFilter(request, response);
postFilter(httpServletRequest, httpServletResponse);
}
}

4. Вывод

Шаблон перехватывающего фильтра фиксирует сквозные проблемы, которые могут развиваться независимо от бизнес-логики. С точки зрения бизнес-операций фильтры выполняются как цепочка действий до или после.

Как мы уже видели, шаблон перехватывающего фильтра может быть реализован с использованием различных стратегий. В приложениях «реального мира» эти разные подходы можно комбинировать.

Как обычно, вы найдете исходники на GitHub .