[Java] Putting Hypermedia Back in REST with JAX-RS

原文はこちら。
https://community.oracle.com/docs/DOC-998953

あなたのJavaエンタープライズ・アプリケーションで本格的なハイパーメディアドリブンのREST APIを実装すると、変更に対してより柔軟性と弾力性が増します。よい副作用として、一つ一つのRESTリソースを文書化する必要性もなくなります。

誰もがRESTにしています。少なくとも誰もがやっていると主張しています。しかし、ハイパーメディアを使わずにビルドされている場合を除き、発見できるほとんどのWeb APIは、「RPCスタイル」と「HTTPセマンティクスを持つ基本的なリソース」の間にあります。

Real-World "REST" Examples

多くの実プロジェクトでは、任意のHTTP APIのことを間違ってRESTfulと呼んでいます。Listing 1の例を見てみましょう。
POST /doSomeAction
<someActionRequest>
    <param>12345</param>
</someActionRequest>
200 OK
<someActionResponse>
    <value>2345</value>
</someActionResponse>
Listing 1. RPC HTTP example (request and response)
これらのHTTP呼び出しは、実際にはHTTPを使ってメソッドを呼び出しています。doSomeActionというURLはメソッド名で、POSTが全ての呼び出しで使われています(データの読み取りにおいてもです)。そしてリクエストおよびレスポンスのボディ部分は入出力パラメータです。
これはSOAPエンベロープがないSOAP以外の何者でもないと結論付けることができます。

Resources and HTTP Semantics


REST APIのURLはアプリケーションのビジネス・エンティティを表しています。つまり、APIデザイン時には動詞ではなくオブジェクトの観点で検討する必要がある、ということです。
以下はユーザー管理モジュールのために、ユーザーオブジェクトをHTTPで公開します(Listing 2)。
GET /users
200 OK

<users>
    <user>
        <id>12345</id>
        <name>Duke</name>
        <motto>Java rocks!</motto>
    </user>
    <!-- some more users ... -->
</users>
Listing 2. Resources example (request and response)
ここでの違いは、URLがビジネスオブジェクト(全ユーザーのリスト)を反映しており、正しいHTTPメソッドを使ってリソースを読み取る、というところです。
クライアントが全ユーザーを読み取り、各ユーザーのリソースをフォローしたいという場合、URLの構築方法を知る必要があるでしょう。特定ユーザーのURLが/users/12345である場合、クライアントは暗黙のうちに一致するオブジェクトのIDを使って全ユーザーのURLを結合する必要があります。これはクライアントがサーバにのみ存在しているべきロジックと密に結合しています。
一般的な誤解は、RESTがいささか予測可能なURLであるということです。実際には、読み込み可能なURLがあると便利ですが、より重要なのは、サーバーはURLの制御に留まり、積極的に必要なリソースにクライアントを導く、という事実です。これはどうやって実現できるのでしょうか。
Listing 3は、リソースとHTTPセマンティクスを使ってユーザーを作成する例です。
POST /users

<user>
    <name>Duke</name>
    <motto>JAX-RS rocks!</motto>
</user>
201 Created
Location: /users/12345
Listing 3. Creating resources (request and response)
サーバはリクエストに対してHTTPステータス201 Createdを返し、Locationヘッダー・フィールドには新規作成されたリソースのURLが含まれています。クライアントはこのURLを利用して特定のユーザーのリソースにアクセスします。これは、もはやURLがクライアント側で作成されないことを意味します。つまりサーバが再びURLを管理下に置いています。

Enter Hypermedia


関連するリソースにアクセスする方法はハイパーメディアを使って指示できます。
リスト4に示すように、再びユーザ・リストの例を考えてみましょう。ただ今回はリソースのリンクを持っています。
GET /users
200 OK

<users>
    <user>
        <name>Duke</name>
        <motto>Java rocks!</motto>
        <link rel="self" href="/users/12345"/>
    </user>
    <!-- some more users ... -->
