前回からの続き。
今回はフィルタについてです。
- フィルタ
- フィルタ適用のタイミング
- リクエストフィルタ
- リクエストフィルタの動作
- レスポンスフィルタ
- Reader Interceptor と Writer Interceptor
- Reader Interceptor と Writer Interceptor の実装例
- フィルタとインタセプタの適用順序
- DynamicFeature
- Name binding
フィルタ
JAX-RS のフィルタは Servlet のフィルタと異なり、リクエストフィルタとレスポンスフィルタが2つに分かれています。
リクエストフィルタとレスポンスフィルタはそれぞれ以下のインターフェースを実装して JAX-RS コンテナに登録することで適用されます。
javax.ws.rs.container.ContainerRequestFilter
javax.ws.rs.container.ContainerResponseFilter
フィルタ適用のタイミング
リクエストフィルタは適用タイミングが2つあります。
- JAX-RS リソースクラスのメソッド特定前(PreMatching)
- JAX-RS リソースクラスのメソッド特定後(PostMatching)
擬似コードで表すと以下のようになります。
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>
実行するとフィルタによりメソッド上書きができていることが分かります。
レスポンスフィルタ
レスポンスフィルタは以下のインターフェースを実装して定義します。
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 が追加されているのが分かります。
Reader Interceptor と Writer Interceptor
HTTPヘッダだけでなく、メッセージボディの変更を行うには ReaderInterceptor
と WriterInterceptor
を使います。
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: Designing and Developing Distributed Web Services (English Edition)
- 作者:Bill Burke
- 出版社/メーカー: O'Reilly Media
- 発売日: 2013/11/15
- メディア: Kindle版