読者です 読者をやめる 読者になる 読者になる

Jersey Grizzly で始める JAX-RS 入門 〜STEP6〜


前回からの続き。

今回はフィルタについてです。

フィルタ

JAX-RS のフィルタは Servlet のフィルタと異なり、リクエストフィルタとレスポンスフィルタが2つに分かれています。

リクエストフィルタとレスポンスフィルタはそれぞれ以下のインターフェースを実装して JAX-RS コンテナに登録することで適用されます。

  • javax.ws.rs.container.ContainerRequestFilter
  • javax.ws.rs.container.ContainerResponseFilter

フィルタ適用のタイミング

リクエストフィルタは適用タイミングが2つあります。

擬似コードで表すと以下のようになります。

for (filter : preMatchFilters) {
    filter.filter(request);
}

jaxrs_method = match(request);

for (filter : postMatchFilters) {
    filter.filter(request);
}

response = jaxrs_method.invoke();

for (filter : responseFilters) {
    filter.filter(request);
}

javax.ws.rs.container.ContainerRequestFilter を実装したフィルタは、通常 PostMatching のタイミングで呼び出されます。 フィルタに @javax.ws.rs.container.PreMatching アノテーションを付けることで PreMatching のタイミングで処理を行うフィルタが定義できます。

アノテーションを付けなかった場合は PostMatching となります。

リクエストフィルタ

リクエストフィルタは以下のインターフェースを実装して定義します。

public interface ContainerRequestFilter {
    public void filter(ContainerRequestContext requestContext) throws IOException;
}

例えば Firewall で PUT と DELETE メソッドが禁止されてる場合など、POST や GET メソッドで処理を代用したい場合があります。

この場合は POST や GET でリクエストを行い、HTTP ヘッダに X-HTTP-Method-Override=DELETE のように上書き用のメソッドを指定して代用する場合があります。

このようなケースではリソースクラスの JAX-RS メソッドが特定される前(HTTPメソッドによりJAX-RSメソッドが決まる前)にフィルタを適用する必要があります。

@PreMatching を指定して以下のようにフィルタを定義します(jersey にはこの用途であらかじめ org.glassfish.jersey.server.filter.HttpMethodOverrideFilter が用意されているため、通常はこちらを使うとよいでしょう)。

@Provider
@PreMatching
public class HttpMethodOverrideFilter implements ContainerRequestFilter {
    @Override
    public void filter(ContainerRequestContext requestContext) throws IOException {
        String methodOverride = requestContext.getHeaderString("X-Http-Method-Override");
        if (methodOverride != null)  requestContext.setMethod(methodOverride);
    }
}

man.java のリソース設定で、作成したフィルタのパッケージ("example.web.filter")を指定しましょう。

    public static class RsResourceConfig extends ResourceConfig {

        public RsResourceConfig() {
            packages("example.web.resource");
            packages("example.web.filter");
        }
    }

フィルタの登録は以下のように直接フィルタクラスを登録することもできます。

    public static class RsResourceConfig extends ResourceConfig {

        public RsResourceConfig() {
            packages("example.web.resource");
            register(HttpMethodOverrideFilter.class);
        }
    }

register にて直接クラスを登録した場合には、フィルタクラスに @Provider を付ける必要はありません。

リクエストフィルタの動作

X-Http-Method-Override ヘッダを付けてGETリクエストを行うよう index.html を変更してみましょう。

    <h2>Delete Customer(X-Http-Method-Override)</h2>
    <div class="row">
        <div class="col-xs-2">
            <input type="text" id="idXDeleteRequestId" class="form-control" value="1" placeholder="ID">
        </div>
        <div class="col-xs-8">
            <button type="button" id="idXDeleteRequestButton" class="btn btn-default">Delete</button>
            <font id="idXDeleteResCode" color="red"></font>
        </div>
    </div>
    <script>
        document.getElementById("idXDeleteRequestButton").addEventListener("click", function(){
            var request = new XMLHttpRequest();
            var path = '/customers/' + document.getElementById("idXDeleteRequestId").value
            request.open('GET', path, true);
            request.setRequestHeader('X-Http-Method-Override', 'DELETE');
            request.onload = function() {
                document.getElementById("idXDeleteResCode").textContent = request.status;
            };
            request.send();
        });
    </script>

