FURYU Tech Blog - フリュー株式会社

フリュー株式会社の開発者が技術情報を発信するブログです。

PlayでRESTful Webサービス

はじめまして。モバイル事業部の九岡です。

昨年9月にフリューに転職してきて、社会人としては今年で4年目になります。

JavaScriptScala、そして御坂美琴(重要)をこよなく愛するWebエンジニアです。

本ブログでは、私が個人的に気になっている技術(主にプログラミング言語やWebフレームワークになると思います)を「○○を作ってみた」という形でご紹介していきます。

早速ですが、今回はPlay frameworkでRESTfulなWebサービスを作ってみました。

Play frameworkって?

Play! frameworkは、2009年頃から開発が続けられている、個人的にイチオシのJavaScala向けWebフレームワークです。

発表当時は「Ruby on Rails for Java」のような取り上げられ方をされて話題になったのですが、何故か流行らなかった不遇のWebフレームワークです。

つい先日(2011/4/13)バージョン1.2がリリースされるなど、現在でも活発に開発が続いています。

RESTfulって?

RESTfulは簡単にいうと、「リソースごとにユニークなURIを振り、リソースに対する操作をHTTP METHODで区別する」ような構造のWebサービスのことです。

参考:REST – Wikipedia

詳細な説明は、実装例とあわせてします。

つくってみる

今回は、簡易な「画像共有サービス」をつくってみました。

巷の画像共有サービス(Flickrのようなもの)にはアカウント登録やコメント機能など色々なおまけがついていますが、ここでは画像を

  • アップロードできる
  • 検索できる
  • 閲覧できる
  • 変更できる
  • 削除できる

という5機能に絞ります。

これをREST的に解釈して、今回は以下の2つのリソースがあるということにします。

リソース URI
画像 /photos /photos/{id}
画像ファイル /photos/{id}/data

そして、これらのリソースに対する操作をそれぞれAPIとして切り出します。

5つの機能をそれぞれ「リソースとそれに対する操作(HTTP METHOD)」に割り当てて、今回は以下のように決めました。

API(METHOD + URI) 機能
POST /photos アップロード
GET /photos 検索
GET /photos/{id}/data 閲覧(アップロードされた画像ファイルそのもの)
UPDATE /photos/{id} 画像のメタデータ(今回はタイトルのみ)を変更
DELETE /photos/{id} 画像を削除する

API一覧

これを早速Playで実装してみます。

ひな形の作成

まず、playでアプリの雛形を作成します。
どんなアプリを実装するときも、この方法で作成した雛形を少しずつ変更していけばOKです。

% play new rest
~        _            _
~  _ __ | | __ _ _  _| |
~ | '_ \| |/ _' | || |_|
~ |  __/|_|\____|\__ (_)
~ |_|            |__/
~
~ play! 1.1.1, http://www.playframework.org
~
~ The new application will be created in /Users/mumoshu/Projects/sandbox/rest
~ What is the application name? [rest] <b>ここでENTER</b>
~
~ OK, the application is created.
~ Start it with : play run rest
~ Have fun!

これでカレントディレクトリのrestというディレクトリ以下にアプリが作成されました。

## routes追加

conf/routesに前述の5つのAPIのルート定義を書きます。

# 一覧ページ
GET     /photos                                 Photos.index
# 検索
GET     /photos.{&lt;json|xml>format}              Photos.index
# 画像ファイルのダウンロード
GET     /photos/{&lt;[0-9]+>id}/data               Photos.data
# アップロード
POST    /photos                                 Photos.upload
# 削除
DELETE  /photos/{&lt;[0-9]+>id}                    Photos.delete
# 変更
PUT     /photos/{&lt;[0-9]+>id}                    Photos.update

書式は「HTTP METHOD + パス + コントローラのクラス名.メソッド名」になっています。
指定したパスに、指定したメソッドでリクエストが来た場合、指定したコントローラのメソッドに処理を委譲する、という意味です。
直感的ですね!

コントローラの実装

routesに記載したメソッドをコントローラに実装します。 app/controllers/Photos.java

public class Photos extends Controller {
    public static void index(@Min(0) Integer start, @Min(1) Integer results) {
        if (validation.hasErrors() || start == null || results == null) {
            results = 5;
            start = 0;
        }

        List&lt;Photo> photos = Photo.all().from(start).fetch(results);

        if (request.format.equals("json")) {
            renderJSON(photos);
        } else {
            // Render html or xml.
            render(photos);
        }
    }

    public static void data(long id) {
        Photo photo = findPhotoOrNotFound(id);

        response.contentType = photo.data.type();
        renderBinary(photo.data.get());
    }

    public static void upload(@Valid Photo photo) {
        validateOrError();

        photo.save();
    }

    public static void update(@Required String title, @Required long id) {
        validateOrError();

        Photo photo = findPhotoOrNotFound(id);
        photo.title = title;

        photo.save();
    }
    public static void delete(long id) {
        Photo photo = findPhotoOrNotFound(id);

        photo.delete();
    }

    private static Photo findPhotoOrNotFound(long id) {
        Photo photo = Photo.findById(id);

        if (photo == null) {
            notFound("Photo for id " + id + " is not found.");
        }
        return photo;
    }

    private static void validateOrError() {
        if (validation.hasErrors()) {
            StringBuffer buffer = new StringBuffer();
            for (Error error : validation.errors()) {
                buffer.append(error.message() + ": " + error.getKey() + "\n");
            }
            error(buffer.toString());
        }
    }
}

レスポンスまわりのコードはPlay独特かもしれません。
ポイントは以下の3つです。
これ以外は見た目通りではないでしょうか。

renderJSON(photos)

