Hatena::ブログ(Diary)

winplusの日記 このページをアンテナに追加 RSSフィード

2010-03-19

HTTP コンテント・ネゴシエーション

※オライリーから翻訳書『JavaによるRESTfulシステム構築』が出ましたので、そちらを参照してください。以下の記事はアテになりません。

以下『RESTful Java with JAX-RS』「Capter8:HTTP Content Negotiation」のメモ。目次はこちら

  • SOAアプリケーションには、さまざまなクライアントとやりとりできる柔軟さが要求される。たいていのプログラミング言語がHTTPプロトコルを話せるので、RESTfulサービスはこの点で有利。でも、それだけでは不十分。クライアントごとに異なったデータフォーマット(XML, JSON, YAMLなど)を要求するだろう。国際化したければ、情報の翻訳も必要になる。それに、古いクライアントが新しいサービスとやりとりできる方法も必要。
  • HTTPは、これらの問題点を簡単にあつかえる。いちばん重要な機能のひとつが、クライアントがサーバにどんな形式のレスポンスが欲しいのかを指定できるということ。HTTP コンテント・ネゴシエーション、略して conneg 。この章では、 conneg がどう機能するのか、JAX-RSがどうサポートするのか、そしてRESTfulサービスでこの機能を効果的につかう方法を説明する。

■conneg の説明

  • クライアントはサーバーに、返却して欲しいメディア・タイプを指定することができる。Accept リクエスト・ヘッダーに、欲しい形式の一覧をコンマ区切りで設定すればよい。(リクエストの例)クライアントは、XMLかJSONの形式で /stff を要求している。サーバが希望の形式で提供できないときは 406,"Not Acceptable"が返ってくる。
  • Accept ヘッダには、ワイルドカードも使える(リクエストの例)text/* というメディアタイプは、すべての text 形式を意味する。

◎選択順

  • レスポンスにつかうメディア・タイプを選択するための、明示的な規則と暗黙の規則がある。暗黙の規則は、より細かく特定されたメディア・タイプが優先されるというもの。(リクエストの例)クライアントがワイルドカードよりも具体的なメディアタイプをのぞんでいると、サーバは仮定する。

1. text/html;level=1

2. application/xml

3. text/*

4. */*

  • いちばん細かく指定している text/html;level=1 が最初で、次が、属性なしの application/xml。あとはワイルドカードのタイプで、text/* が先にくるのは、なんでも一致する */* よりは具体的だから。
  • クライアントは MIMEタイプ の q属性をつかうことでも優先順位を指定できる。この属性は0.0から1.0までの数値で、1.0がいちばん優先される。(リクエストの例)q属性が指定されていない場合は、1.0だと仮定される。

1. audio/mpeg