実行するとフィルタによりメソッド上書きができていることが分かります。

f:id:Naotsugu:20160418234245p:plain

レスポンスフィルタ

レスポンスフィルタは以下のインターフェースを実装して定義します。

public interface ContainerResponseFilter {
    public void filter(ContainerRequestContext requestContext, 
            ContainerResponseContext responseContext) throws IOException;
}

レスポンスに Cache-Control ヘッダを付けるには以下のように実装することができます。

@Provider
public class CacheControlFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext requestContext,
                       ContainerResponseContext responseContext) throws IOException {
        if (requestContext.getMethod().equals("GET")) {
            CacheControl cacheControl = new CacheControl();
            cacheControl.setMaxAge(100);
            responseContext.getHeaders().add("Cache-Control", cacheControl);
        }
    }
}

このフィルタも、リクエストフィルタと同じように example.web.filter パッケージに作成しましょう。 @Provider を付与しているので自動でコンテナ登録されます。

実行するとレスポンスヘッダに Cache-Control が追加されているのが分かります。

f:id:Naotsugu:20160418234607p:plain

Reader Interceptor と Writer Interceptor

HTTPヘッダだけでなく、メッセージボディの変更を行うには ReaderInterceptorWriterInterceptor を使います。

  • javax.ws.rs.ext.ReaderInterceptor
  • javax.ws.rs.ext.WriterInterceptor

ReaderInterceptor は以下のようなインターフェース定義となっています。

public interface ReaderInterceptor {
    public Object aroundReadFrom(ReaderInterceptorContext context)
            throws java.io.IOException, javax.ws.rs.WebApplicationException;
}


WriterInterceptor は以下のようなインターフェース定義となっています。

public interface WriterInterceptor {
    void aroundWriteTo(WriterInterceptorContext context)
            throws java.io.IOException, javax.ws.rs.WebApplicationException;
}

Reader Interceptor と Writer Interceptor の実装例

メッセージボディの圧縮を行うインタセプタを以下のように書くことができます。

@Provider
public class GZIPEncoder implements WriterInterceptor {
    public void aroundWriteTo(WriterInterceptorContext ctx) throws IOException, WebApplicationException {
        GZIPOutputStream os = new GZIPOutputStream(ctx.getOutputStream());
        ctx.getHeaders().putSingle("Content-Encoding", "gzip");
        ctx.setOutputStream(os);
        ctx.proceed();
        return;
    }
}

ctx.proceed() により登録されている次のインタセプタの呼び出しが行われます。 これ以上登録が無い場合には MessageBodyWriter.writeTo() によりレスポンスボディの書き出しが行われます。

メッセージボディの伸張を行うインタセプタは以下のように書くことができます。

@Provider
public class GZIPDecoder implements ReaderInterceptor {
    public Object aroundReadFrom(ReaderInterceptorContext ctx) throws IOException, WebApplicationException {
        String encoding = ctx.getHeaders().getFirst("Content-Encoding");
        if (!"gzip".equalsIgnoreCase(encoding)) {
            return ctx.proceed();
        }
        GZipInputStream is = new GZipInputStream(ctx.getInputStream());
        ctx.setInputStream(is);
        return ctx.proceed(is);
    }
}

フィルタとインタセプタの適用順序

フィルタとインタセプタの適用順序は @javax.annotation.Priority をフィルタまたはインタセプタに付与することで定義します。

javax.ws.rs.Priorities には事前定義の定数が定義されています。

package javax.ws.rs;

public final class Priorities {
    private Priorities() { }