</users>
Listing 4. Resources with links example (request and response)
レスポンスにはもはやビジネスオブジェクトのIDは含まれておらず、対応するリソースへのリンクが含まれています。クライアントはアプリケーションの構造を事前に知らなくても、これらのリンクに従うことができますが、同一性だけを認知しておく必要があります。これはクライアントに対し、リンクそれ自身が、包含するオブジェクト(つまりユーザー)のリソースであることを伝えているのです。
その結果、関係性だけを知った上で、リンクを使ってクライアントに全ての必要なロケーションへ指示します。あとはサーバー次第です。
Listing 5は、別の局面でのリソースリンク・アプローチの例です。今回はブックストアの例をJSON形式で表現しています。
GET /books/12345
200 OK

{
    "name": "Java",
    "author": "Duke",
    // + availability, price
    "_links": {
        "self": "/books/12345",
        "add-to-cart": "/shopping_cart"
    }
}
Listing 5. Resource with several links (request and response)
リソースには同一性を示すリンクだけでなく、「ショッピングカートに入れる」機能にアクセスできるURLも含まれています。これがハイパーメディアを使用するメリットです。そのリンクは、ユーザーがカートに本を追加することができる場合にのみ含まれます。本を購入できる場所の例として書店を考えてみましょう。いくつかの前提条件に一致する場合にのみ、本をユーザーのショッピングカートに追加することができます。このビジネスロジック(例えば、在庫がある書籍のみを追加する)は、理想的には、冗長なロジックを防止するため、サーバー上にのみ存在します。
(本の在庫がある場合のみ、ショッピングカートボタンを見せるという)ロジックをクライアントに複製せずに、ロジックが適用される場合にのみレスポンスのボディにサーバはadd-to-cartのリンクを含めます。クライアントはadd-to-cartの関係を知っているので、ファンシーなボタンを表示し、そのボタンがクリックされれば、そのURLにアクセスします。
しかし、APIユーザーは、どのデータをadd-to-cartのURLに送信する必要がある、ということをどうやって知るのでしょうか。このアクションは、HTTPのGET呼び出しではなく、必要なデータ(例えば書籍ID、追加する書籍の個数、など)を伴ってPOST呼び出しをします。この情報を、文書化(つまり、そのURLにアクセスする方法の説明)、もしくは、より洗練されたハイパーメディア・アプローチを介して、APIに含める必要があります。
様々なレベルの管理・制御を可能にするいくつかのハイパーメディアを意識したコンテンツ・タイプがあります。  M. AmundsenによるH Factorに関するエントリを参照ください。
Hypermedia Types > H factor
http://amundsen.com/hypermedia/hfactor/
あるハイパーメディアを意識したコンテンツ・タイプにSirenがあります。これを使うと、リンクを定義できるだけでなく、いわゆるアクションも定義できます。シンプルなGET呼び出しを使う以外の方法でリソースへアクセスする方法を示します。
Siren: a hypermedia specification for representing entities
https://github.com/kevinswiber/siren
Listing 6はリンクとアクションを持つ書籍の例です。
GET /books/12345
200 OK

{
    "class": [ "book" ],
    "properties": {
        "isbn": "1-1234-5678",
        "name": "Java",
        "author": "Duke",
        "availability": "IN_STOCK",
        "price": 29.99
    },
    "actions": [
        {
            "name": "add-to-cart",
            "title": "Add Book to cart",
            "method": "POST",
            "href": "http://api.jamazon.example.com/shopping_cart",
            "type": "application/json",
            "fields": [
                { "name": "isbn", "type": "text" },
                { "name": "quantity", "type": "number" }
            ]
        }
    ],
    "links": [
        { "rel": [ "self" ], "href": "http://api.jamazon.example.com/books/1234" }
    ]
}
Listing 6. Siren book resource example (request and response)
クライアントはadd-to-cart機能を呼び出すためのあらゆる必要な情報、これにはURL、HTTPメソッド、Content-type、あらゆる必要なコンテントデータが含まれますが、これらをJSONプロパティの形式で受け取ります。ISBNと個数のフィールドの情報の出所だけは、知っておく必要があります。これはクライアント上に存在する必要があるビジネス・ロジックであることは明らかです。
その結果、クライアントは、アプリケーションのドメイン・モデル以外の前提知識を持たなくても、自己探索方式でAPIを使用してナビゲートすることができます。「利用方法」の情報はAPI自身に焼き付けてあるので、多くのドキュメントをここに保存することができます。
それに加えて、ハイパーメディアを使用した場合には、API変更に対してより高い柔軟性を持たせることができ、クライアントは対応することができます。
一般的には、APIが大きくなり、利用するクライアントの種類が増えれば増えるほど、REST APIをハイパーメディア駆動にすることはますます理に適います。