2. text/*

3. application/xml

4. */*

  • 暗黙の限定子1.0をもつ audio/mpeg が最初。次が、限定子0.9の text/*。application/xml の方が細かい指定だけど、優先順位が低いので3番目になる。これらのどれも提供できない場合は、なんでもいい。

■言語のネゴシエーション

  • クライアントは、 Accept-Language ヘッダをつかって、送り返して欲しいデータの自然言語を指定できる。(リクエストの例)この例では、英語、スペイン語、フランス語を要求している。ISO-639規格 による2文字で言語を指定する。ISO-3166 の2文字の国コードを続けることができる。たとえば、en-us は米国英語をあらわす。
  • Accept-Language ヘッダも限定子をサポートしている。(リクエストの例)クライアントは、スペイン語かフランス語がいいんだけど、それがない場合には英語でもいい。
  • クライアントとサーバは、メッセージ・ボディの自然言語を指定するために、Content-Language ヘッダーをつかう。

エンコードネゴシエーション

  • ネットワークの帯域を節約するために、メッセージを圧縮することがよくある。一般的にはGZIPがつかわれる。クライアントは、サポートするエンコーディングを指定するために、Accept-Encoding ヘッダーをつかう。(リクエストの例)GZIP圧縮されたものか、Deflate圧縮されたもの(deflate)を要求している。
  • Accept-Encoding ヘッダも限定子をサポートしている。(リクエストの例)実際のところ、クライアントは、サーバがどのエンコーディング方法をつかおうと気にしない。
  • クライアントとサーバは、メッセージ・ボディをエンコードする場合は、Content-Encoding ヘッダーが必須。

JAX-RS と conneg

  • JAX-RSは、conneg を管理する機能をいくつか備えている。Accept ヘッダによってメソッドの振り分けをおこなう。コンテントの情報を直接みる。多重に決定される場合をあつかうためのAPIもある。

◎メソッドの振り分け

  • これでの章で、@Producesアノテーションによるメソッドの振り分けをみた。Accept ヘッダにある一覧のうち優先されるメディア・タイプを、@Producesアノテーションで指定されたメタデータと照合している。(CoustomerResourceクラス)3つのgetメソッドがあって、ぜんぶ同じURIだけどデータフォーマットが違う。JAX-RSは Accept ヘッダが何であるかによって、3つから1つを選ぶ。(リクエストの例)

◎JAXB をつかって conneg を利用する

  • 6章でJAXBアノテーション JavaオブジェクトとXML・JSONとの変換をみた。JAX-RS と conneg との統合を利用すれば、両方の形式を1つのJavaメソッドで実装できる。(MyServiceのソース)getCustomer()メソッドは、@Produces で指定されたXMLとJSONのどちらでも、生成することができる。Customerクラスのオブジェクトが返されるが、これにはJAXBアノテーションが宣言されている。Acceptヘッダー内の情報で、返却されたJavaオブジェクトを変換するためにの MessageBodyWriter を選ぶ。

◎複雑なネゴシエーション

  • 単純な @Produces アノテーションでは間に合わない場合、ヘッダーの値を直接みるコードか、conneg を扱うJAX-RS APIをつかったコードを書く必要がある。

○Accept ヘッダをみる

  • 5章で javax.ws.rs.core.HttpHeaders を紹介した。このインターフェイスは、前処理されたconneg の情報をいくつか含んでいる。(HttpHeadersのソース)getAcceptableMediaTypes()メソッドは Acceptヘッダーにあるメディアタイプのリストを含む。メディアタイプはjavax.ws.rs.core.MediaType で表現される。戻り値は q の値でソートされている。
  • getAcceptableLanguages()メソッドは Accept-Languages をあつかう。 java.util.Locale のリストで、同じく q の値でソートされている。
  • @javax.ws.rs.core.Contextアノテーションを指定することで、HttpHeadersを注入する(MyServiceクラスのソース)HttpHeadersを注入されたオブジェクトから直接要請されたメディアタイプと言語をとりだし、それを適用した ResponseBuilder から Response オブジェクトをつくっている。

○いろいろな処理

  • メディアタイプ、言語、エンコーディングによる複数の集合と取り組む必要がある場合のために、JAX-RSはあるAPIを持っている。javax.ws.rs.core.Request インターフェイスと javax.ws.rs.core.Variantクラスをつかうことができる(Variantクラスのソース)Variantクラスは、メディアタイプ、言語、エンコーディングを含む単純な構造体。JAX-RSリソースメソッドがサポートする一組をあらわす。Requestインタフェースとやりとりするために、このオブジェクトのリストをつくる(Requestインタフェースのソース) selectVariant()メソッドは、Variantオブジェクトのリストをとる。リクエストの Accept・Accept-Language・Accept-Encoding の各ヘッダーとVariantのリストを比較し、リクエストにいちばんふさわしいものを選ぶ。一致するものがないばあいは、nullを返す。(MyServiceクラスのgetSomething()メソッド) XML、JSON、英語、スペイン語、deflate圧縮、GZIPエンコードをサポートしていると伝えるためとはいえ、getSomething()メソッドは長すぎる。selectVariant() API をつかって手動で選択する、というのはたいてい避けた方がいい。JAX-RSは、こんな複雑な選択を簡単にできるように、javax.ws.rs.core.Variant.VariantBuilder を提供している。(VariantListBuilderクラスのソース)一連のメディアタイプ、言語、エンコーディングを、VariantBuilderクラスに追加することができる。すると、これらの可能なすべての組み合わせを自動的に生成する。(VariantListBuilderクラスのソース)mediaTypes(), languages(), encodings()メソッドをよびだすことで、VariantListBuilderとやりとりできる。アイテムの追加がおわったら、build()メソッドを呼び出して、Variantのリストを生成する。
  • 2つか3つの異なった組み合わせが必要なら、VariantBuilder.add()メソッドが区切りになる。アイテムが追加されたときの組にもとづいて、Variantのリストを生成する。(VariantListBuilderをつかったソース)英語、スペイン語、フランス語の text/plain を圧縮形式だけでサポートする、という組が追加された。
  • 現実的には、 Request.selectVariant() API の出番は多くない。ひとつめの理由は、エンコーディングを追加するのは簡単じゃない。たいていのJAX-RS実装はGZIPをサポートしていて、コードを書くことなく使えるから、これを使えばよい。
  • ふたつ目に、@Producesアノテーションと Acceptヘッダにもとづいて自動的にメディア・タイプが決まる。メディア・タイプをサポートしないで、言語だけを指定することはない。ほどんどの場合、関心があるのは言語だけで、これは HttpHeaders.getAcceptableLanguages() で簡単に手に入る。

URIパターンによるネゴシエーション

  • conneg は強力。でも、クライアントとくにブラウザがサポートしていないという問題がある。たとえば Firefox は Acceptヘッダを以下のようにハードコーディングしている。(リクエスト・ヘッダ)ブラウザをつかってJSONで表現された情報をみたいとしても無理。
  • このようなクライアントをサポートする一般的なパターンは、conneg の情報を Acceptヘッダを通すかわりにURIに埋め込んでしまうというもの。(URIの例)情報は、URIのパスかファイルの接尾辞として埋め込まれている。簡単な@Path式のパターンをつくれば、JAX-RSリソースメソッドの中でこれをモデルにできる。(getCustomer()のソース)
  • JAX-RSの仕様が最終版になるまで、ファイル名の接尾辞を中心とした仕組みが含まれていた。エキスパート・グループがその意味論で合意できなかったので、結局外された。JAX-RS実装はまだこの機能をサポートしているが、どのように動くのかを入念に調べるのが肝心。
  • 仕様できまっていた方法でいまJAX-RSの実装で動く方法は、メディアタイプと言語を、ファイルの接尾辞に対応させるというもの。xmlという接尾辞は、application/xml に、en という接尾辞は en-US に対応する。JAX-RSの実装は、conneg の情報を Accept あるいは Accept-Language のかわりに、接尾辞からとりだす。(getCustomer()のソース) GET /customers.json というリクエストがきたら、.json接尾辞を取り出して、リクエストのパスから削除する。それからjsonと一致するメディアタイプをみつける。json は application/json に対応しているので、 Acceptヘッダーのかわりにこの情報をつかって、getJson()メソッドに振り分けられる。

■コンテント・ネゴシエーションを活用する

  • これまでの例では、たんにXMLとかJSONのようなよく知られたメディアタイプを区別するためにconneg をつかった。すごく役に立つが、これが conneg の主な目的ではない。時間が経過すれば、ウェブサービスは発展する。新機能の追加によりデータフォーマットが変化するだろう。この変更をどう管理する?古いバージョンのサービスでしか動かないクライアントはどうする?conneg 中心としたモデリングで、これらの課題に対応できる。

◎新しいメディアタイプをつくる

  • RESTの重要な原則のひとつは、リソースの複雑さをデータフォーマットに集中するということ。URIとメソッドは固定されているが、データフォーマットは発展させることができる。
  • クライアントは、データフォーマットの異なるバージョンを手に入れるために、メディアタイプをつかえる。アプリケーションで新しいメディアタイプを定義するのが、一般的な対応方法。接頭辞vndをつけて新しいフォーマット名に、"+”で区切って具体的なメディアタイプをつづけるという規約にする。(メディアタイプ) vnd は vendor、rht は Red Hat、もちろん customers は 顧客のデータフォーマットということ。+xml はこのフォーマットが XMLにもとづいていることを知らせる。JSONでも同様(メディアタイプ)この Red Hat形式 のメディアタイプ名をもとに、バージョン情報を付け加える。(メディアタイプ)サブタイプ名はそのままにして、バージョンを特定するために属性をつかった。自作のメディアタイプにバージョン属性を指定するのは、一般的なパターン。

◎柔軟なスキーマ

  • バージョンをメディアタイプで管理するのはいいんだけど、変更を管理する主要な方法にしてはいけない。データフォーマットを決めるときは、後方互換性にとくに注意すべき。
  • 最初のスキーマは、拡張されたあるいは独自の要素と属性を、それぞれすべてのスキーマ受け入れらるようにしておくべき。(customerのXML表現)この例では、id属性に、どんな属性をでも追加できる。また、first と last の属性に、どんなXML要素でも追加することができる。新しいデータフォーマットが初期のデータ構造を含んでいるなら、古いクライアントは古いバージョンのスキーマそのままで、新しいバージョンのスキーマはそれとして処理できる。
  • スキーマが発展するにつれて、新しい属性や要素が追加できるけれど、それらはオプションであるべき。(customerのXML表現) street, city, state, それに zip を追加したが、それらをオプションにした。古いクライアントは古いバージョンのデータフォーマットで、PUTもPOSTもできる。

■まとめると

  • この章では、HTTPコンテント・ネゴシエーションの機能と、この機能を利用したJAX-RSにもとづくウェブサービス書き方を学んだ。データフォーマット、言語、およびエンコーディングをクライアントが指定し、JAX-RSがこれを扱うための方法をみた。データフォーマットを作成し、独自のメディアタイプを定義するにあたっての、一般なガイドラインを議論した。第21章で、本章のコードを試運転する。

スパム対策のためのダミーです。もし見えても何も入力しないでください
ゲスト


画像認証

トラックバック - http://d.hatena.ne.jp/winplus/20100319/1269002150