    /** Security authentication filter/interceptor priority. */
    public static final int AUTHENTICATION = 1000;

    /** Security authorization filter/interceptor priority. */
    public static final int AUTHORIZATION = 2000;

    /** Header decorator filter/interceptor priority. */
    public static final int HEADER_DECORATOR = 3000;

    /** Message encoder or decoder filter/interceptor priority. */
    public static final int ENTITY_CODER = 4000;

    /** User-level filter/interceptor priority. */
    public static final int USER = 5000;

}

値の小さいものが優先的に処理されます。

以下のようにフィルタにアノテーションを付けることで指定します。

@Provider
@Priority(Priorities.HEADER_DECORATOR)
public class PoweredByResponseFilter implements ContainerResponseFilter {
    @Override
    public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
        throws IOException {
            responseContext.getHeaders().add("X-Powered-By", "Jersey :-)");
    }
}

フィルタの登録時に以下のように Priority 指定することもできます。

    public static class RsResourceConfig extends ResourceConfig {
        public RsResourceConfig() {
            packages("example.web.resource");
            register(CacheControl.class, Priorities.HEADER_DECORATOR);
        }
    }

DynamicFeature

フィルタやインタセプタをカスタマイズするには javax.ws.rs.container.DynamicFeature を使います。

DynamicFeature は以下のインターフェース定義となっています。

public interface DynamicFeature {
    public void configure(ResourceInfo resourceInfo, FeatureContext context);
}

このインターフェースを介して、アプリケーションの初期化時にカスタマイズした処理を行うことができます。


例えば、example.web.filter.PoweredByResponseFilter.java を以下のようにコンストラクタで設定値を取るようにしてみます。

@Priority(Priorities.HEADER_DECORATOR)
public class PoweredByResponseFilter implements ContainerResponseFilter {
    private final String version;
    public PoweredByResponseFilter(String version) {
        this.version = version;
    }

    @Override
    public void filter(ContainerRequestContext requestContext,
                       ContainerResponseContext responseContext) throws IOException {
        responseContext.getHeaders().add("X-Powered-By", "Jersey :" + version);
    }
}

このフィルタは DynamicFeature の実装クラス example.web.filter.PoweredByFeature.java を以下のようにすることでアプリケーションの初期化時にフィルタの初期処理を行うことができます。

@Provider
public class PoweredByFeature implements DynamicFeature {

    @Override
    public void configure(ResourceInfo ri, FeatureContext ctx) {

        if (CustomerResource.class.equals(ri.getResourceClass())
                && ri.getResourceMethod().getName().equals("createCustomer")) {
            ctx.register(new PoweredByResponseFilter("2.22"));
        }

    }
}

ここでの例では、特定のメソッドに合致する場合にのみフィルタを登録するようにしています。

特定のメソッドだけにフィルタを適用したい場合、フィルタの初期化処理を行いたい場合などに DynamicFeature を使うことができます。

Name binding

フィルタやインタセプタは javax.ws.rs.NameBinding にてクラス単位またはメソッド単位で適用範囲を制限することができます。

NameBinding は以下のアノテーション定義となっています。

@Target(ElementType.ANNOTATION_TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NameBinding { }


このアノテーションを付けた新しいアノテーションを定義します。

@NameBinding
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PoweredBy {}

このアノテーションを対象のフィルタに付け、

@PoweredBy
@Priority(Priorities.HEADER_DECORATOR)
public class PoweredByResponseFilter implements ContainerResponseFilter {
    ・・・
}

対象とするリソースクラスに付けることでフィルタやインタセプタの適用箇所を指定することができます。

@PoweredBy
@Path("customers")
public class CustomerResource {
    ・・・
}

もちろんクラスだけでなく、メソッドに指定することもできます。


ここまでのソースはGithubを参照してください。



RESTful Java with JAX-RS 2.0

RESTful Java with JAX-RS 2.0