Enter Java EE

JAX-RSはJava EEでRESTful Webサービスを開発するための標準です。
Listings 7と8では、(Listing 5で示した)HTTP呼び出しを処理するJAX-RSリソースクラスとアノテーションが付いたPOJOを示しています。
@Path("books")
@Produces(MediaType.APPLICATION_JSON)
public class BooksResource {

    @Inject
    BookStore bookStore;

    @Context
    UriInfo uriInfo;

    @GET
    public List<Book> getBooks() {
        final List<Book> books = bookStore.getBooks();

        books.stream().forEach(u -> {
            final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(u.getId());
            u.getLinks().put("self", selfUri);
        });

        return books;
    }

    @GET
    @Path("{id}")
    public Book getBook(@PathParam("id") final long id) {
        final Book book = bookStore.getBook(id);

        final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(book.getId());
        book.getLinks().put("self", selfUri);

        // check if business logic applies
        if (bookStore.isAddToCartAllowed(book)) {
            final URI addToCartUri = uriInfo.getBaseUriBuilder().path(CartResource.class).build();
            book.getLinks().put("add-to-cart", addToCartUri);
        }
        return book;
    }
}
Listing 7. JAX-RS resource class
@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class Book {

    @XmlTransient
    private long id;
    private String name;
    private String author;

    @XmlElement(name = "_links")
    private Map<String, URI> links = new HashMap<>();

    // getters & setters
}
Listing 8. Book example POJO
getBooksリソース・ハンドラは、BookStore ManagedBeanを呼び出して、対応するURIを付加し、クライアントに返す本を取得します。注意いただきたいのは、Content-typeがapplication/jsonであっても、JAXB(Java Architecture for XML Binding)アノテーションを使って、JSON出力にJavaオブジェクトをマッピングできる、ということです。アプリケーションサーバに同梱されている主要なJSONマッピングフレームワークもまた、JAXBアノテーションを考慮します。
JAX-RSのUriInfo機能を使って、JAX-RSクラスから直接得た情報を使ってURIをプログラムで作成します。pathメソッドは@Pathアノテーションのpathの部分を連結します。getBookメソッドには、書籍の実際のIDで置換する必要があるパス・パラメーターが含まれています。これは、対応する引数を使ってbuildmethodを呼び出すことによって行われます。
UrlInfoコンポーネントのもう一つの有用な機能は、getBaseUriBuilderというビルダーです。これは与えられたHTTPリクエストに従って、プロトコル、ホスト、ポートを作成します。アプリケーションサーバが例えばApacheやnginxの背後にある場合、元のHTTPリクエストのリクエストプロトコル、ホストやポート、つまりはプロキシサーバのベースURLを使ってURIを作成します。これはJAX-RSの標準機能であり、設定作業とJava EEアプリケーションをITランドスケープとの密結合を最小限にとどめることができます。
getBookリソースハンドラは、基本的には1個の書籍のためにのみ同じことをします。そしてビジネスロジックが一致した場合、このリソースハンドラはadd-to-cartリンクも含みます。これが、ハイパーメディアを使ってクライアントの挙動を制御する方法の一例です。

Listing 9および10は、与えられた例における可能性のあるレスポンスです。
GET /hypermedia-test/resources/books
200 OK

[
  {
    "name": "Java",
    "author": "Duke",
    "_links": {
      "self": "http://localhost:8080/hypermedia-test/resources/books/1"
    }
  },
  {
    "name": "Hello",
    "author": "World",
    "_links": {
      "self": "http://localhost:8080/hypermedia-test/resources/books/2"
    }
  }
]
Listing 9. Books resource example
GET /hypermedia-test/resources/books/1
200 OK

