«1. Обзор

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

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

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

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

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

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

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

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

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

$> mvn install jetty:run

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

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

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

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

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

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

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

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

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);
        }
    }
    
    ...
}

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

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

    ...
}

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

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

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

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

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

public abstract class FrontCommand {
    ...

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

    ...
}

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

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

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

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

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());
    }
}

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

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.0, с помощью аннотации.

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

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

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

«

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

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

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

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

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

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

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

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

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

4. Заключение

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

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

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

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

«

As usual, you’ll find the sources on GitHub.