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.baeldung.jersey.server");
    }
}

ジャージーでAPIを作成する方法をさらに深く掘り下げたい場合は、この記事をチェックしてください。

また、クライアントに焦点を当てた記事を見て、Jerseyを使用してJavaクライアントを作成する方法を学ぶこともできます。

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

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

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

このフィルターは、リソースが一致した後に実行されることに注意してください。

リソースマッチングの前にフィルターを実行する場合は、フィルターに@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");
    }
}

このフィルターをテストするための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メッセージ本文のマーシャリングとアンマーシャリングとより密接に関連しています。 サーバー側とクライアント側の両方で使用できます。

これらはフィルターの後で、メッセージ本文が存在する場合にのみ実行されることに注意してください。

インターセプターには、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();
    }
}

は、proceed()メソッド を呼び出して、チェーン内の次のインターセプターを呼び出す必要があることに注意してください。 すべてのインターセプターが実行されると、適切なメッセージ本文リーダーが呼び出されます。

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

ここでも、メソッド progress()を呼び出して、次のインターセプターを呼び出す必要があります。

すべてのインターセプターが実行されると、適切なメッセージ本文ライターが呼び出されます。

以前にクライアントフィルターで行ったように、このインターセプターをクライアント構成に登録する必要があることを忘れないでください

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

    return ClientBuilder.newClient(config);
}

5. 執行順序

これまでに見たすべてのことを、クライアントからサーバーへの要求中にフィルターとインターセプターがいつ実行されるかを示す図に要約してみましょう。

ご覧のとおり、フィルターは常に最初に実行され、インターセプターは適切なメッセージ本文のリーダーまたはライターを呼び出す直前に実行されます。

作成したフィルターとインターセプターを見ると、次の順序で実行されます。

  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. 名前バインディング

これまで見てきたフィルターとインターセプターは、すべての要求と応答に対して実行されるため、グローバルと呼ばれます。

ただし、これらは、名前バインディングと呼ばれる特定のリソースメソッドに対してのみ実行されるように定義することもできます。

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アプリケーションでそれらをどのように使用できるかを理解することに焦点を当てました。

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