{
  "name": "Java",
  "author": "Duke",
  "_links": {
    "self": "http://localhost:8080/hypermedia-test/resources/books/1",
    "add-to-cart": "http://localhost:8080/hypermedia-test/resources/shopping_cart"
  }
}
Listing 10. Book resource example
Sirenなどのコンテント・タイプを使ったより洗練された例は、レスポンスにより多くの情報を必要とします。Siren4Jのようなライブラリには、所望のハイパーメディア・コンテント・タイプでレスポンスを作成する機能があります。
Java library for the Siren Hypermedia Type Specification
https://github.com/eserating/siren4j/
しかしながら、外部ライブラリを使って依存性を増やさず、できる限り生成されるデプロイメントアーティファクトのサイズを小さくするため、ふつうのJava EE 7の機能を使う方法をご紹介しましょう。これはつまり、コンテント・タイプに応じてフィールドを含む独自のJavaクラスを作成する、もしくはプログラム的なアプローチを使う、ということを意味します。JSON-Pは、最大限JSONの構造をプログラムで作成できる機能を提供します。
JSR 353: JavaTM API for JSON Processing
https://jcp.org/en/jsr/detail?id=353
Listing 11は、bookリソースのレスポンスをJSON-Pを使い、プログラムで作成する例です。
@GET
public JsonArray getBooks() {
    return bookStore.getBooks().stream().map(b -> {
        final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(b.getId());
        final JsonObject linksJson = Json.createObjectBuilder().add("self", selfUri.toString()).build();
        return Json.createObjectBuilder()
                .add("name", b.getName())
                .add("author", b.getAuthor())
                .add("_links", linksJson).build();
    }).collect(Json::createArrayBuilder, JsonArrayBuilder::add, JsonArrayBuilder::add).build();
}

@GET
@Path("{id}")
public JsonObject getBook(@PathParam("id") final long id) {
    final Book book = bookStore.getBook(id);

    final URI selfUri = uriInfo.getBaseUriBuilder().path(BooksResource.class).path(BooksResource.class, "getBook").build(book.getId());
    final JsonObjectBuilder linksJson = Json.createObjectBuilder().add("self", selfUri.toString());

    if (bookStore.isAddToCartAllowed(book)) {
        final URI addToCartUri = uriInfo.getBaseUriBuilder().path(CartResource.class).build();
        linksJson.add("add-to-cart", addToCartUri.toString());
    }

    return Json.createObjectBuilder()
            .add("name", book.getName())
            .add("author", book.getAuthor())
            .add("_links", linksJson).build();
}
Listing 11. Book resource example using JSONP
StreamsやLambda式、メソッドハンドルといったJava 8の言語機能を使って、取得されたPOJOのJsonObjectsとJsonArraysをプログラムで作成しています。このアプローチを使うと、開発者はレスポンスの構造や利用するコンテント・タイプを完全に管理することができます。
実プロジェクトでは、ビジネス・オブジェクトをリンクやアクションを含むハイパーメディア・レスポンスにマッピングするためのこの機能は、Contexts and Dependency Injection (CDI) Managed Beanのような単独のコンポーネントに分割され、プロジェクト内の全てのRESTリソースクラスで再利用されることでしょう。
完全なサンプルは、以下のリンクからどうぞ。
Hypermedia with JAX-RS
https://github.com/sdaschner/jaxrs-hypermedia
サンプルには、リソースのリンクだけを含むハイパーメディアAPIを作成する方法や、様々なアプローチを使い、アクションを含める方法(Sirenを使って実現しています)が含まれています。

Conclusion


Java EE 7アプリケーションでハイパーメディア・ドリブンのREST APIを作成するために必要なものを全てご紹介しました。
どのレベルのハイパーメディアを適用することが理に適っているのか、どのHTTPコンテント・タイプを使うべきか、レスポンスを作成するにあたって、ふつうのJava EE 7のアプローチに比べて、3rdパーティの依存性(ライブラリ)を利用することは妥当なのかどうか、ということを設計するにあたり選択することが最も重要です。

See Also

About the Author


Sebastian DaschnerはフリーランスのJavaコンサルタント、ソフトウェア開発者、アーキテクトです。プログラミングやJava EEのエンスージアストであり、JCPへ参加し、JSR 370 Expert Group (JSR 370: JavaTM API for RESTful Web Services (JAX-RS 2.1) Specification)のメンバーです。また、GitHubで様々なオープンソースプロジェクトのハッキングをしています。6年以上Javaに関わってきました。Javaのほかに、LinuxやDockerのようなコンテナ技術のヘビーユーザーでもあります。自身のブログTwitterでコンピュータ・サイエンスのプラクティスを啓蒙しています。

0 件のコメント:

コメントを投稿