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

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

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

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.foreach.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

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

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

@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

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

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

@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. Порядок исполнения

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

./93699737dde70ad7fb4d5130a83de41d.png

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

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

  1. ЗапросКлиентФильтр
  2. ЗапросКлиентВрайтерИнтерсептор
  3. PrematchingRequestFilter
  4. RestrictedOperationsRequestFilter
  5. Реквестсерверреадеринтерцептор
  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 .