Джерси Фильтры и Перехватчики

Джерси Фильтры и перехватчики

1. Вступление

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

Мы будем использовать здесь Jersey 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.example.jersey.server");
    }
}

Если вы хотите глубже понять, как создать API с помощью Jersey, вы можете проверитьthis article.

Вы также можете взглянуть наour client-focused article и узнать, как создать клиент Java с Джерси.

3. фильтры

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

Проще говоря,filters let us modify the properties of requests and responses - например, заголовки HTTP. Фильтры могут применяться как на стороне сервера, так и на стороне клиента.

Имейте в виду, чтоfilters are always executed, regardless of whether the resource was found or not.

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().

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

Напомним, чтоthis filter is executed after the resource was matched.

Если мы хотим выполнить фильтр перед сопоставлением ресурсов,we can use a pre-matching filter by annotating our filter with the @PreMatching annotation:

@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. Реализация фильтра сервера ответа

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

Для этогоour filter has to implement the ContainerResponseFilter interface и реализуем его единственный метод:

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

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

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-сообщений, содержащихся в запросах и ответах. Их можно использовать как на сервере, так и на стороне клиента.

Имейте в виду, чтоthey’re executed after the filters and only if a message body is present.

Есть два типа перехватчиков: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

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

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

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

Обратите внимание, чтоwe have to call the proceed() methodto call the next interceptor in the chain. Как только все перехватчики будут выполнены, будет вызвано соответствующее устройство чтения тела сообщения.

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

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

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

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

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

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

Don’t forget that you have to register this interceptor in the client configuration, как мы делали раньше с клиентским фильтром:

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

    return ClientBuilder.newClient(config);
}

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

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

image

Как видим,the filters are always executed first, and the interceptors are executed right before calling the appropriate message body reader or writer.

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

  1. RequestClientFilter

  2. RequestClientWriterInterceptor

  3. PrematchingRequestFilter

  4. RestrictedOperationsRequestFilter

  5. RequestServerReaderInterceptor

  6. ResponseServerFilter

  7. ResponseClientFilter

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

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

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

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

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

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

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

Однакоthey can also be defined to be executed only for specific resource methods, который называется привязкой имени.

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 и как мы можем использовать их в веб-приложении.

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