概要
先日の JJUG ナイトセミナーで、梅澤 雄一郎さん @garbagetown が Play Framework を紹介されていましたので便乗してみます。Play は Scala ベースの2系が主流で、こちらは日本語でもたくさんドキュメントが書かれていますので、私は仕事で使ったことのある1系について書きます。
個人的な話
私が社会人になって最初に使ったのが Play の1系でした。先輩方が社内向けに改造した Play (version 1.2.5 ベース) を使っていて、社内の Web API 連携や新規の Web API 開発、あるいは PHP の代替としての Web ツール開発で活躍していました。今も重要なシステムがこのフレームワークのアプリケーションで開発され動いています。初めて使った時は Java なのにコンパイル不要で動くことに衝撃を受けました。
最近は別にいろいろと便利な Framework が出てきました。Framework 本体の開発体制に難があって学習コストがそれなりにかかる Play 1系 は残念ながら部内での採用が減ってきています。
Play 1系 とは
まず、 Play は Web Framework であり、決して Java の Framework ではないそうです。実際、1系ではコアの部分に Java ではなく Python が使われています。「Java の起動の遅さをクリアするために Python を採用した」と先日の JJUG ナイトセミナーで聞いて知りました。
2012年にバージョン 1.2 がリリースされ、Scala ベースで1系と完全に非互換の2系が本格的に指導してきてから、1系の開発は終了してしまったという雰囲気が漂っていました。1系から2系にそのまま移行することはできず、ある程度の追加学習とアプリケーションの改良が必要であるため、1系である程度の規模のアプリケーションを開発・運用してしまっていたプロジェクトでは Play 本体に手を加えながら細々と運用し続けていました。全社的に Java8 への移行に取り組んだ際に、この Play は 1.2 系が Javassist というライブラリの影響で Java8 で動作せずに数日調査に費やした末、1.3 へのアップグレードで対応しています。
2016年の1月、「Play 1系 は終わった」状態から3年ほど経過し、ふと1系は本当に完全終了したのかと思い出してダウンロードページを見てみたら、1.3 どころか 1.4 が存在していて、全然終わってませんでした。先日のナイトセミナーで聞いた話ですと、エストニアのコミッターの方(Andrei Solntsev 氏?)が現れたそうで、その方が中心的に現在の1系の改良を担ってくださっているらしいです。
Play 1系 の特徴
思い付く限りで書いてみます。公式ドキュメントへのリンクを一部追加してあります。
Java で Web アプリケーションを構築できる
今はいくらでも選択肢がありますが、使い始めた当時は Java SE で Web アプリケーションを構築する仕組みがさほど一般的でなかったような気がします。
Servlet が不要
デプロイ時は Play とアプリケーションをサーバに置いて、アプリケーションを起動するだけです。
Hot reloading
開発モードでは変更が即反映されます。アプリケーションの再起動が不要です。やはり当時は画期的なポイントでした。
IDE がいらないくらいの開発容易性&開発サポート
IDE との連携もサポートされていますが、コンパイラが組み込まれているので IDE なしでも十分開発が可能です。実際、私は先輩から「サーバにデプロイするならサーバで vim 使って開発した方が早い」という言葉を真に受けて1年以上開発サーバ上で直接 vim を使って開発をしていました。その経験を通して、まともな Java の開発には IDE のリファクタリング機能を使いこなすことが重要だと実感しました。
ちなみに、コンパイルエラーの時にアクセスするとこんな感じのエラーメッセージが表示されます(開発モードの時のみ)。
POSTパラメータの受け取りが楽
Controller クラスのメソッドに引数として書いておけば、 GET でも POST でも簡単に受け取ることが可能です。先週 Vert.x を使っていて 「Play は楽だったな」と思いました。
Groovy の HTML テンプレートエンジン
HTML のテンプレートを Groovy を使って記述することが可能です。 Java のオブジェクトを渡してテンプレート側で利用できます。
非同期ジョブ
アプリケーションとは別に非同期で何かしらの処理を実行させることが可能です。 play.jobs.Job クラスを継承することで定義できます。Job は起動時に Job クラスを継承したクラスを Framework が自動で検索し、 JobScheduler に登録されます。
この機能、一言でいうなら Java で cron を書くようなものです。大抵の場合は定期的に実行したい処理を Job を継承して実装します。使う際は UNIX の cron と書式が違うので注意してください。
参考
便利なライブラリ
play.libs という独自のライブラリが大量に実装されています。
play.libs.IO
I/O 処理で便利なメソッドが大量に用意されています。が、今なら NIO2 を使った方がよいでしょう。
play.libs.WS
非同期での HTTP 通信をサポートした HTTP クライアントライブラリです。com.ning の HTTP クライアントをラップして使いやすくしたものです。
例えば、 Yahoo! JAPAN のトップページに GET でアクセスし、レスポンスの Body を String で取得する、という操作は下記のコードで実装可能です。
String html = WS.url("http://www.yahoo.co.jp").get().getString();
簡潔でよいですね。
play.libs.Mail
メール送信を使いやすくしたライブラリです。
ライセンス
Apache License version 2.0です。ただ、依存ライブラリが多数あるので、そちらのライセンスも確認する必要があります。
Getting Start
環境
Key | Value |
---|---|
Java SE | 1.8.0_91 |
OS | Windows 10 |
Play | 1.4.2 |
zip のダウンロード
1系は Download の下の方にあります。せっかくなので最新の 1.4.2 を試してみましょう。
Java8 環境だと Javassist のバージョンが 3.18未満の Play1.x はコンパイルができずに動作しないのでご注意ください。
インストール作業に相当する作業
インストーラも Activator もなく、ファイルを置いて自分でパスを通すだけです。
ダウンロードしてきた play-1.4.2.zip を伸長します。生成された play-1.4.2 フォルダは任意のフォルダに配置し、環境変数 Path にそこへのパスを追加してください。
ターミナルを開いて play と打ち込んで実行してみましょう。上手くいっていれば、下記のバナーが表示されます。
$ play
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.4.2, https://www.playframework.com
~
~ Usage: play cmd [app_path] [--options]
~
~ with, new Create a new application
~ run Run the application in the current shell
~ help Show play help
~
おめでとう、Play 1.4.2 は正常にインストールされました。
新規のひな形プロジェクト作成
play コマンドにはさまざまなサブコマンドがあり、そのうちの new を使うとひな形プロジェクトを生成してくれます。new コマンドの引数には プロジェクト名を渡すことができます。渡さなくても new コマンド時にWhat is the application name?
と確認されるので、その時に名前を入力してもよいです。
$ play new play_example
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.4.2, https://www.playframework.com
~
~ The new application will be created in /path/to/play_example
~ What is the application name? [play_example]
~
~ OK, the application is created.
~ Start it with : play run play_example
~ Have fun!
~
ひな形プロジェクトの中身
$ tree /F
├─app
│ ├─controllers
│ │ Application.java
│ │
│ ├─models
│ └─views
│ │ main.html
│ │
│ ├─Application
│ │ index.html
│ │
│ └─errors
│ 404.html
│ 500.html
│
├─conf
│ application.conf
│ dependencies.yml
│ messages
│ routes
│
├─documentation
│ │ template.html
│ │ welcome.textile
│ │
│ ├─files
│ │ manual.css
│ │ wiki.css
│ │
│ └─images
│ logo.png
│
├─lib
├─public
│ ├─images
│ │ favicon.png
│ │
│ ├─javascripts
│ │ jquery-1.6.4.min.js
│ │
│ └─stylesheets
│ main.css
│
└─test
Application.test.html
ApplicationTest.java
BasicTest.java
data.yml
Folder name | Description |
---|---|
app | Model/View/Controller |
conf | routes や application.conf |
lib | 依存jarをここに入れるとクラスパスに自動で含める |
public | assets(JavaScript/CSS/image) |
test | テスト用 の Model/View/Controller やデータファイル |
app
app はさらに下記の通りの階層となっています。このうち、controllers と views は特別な意味を持つフォルダで、models はただの Java package フォルダです。このほか、Java のクラスはここにパッケージ階層に合ったフォルダを作って置いていきます。
Folder name | Description | File Extension | Language |
---|---|---|---|
app/controllers | Controller | .java | Java |
app/models | Model | .java | Java |
app/views | View | .html | HTML + Groovy |
試しに動かす
app や conf のある階層で play start コマンドを実行するとアプリケーションが起動します。特にオプションで指定しない限りは debug モードという開発用のモードで起動されます。
$ play start
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.4.2, https://www.playframework.com
~
~ using java version "1.8.0_91"
~ OK, /path/to/play_example is started
~ output is redirected to /path/to/play_example/logs/system.out
~ pid is 5424
~
http://localhost:9000
にアクセスすると、documentation フォルダに入っている HTML が表示されます。
また、適当なパス (例: http://localhost:9000/notexists )にアクセスすると、現在のアプリケーションでアクセス可能なパスを表示してくれます。これは開発モードでの機能であり、本番デプロイ時には本番モード(後述)を使うことで表示されないようにできます。
少しいじる
新しいパスを追加してみましょう。
Application.java の修正
Play では、 app/controllers 以下に置かれた、 Controller クラスを継承したクラスを起動時に(開発モード時は変更直後にも)読み込みます。
package controllers;
import play.*;
import play.mvc.*;
import java.util.*;
import models.*;
public class Application extends Controller {
public static void index() {
render();
}
}
ここに新しいメソッドを追加してみます。なお Play 1系では、 route と紐づけるメソッドは public static void
で宣言するというルールがあります。
public static void example() {
renderText("Hello world.");
}
renderText は引数の文字列を text/plain でレンダリングするメソッドです。
conf/routes にroute を追加
初期状態では下記のようになっています。
# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~
# Home page
GET / Application.index
# Ignore favicon requests
GET /favicon.ico 404
# Map static resources from the /app/public folder to the /public path
GET /public/ staticDir:public
# Catch all
* /{controller}/{action} {controller}.{action}
各パスで許容する HTTP メソッドを指定できる。右端ではパスに対しどの Controller クラスのどのメソッドを呼び出すかを指定します。このファイルに下記の1行を追記してください。
GET /example Application.example
debug モード時はアクセスがあるたびに自動で変更が反映されるので、リロードもしなくて大丈夫です。変更を加えたらすぐ http://localhost:9000/example
にアクセスしてみましょう。画面に Hello world.
と表示されていれば成功です。
パラメータの利用
先ほどの example メソッドを少し修正してみます。
public static void example(String name) {
renderText(String.format("Hello %s.", name));
}
http://localhost:9000/example
にアクセスすると Hello null.
と表示されます。では GET のリクエストパラメータで name を渡してみましょう。http://localhost:9000/example?name=toast
こうすると表示が Hello toast. に変わるはずです。
パラメータが指定されなかった場合、primitive 型はデフォルト値、参照型は null が入ります。
例えば次の場合、 a と b は int なので、指定されなかった時はデフォルトの 0 が入ります。
public static void example(int a, int b) {
renderText(String.format("a + b = %d.", a + b));
}
http://localhost:9000/example?a=1
にアクセスしてみてください。 a + b = 1.
と表示されるはずです。
パラメータが指定されなかったことを把握したい場合は参照型で引数を宣言し null チェックするか、
下記のように Controller クラスのオブジェクトが所有する request の params の data に含まれているかを確認する方法があります。
request.params.data.containsKey("b")
テスト
Play の Testは独自の実装となっており、単体テストなら UnitTest、機能テスト(HTTPを利用したrouting含めてのテスト)は FunctionalTest を継承してテストクラスを定義します。独自に定義された有用な assertion が利用可能となります。
例えば、下記のようなメソッドをテストしたい場合を考えましょう。
public static void example(String name) {
renderText(String.format("Hello %s.", name));
}
public static void add(int a, int b) {
renderText(String.format("a + b = %d.", a + b));
}
ひな形ではすでに下記の通り test/ApplicationTest.java が用意されているので、これにメソッドを追加します。
import org.junit.*;
import play.test.*;
import play.mvc.*;
import play.mvc.Http.*;
import models.*;
public class ApplicationTest extends FunctionalTest {
@Test
public void testThatIndexPageWorks() {
Response response = GET("/");
assertIsOk(response);
assertContentType("text/html", response);
assertCharset(play.Play.defaultWebEncoding, response);
}
}
下記の通り、2つテストメソッドを追加します。
@Test
public void testExample() {
Response response = GET("/example?name=toast");
assertIsOk(response);
assertContentType("text/html", response);
assertCharset(play.Play.defaultWebEncoding, response);
assertContentEquals("Hello toast.", response);
}
@Test
public void testAdd() {
Response response = GET("/add?a=1");
assertIsOk(response);
assertContentType("text/html", response);
assertCharset(play.Play.defaultWebEncoding, response);
assertContentEquals("a + b = 1.", response);
}
重要なのは下記2点です。
1. テスト対象の route を指定
Response response = GET("/example?name=toast");
2. 想定されるレスポンスのAssertを追加
今回は Plain text なので、レスポンスの body 全体をチェックします。
assertContentEquals("Hello toast.", response);
アプリケーションの停止
テストを実行する際はアプリケーションを停止しておきます。
$ play stop
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.4.2, https://www.playframework.com
~
~ OK, /path/to/play_example is stopped
~
テストの実行
コマンドは2種類あります。今回は auto-test を使います。 test 以下のテストをすべて実行するコマンドです。
$ play auto-test
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.4.2, https://www.playframework.com
~ framework ID is test
~
~ Running in test mode
~ Ctrl+C to stop
~
~ Deleting /path/to/play_example\tmp
~
~ using java version "1.8.0_91"
Listening for transport dt_socket at address: 8000
08:00:32,768 INFO ~ Starting /path/to/play_example
08:00:33,659 WARN ~ You're running Play! in DEV mode
~
~ Go to http://localhost:9000/@tests to run the tests
~
08:00:33,734 INFO ~ Listening for HTTP on port 9000 (Waiting a first request to start) ...
~ Server is up and running
~
~ Starting FirePhoque...
~ 3 tests to run:
~
~ BasicTest... PASSED 1s
~ ApplicationTest... FAILED ! 0s
~ Application... PASSED 4s
~
~ Some tests have failed. See file:///path/to/play_example\test-result for results
~
おや、1つ失敗したようです。テスト結果は test-result に出力されていますので確認しましょう。 ApplicationTest.class.failed.html を開いてみると……
Failure, Response contentType missing
In /test/ApplicationTest.java, line 21 :
assertContentType("text/html", response);
Show trace
java.lang.AssertionError: Response contentType missing
どう見てもコピペのせいです。本当にありがとうございました。
~ Server is up and running
~
~ Starting FirePhoque...
~ 3 tests to run:
~
~ BasicTest... PASSED 1s
~ ApplicationTest... PASSED 0s
~ Application... PASSED 4s
~
~ All tests passed
~
すべてのテストがパスすると、 test-result フォルダに result.passed というファイルが生成されます。
また、テストのコンパイルでエラーがあった場合はテストが実行されずに終了します。内容は test-result/application.log に記録されます。
Word cloud アプリケーションを作る
Controller 継承クラスを controllers パッケージに、view テンプレートを views に、それぞれ置かなくてはいけない点を除けば、比較的自由な階層で構成することが可能です。
メソッドの追加
必ず public static void で宣言してください。オブジェクト指向からは外れますが、Play 1系 を使う限りは我慢してください。
View へのパラメータ受け渡し
renderArgs というオブジェクトに put します。文字列や数値だけでなく Java のオブジェクトを渡すことも可能です。 View が Groovy なので、そちら側で オブジェクトを操作することができます。
renderArgs.put("wcData", new WordCloud().count(sentence));
renderArgs.put("paramSentence", sentence);
renderArgs.put("width", WIDTH);
renderArgs.put("height", HEIGHT);
共通して使うテンプレート
あるウェブサイトで共通したヘッダとフッタを持たせたい場合、共通して使うテンプレートとして定義したいと思います。 Play 1系 の場合は下記のような共通テンプレートを定義し、
<!DOCTYPE html>
<head>
<title>Word cloud</title>
<base href="/">
<script src="public/javascripts/d3/d3.js" charset="utf-8"></script>
<link rel="stylesheet" type="text/css" href="public/stylesheets/main.css"></link>
</head>
<body>
<header>
<img id="wikiIcon" src="public/images/icon.png">
<div id="logo">Word cloud</div>
</header>
#{doLayout /}
</body>
</html>
#{doLayout /}
の部分に挿入するサブテンプレートを下記の通り定義します。Groovy 部分は %{%}
で囲います。
#{extends "common_header.html"/}
<div class="main">
%{ if (paramSentence != null) { %}
<p>以下の文章のワードクラウドを表示しています。</p>
<p class="param_sentence">
${paramSentence}
</p>
%{ } %}
<p align="center">
<svg/>
</p>
<div class="input_area">
<form method="post" action="/wc" accept-charset="UTF-8">
<textArea class="input_box" name="sentence" maxlength="10000"
placeholder="ここに文章を入力してください。"></textArea>
<input type="submit" name="送信">
</form>
</div>
</div>
%{ } %}
#{extends "common_header.html"/}
のように、共通テンプレートを指定してください。
デフォルトのテンプレートのパス
Controller クラスの render メソッドを引数なしで呼び出した場合、 app/views フォルダの クラス名/メソッド名.html という名前のテンプレートを使おうとします。例えば、Application クラスの index メソッドで使うテンプレートは app/views/Application/index.html というパスに置きます。存在しない場合はコンパイルエラーが出力されます。
動かす
文章を入力し、送信を押すとワードクラウドが表示されます。
ほか
.gitignore
GitHub の標準では下記が定義されているようです。おそらく2系のものでしょうが、1系でも利用できます。
# Ignore Play! working directory #
bin/
/db
.eclipse
/lib/
/logs/
/modules
/project/target
/target
tmp/
test-result
server.pid
*.eml
/dist/
.cache
今回のソースコード
play_example(GitHub Repository)
本番モード
これまで使ってきた開発モードは変更があるたびに、最初の1回目のアクセス時に自動でコンパイルが実行されていました。本番モードでは起動時に1回だけコンパイルが実行され、以降は修正を反映することなく動き続けます。また、開発時には便利だったコンパイルエラーの表示や route 一覧も出なくなります。
本番モードを有効にするには、conf/application.conf に下記の設定を追記してください。
application.mode=prod
あるいは、起動コマンドで --%prod
をつけると本番モードで起動します。
$ play start play_example --%prod
~ _ _
~ _ __ | | __ _ _ _| |
~ | '_ \| |/ _' | || |_|
~ | __/|_|\____|\__ (_)
~ |_| |__/
~
~ play! 1.4.2, https://www.playframework.com
~ framework ID is prod
~
~ using java version "1.8.0_91"
~ OK, /path/to/play_example is started
~ output is redirected to /path/to/play_example\logs\system.out
~ pid is 9496
~
「framework ID is prod」と出ていれば本番モードで動いています。
まとめ
この記事ではPlay Framework 1.x の動作環境を用意し、簡単なアプリケーションを開発する方法を説明しました。
Play は Framework 本体のバージョンが上がるごとに下位互換性を切って機能を強化・洗練させていく方針のようですので、長い期間運用し続けるアプリケーションで採用するにはリスキーかもしれません。が、機能が豊富で、1回使い方を覚えれば開発しやすい Framework であることも確かです。1系は何より zip をダウンロードして伸長して Path を通せばそれで導入が終わってしまう導入の手軽さが 2系にない強みだと個人的に思っています。
趣味や小規模な開発での選択肢として覚えておくと、技術の幅が広がってよいかもしれませんし、Java の限界に挑んだ Guillaume Bort 氏の先鋭的なコードを眺めるのは Java 開発者の幅を広げるのに資する行動だと思います。
……まあ、本当に今から始めるなら Scala と Akka の勉強にもなる 2.x をやった方がよいのかもしれない、ということを否定するつもりは毛頭ありません。
参考
Play 1系 についてより深く知りたい方は下記のドキュメントをご覧ください。Play で何ができるか、あの機能を実装するにはどうすればよいか、等々はおおよそ公式ドキュメントに書いてあります。翻訳してくださった方に感謝の念が尽きません。
- Play 1系 の公式ドキュメント(日本語)……ここを見れば大体のことは書いてあります。
- Play 1系 の API ドキュメント
- Play 1系 のソースコード(GitHub)
余談
WSAsync クラスのコメントが興味深いです。「○しいお米セ○ウムさん」を思い出しました。ほかにもフランス語のコメントがごっそり英語で書き換えられていたり、いろいろと面白いコードが散見されますので、暇つぶしにリポジトリを眺めてみると面白い Framework です。
余談2
Play の Job や便利ライブラリが好きすぎて Play 以外でも使いたくなったので、個人的に移植してみたリポジトリへのリンクを貼っておきます。