ジャージーフィルターとインターセプター

ジャージーフィルターとインターセプター

1. 前書き

この記事では、Jerseyフレームワークでフィルターとインターセプターがどのように機能するか、およびこれらの主な違いについて説明します。

ここではJersey2を使用し、Tomcat9サーバーを使用してアプリケーションをテストします。

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

JerseyでAPIを作成する方法をさらに深く掘り下げたい場合は、this articleを確認できます。

また、our client-focused articleを見て、Jerseyを使用して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());
        }
    }
}

この単純なフィルターは、abortWith()メソッドを呼び出すことにより、要求に言語“EN”が含まれる要求を拒否するだけです。

例が示すように、要求のコンテキストを受け取るメソッドを1つだけ実装する必要がありましたが、必要に応じて変更できます。

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.

インターセプターには、ReaderInterceptorWriterInterceptorの2種類があり、サーバー側とクライアント側の両方で同じです。

次に、サーバー上に別のリソースを作成します。これは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の実装

ライタインターセプタは、リーダインターセプタと非常によく似た方法で動作しますが、アウトバウンドストリームを操作するため、クライアント側のリクエストまたはサーバー側の応答で使用できます。

クライアント側で、リクエストにメッセージを追加するライターインターセプターを作成しましょう。

@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. 静的バインディング

名前バインディングを行う1つの方法は、目的のリソースで使用される特定の注釈を作成することにより静的に行うことです。 この注釈には、@NameBindingメタ注釈を含める必要があります。

アプリケーションで作成しましょう:

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

その後、この@HelloBindingアノテーションでいくつかのリソースにアノテーションを付けることができます。

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

最後に、フィルターの1つにもこのアノテーションを付けます。そのため、このフィルターは、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に関連付けています。

DynamicFeatureを介して構成しているため、このフィルターから@Providerアノテーションを削除する必要があったことを覚えておくことが重要です。

これを行わないと、フィルターは2回実行されます。1回はグローバルフィルターとして、もう1回はgetHiGreeting()メソッドにバインドされたフィルターとして実行されます。

7. 結論

このチュートリアルでは、Jersey 2でフィルターとインターセプターがどのように機能し、Webアプリケーションでそれらを使用する方法を理解することに焦点を当てました。

いつものように、例の完全なソースコードはover on GitHubで入手できます。