で、Photoのリストをjsonシリアライズしてレスポンスに出力します。
簡単にいうと、出力したいオブジェクトをrenderJSONするだけで参照用のWeb APIが実装できます。

render(photos)

で、PhotoのリストをHTMLテンプレートに渡した上で描画します。
renderの引数に渡したphotosは、HTMLテンプレートからもphotosという名前で参照できます。

routesで以下のようなAPIを定義したことを覚えていますか?

GET     /photos                                 Photos.index
GET     /photos.{&lt;json|xml>format}              Photos.index

ここでは、/photosのパスに

の3パターンを許容しています。

render()を読んだ場合、リクエストされたときの拡張子に応じて描画するテンプレートを規約により勝手に決めてくれます。
例えば、Photos.indexの場合、拡張子を省略するとindex.html、.xmlならindex.xmlというテンプレートが使われます。

notFound

404 NOT FOUNDを返します。

モデルの実装

app/models/Photo.java

@Entity
public class Photo extends Model {
    @Required
    public String title;

    @Required
    public Blob data;

    public String dataUrl;

    @PrePersist
    @PreUpdate
    protected void onSave() {
        Map&lt;String, Object> args = new HashMap&lt;String, Object>();
        args.put("id", id);

        Router.ActionDefinition actionDefinition = Router.reverse("Photos.data", args);
        actionDefinition.absolute();

        dataUrl = actionDefinition.url;
    }
}

ここでも詳細は割愛しますが、ポイントは以下の3点です。

getter/setterが不要

Javaに詳しい方は違和感を感じるかもしれません。
Playのモデルではgetter/setterを書かなくてもOKです。
値のget/setしかしない上にサブクラスでオーバーライドすることもないのであればgetter/setterは要らないよね、というPlay開発者の思い切った判断ですね!

インターセプター(PrePersist, PreUpdate, etc)

Playは標準でいくつかのインターセプターを用意しています。
上記の例では、モデルをDBに永続化する前に必ず呼ばれるメソッドonSave()を実現するために使っています。

ビューの作成

app/views/Photos/index.html

#{extends 'main.html' /}
#{set title:'Photos.index' /}


<ul>
  #{list items:photos, as:'photo'}
  
  
  <li>
    ${photo.id}:${photo.title} <img src="@{Photos.data(photo.id)}" />
  </li>
  
      #{/list}
  
</ul>

#{form @Photos.upload(), enctype:'multipart/form-data'}


<input type="text" name="photo.title" />
<input type="file" name="photo.data" />
<input type="submit" name="submit" value="Upload" />
#{/form}

ここでの実装ポイントは4点です。

タグ

#{名前}#{/名前} でPlayに組み込まれているタグを呼び出せます。

リンク

@{コントローラ.メソッド(引数)} は、指定したコントローラのメソッドに対応するURLに置き換わります。

変数の利用

${変数} で、コントローラのrender()に渡した変数の値が参照できます。

POSTパラメータの指定

先ほどコントローラで

Photos.upload(Photo photo)

のようなメソッドを定義したことを覚えていますか? POSTするときに、photo.title、photo.dataというパラメータを指定すると、それぞれphotoにセットされた状態でコントローラのメソッドに渡されます。

app/views/Photos/index.xml

<?xml version="1.0"?>
&lt;photos>
#{list items:photos, as:'photo'}
&lt;photo>

        &lt;dataUrl>${photo.dataUrl}&lt;/dataUrl>
        &lt;data>
            &lt;UUID>${photo.data.UUID}&lt;/UUID>
            &lt;type>${photo.data.type}&lt;/type>
        &lt;/data>
    &lt;/photo>
#{/list}
&lt;/photos>

前述の通り、/index.xmlをリクエストした場合はこのテンプレートが使われます。

実行する

play test

で、Playに組み込みHTTPサーバが起動します。

http://localhost:9000/photos
にアクセスすると、Photos/index.htmlの内容が表示され、ページ内のフォームから画像のアップロードができます。

そして本題のRESTfulなAPIを使ってみます。
RESTfulなWebサービスの利点として、HTTP通信ができて、JSONXMLをデコードできる言語・環境ならどこからでもアクセスできますが、ここではお手軽にjQueryでアクセスします。

画像の更新であれば、

$.ajax({
  type: 'put',
  url: '/photos/1',
  data: {title: "変更後の写真タイトル"},
  success: function() {
    /* リクエスト成功時の処理 */
  },
  error : function() {
    /* リクエスト失敗時の処理 */
  }
});

というようなコードになります。
routesで定義したとおり、APIはHTTP METHODとパスにより区別されます。
更新APIはroutesでPUT /photos/{id}と定義したので、jQueryからリクエストするときのURLも’/photos/1’のようになります。

また、削除であれば

$.ajax({
  type: 'delete',
  success: function() { /* */ }
});

検索であれば

$.ajax({
  type: 'get',
  data: {start: /*取得開始位置*/0, results: /* 取得件数 */10},
  success: function(response) { /* */ }
});

という感じになります。

総括

  • Play frameworkでRESTfulなWebサービスを実装しました
  • Play frameworkのroutes, コントローラ、モデル、ビューの基本的な書き方を説明しました
  • ブラウザからWebページとして、HTTP経由でAPIとしてアクセスできることを示しました

駆け足の説明でしたが、Play frameworkを使うとお手軽にRESTfulなWebサービスが実装できることが、雰囲気だけでもわかっていただけたでしょうか?

ソースコードGitHubで公開しています。

github.com

試しに動かしてみたいという方はぜひgit cloneしてみてください。

次回は、このアプリのユニットテスト・ファンクションテストについて書きます。
テストのないコードはレガシーコードだ!!(挨拶)