«1. Введение

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

Здесь мы будем использовать Джерси 2, а тестировать наше приложение будем на сервере Tomcat 9.

2. Настройка приложения

Давайте сначала создадим простой ресурс на нашем сервере:

@Path("/greetings")
public class Greetings {

    @GET
    public String getHelloGreeting() {
        return "hello";
    }
}

Также давайте создадим соответствующую конфигурацию сервера для нашего приложения:

@ApplicationPath("/*")
public class ServerConfig extends ResourceConfig {

    public ServerConfig() {
        packages("com.baeldung.jersey.server");
    }
}

Если вы хотите копнуть глубже о том, как создать API с помощью Джерси, вы можете прочитать в этой статье.

Вы также можете ознакомиться с нашей статьей, ориентированной на клиента, и узнать, как создать Java-клиент с помощью Jersey.

3. Фильтры

Теперь приступим к работе с фильтрами.

Проще говоря, фильтры позволяют нам изменять свойства запросов и ответов — например, заголовки HTTP. Фильтры могут применяться как на стороне сервера, так и на стороне клиента.

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

3.1. Реализация фильтра сервера запросов

Давайте начнем с фильтров на стороне сервера и создадим фильтр запросов.

Мы сделаем это, реализовав интерфейс ContainerRequestFilter и зарегистрировав его как Provider на нашем сервере:

@Provider
public class RestrictedOperationsRequestFilter implements ContainerRequestFilter {
    
    @Override
    public void filter(ContainerRequestContext ctx) throws IOException {
        if (ctx.getLanguage() != null && "EN".equals(ctx.getLanguage()
          .getLanguage())) {
 
            ctx.abortWith(Response.status(Response.Status.FORBIDDEN)
              .entity("Cannot access")
              .build());
        }
    }
}

Этот простой фильтр просто отклоняет запросы с языком «EN» в запросе, вызывая abortWith () метод.

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

Имейте в виду, что этот фильтр выполняется после сопоставления ресурса.

В случае, если мы хотим выполнить фильтр перед сопоставлением ресурсов, мы можем использовать фильтр предварительного сопоставления, аннотировав наш фильтр аннотацией @PreMatching:

@Provider
@PreMatching
public class PrematchingRequestFilter implements ContainerRequestFilter {

    @Override
    public void filter(ContainerRequestContext ctx) throws IOException {
        if (ctx.getMethod().equals("DELETE")) {
            LOG.info("\"Deleting request");
        }
    }
}

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

2018-02-25 16:07:27,800 [http-nio-8080-exec-3] INFO  c.b.j.s.f.PrematchingRequestFilter - prematching filter
2018-02-25 16:07:27,816 [http-nio-8080-exec-3] INFO  c.b.j.s.f.RestrictedOperationsRequestFilter - Restricted operations filter

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

Теперь мы реализуем фильтр ответов на стороне сервера, который просто добавит к ответу новый заголовок.

Для этого наш фильтр должен реализовать интерфейс ContainerResponseFilter и реализовать его единственный метод:

@Provider
public class ResponseServerFilter implements ContainerResponseFilter {

    @Override
    public void filter(ContainerRequestContext requestContext, 
      ContainerResponseContext responseContext) throws IOException {
        responseContext.getHeaders().add("X-Test", "Filter test");
    }
}

Обратите внимание, что параметр ContainerRequestContext используется как доступный только для чтения, поскольку мы уже обрабатываем ответ. .

2.3. Реализация клиентского фильтра

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

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

@Provider
public class RequestClientFilter implements ClientRequestFilter {

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.setProperty("test", "test client request filter");
    }
}

Давайте также создадим клиент Джерси для тестирования этого фильтра:

public class JerseyClient {

    private static String URI_GREETINGS = "http://localhost:8080/jersey/greetings";

    public static String getHelloGreeting() {
        return createClient().target(URI_GREETINGS)
          .request()
          .get(String.class);
    }

    private static Client createClient() {
        ClientConfig config = new ClientConfig();
        config.register(RequestClientFilter.class);

        return ClientBuilder.newClient(config);
    }
}

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

Наконец, мы также создадим фильтр для ответа в клиенте.

Это работает так же, как на сервере, но реализует интерфейс ClientResponseFilter:

@Provider
public class ResponseClientFilter implements ClientResponseFilter {

    @Override
    public void filter(ClientRequestContext requestContext, 
      ClientResponseContext responseContext) throws IOException {
        responseContext.getHeaders()
          .add("X-Test-Client", "Test response client filter");
    }

}

Опять же, ClientRequestContext предназначен только для чтения.

4. Перехватчики

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

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

Существует два типа перехватчиков: ReaderInterceptor и WriterInterceptor, и они одинаковы как для серверной, так и для клиентской стороны.

Далее мы собираемся создать еще один ресурс на нашем сервере, доступ к которому осуществляется через POST и получает параметр в теле, поэтому при доступе к нему будут выполняться перехватчики:

@POST
@Path("/custom")
public Response getCustomGreeting(String name) {
    return Response.status(Status.OK.getStatusCode())
      .build();
}

Мы’ Мы также добавим в наш клиент Jersey новый метод — для тестирования этого нового ресурса:

public static Response getCustomGreeting() {
    return createClient().target(URI_GREETINGS + "/custom")
      .request()
      .post(Entity.text("custom"));
}

4.1. Реализация ReaderInterceptor

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

«Давайте создадим перехватчик на стороне сервера, чтобы написать собственное сообщение в теле перехваченного запроса:

@Provider
public class RequestServerReaderInterceptor implements ReaderInterceptor {

    @Override
    public Object aroundReadFrom(ReaderInterceptorContext context) 
      throws IOException, WebApplicationException {
        InputStream is = context.getInputStream();
        String body = new BufferedReader(new InputStreamReader(is)).lines()
          .collect(Collectors.joining("\n"));

        context.setInputStream(new ByteArrayInputStream(
          (body + " message added in server reader interceptor").getBytes()));

        return context.proceed();
    }
}

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

3.2. Реализация WriterInterceptor

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

Давайте создадим перехватчик записи, чтобы добавить сообщение к запросу на стороне клиента:

@Provider
public class RequestClientWriterInterceptor implements WriterInterceptor {

    @Override
    public void aroundWriteTo(WriterInterceptorContext context) 
      throws IOException, WebApplicationException {
        context.getOutputStream()
          .write(("Message added in the writer interceptor in the client side").getBytes());

        context.proceed();
    }
}

Опять же, мы должны вызвать метод continue() для вызова следующего перехватчика.

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

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

private static Client createClient() {
    ClientConfig config = new ClientConfig();
    config.register(RequestClientFilter.class);
    config.register(RequestWriterInterceptor.class);

    return ClientBuilder.newClient(config);
}

5. Порядок выполнения

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

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

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

  1. RequestClientFilter
  2. RequestClientWriterInterceptor
  3. PrematchingRequestFilter
  4. RestrictedOperationsRequestFilter
  5. RequestServerReaderInterceptor
  6. ResponseServerFilter
  7. ResponseClientFilter

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

Приоритет указывается целым числом и сортирует фильтры и перехватчики в порядке возрастания запросов и в порядке убывания ответов.

Давайте добавим приоритет к нашему RestrictedOperationsRequestFilter:

@Provider
@Priority(Priorities.AUTHORIZATION)
public class RestrictedOperationsRequestFilter implements ContainerRequestFilter {
    // ...
}

Обратите внимание, что мы использовали предопределенный приоритет для целей авторизации.

6. Привязка имен

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

Однако их также можно определить для выполнения только для определенных методов ресурсов, что называется привязкой имен.

6.1. Статическая привязка

Один из способов сделать привязку имени — статически, создав определенную аннотацию, которая будет использоваться в нужном ресурсе. Эта аннотация должна включать метааннотацию @NameBinding.

Давайте создадим его в нашем приложении:

@NameBinding
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloBinding {
}

После этого мы можем аннотировать некоторые ресурсы этой аннотацией @HelloBinding:

@GET
@HelloBinding
public String getHelloGreeting() {
    return "hello";
}

Наконец, мы собираемся аннотировать один из наших фильтров с помощью этой аннотации, поэтому этот фильтр будет выполняться только для запросов и ответов, которые обращаются к методу getHelloGreeting():

@Provider
@Priority(Priorities.AUTHORIZATION)
@HelloBinding
public class RestrictedOperationsRequestFilter implements ContainerRequestFilter {
    // ...
}

Имейте в виду, что наш RestrictedOperationsRequestFilter больше не будет срабатывать для остальных ресурсов.

6.2. Динамическое связывание

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

Давайте сначала добавим еще один ресурс на наш сервер для этого раздела:

@GET
@Path("/hi")
public String getHiGreeting() {
    return "hi";
}

Теперь давайте создадим привязку для этого ресурса, реализуя интерфейс DynamicFeature:

@Provider
public class HelloDynamicBinding implements DynamicFeature {

    @Override
    public void configure(ResourceInfo resourceInfo, FeatureContext context) {
        if (Greetings.class.equals(resourceInfo.getResourceClass()) 
          && resourceInfo.getResourceMethod().getName().contains("HiGreeting")) {
            context.register(ResponseServerFilter.class);
        }
    }
}

В этом случае мы связываем метод getHiGreeting() для ResponseServerFilter, который мы создали ранее.

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

Если мы этого не сделаем, фильтр будет выполнен дважды: один раз как глобальный фильтр, а другой раз как фильтр, привязанный к методу getHiGreeting().

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

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

Как всегда, полный исходный код примеров доступен на GitHub.