Play framework
Play framework について何か備忘録でも書いていこうかと思ったけど(Wicketの記事みたいに)、ドキュメントが充実しているのでその必要は無かった。わからないことがあった場合はとりあえずここを見ればほとんどの場合解決する。かなりわかりやすく書いてあってとても便利。他には、codezineの記事もわかりやすい。
StrutsやWicketなどの他のJava製Webアプリケーションフレームワークと比べていろんな部分で簡単で作りやすいうえ、DBアクセスのための機能も備わっているのがありがたい(しかも普通のJPAを素で使うよりもずっと簡単)。eclipseにインポートするための設定ファイルを作ってくれたり、ホットデプロイ(と呼んでいいのかそもそも疑問だが)によりサーバを起動しっぱなしでソースの修正や動作確認が行えたり、エラー時にエラー箇所をバッチリ表示してくれたりと、今まで無駄になっていた時間を削ってくれるのもすばらしい。
でも、Java初心者の人がこれを使って何かを作るのはおすすめしない。Javaの作法をあえて無視して作られているから、それがクセになると後でいろいろ困ると思うよ。
とはいえ何も無いのも寂しいのでいくつかメモを…
ストアドを呼び出す
CallableStatement でストアドを呼び出したいから Connection が欲しいよーという場合は、
- application.conf で「db=mysql:user:pwd@database_name」を使わずに「db.url」~「db.pass」を設定する。その際「db.url」の引数に「noAccessToProcedureBodies=true」を追加する。
- DB.getConnection() で java.sql.Connection を取得する。
これで Connection を取得してあとは Connection#prepareCall(うんたら) で CallableStatement を作成すればいい。もちろん play.db.jpa.Model を継承したクラスもちゃんと使えるのでご心配なく。
Cacheクラスについて
前述のドキュメントを読むとわかるとおり、Playにおいて「Session」と呼んでいるものは実際には「Cookie」のことだ。サーブレットでいうSessionに近いものは「Cache」になる。正しくは違うものだけどサーバサイドで情報を保持するという観点から見れば似たようなもの。
ところが、デフォルト設定ではCacheクラスを使用して保存したデータはJVMのメモリ上に保存されてしまう。Playはステートレスを売りにしているフレームワークなのにこれじゃサーバの分散化に対応できず台無し。
しかし!PlayはMemcachedに対応している!なんと設定ファイルにMemcachedの設定を記述する(というかコメントアウトを外す)だけで、コードはそのままにデータをMemcachedに保持することができてしまうのであった。めでたし。
というわけにはいかなかった。コーディング次第ではJVMで動いてもMemcachedで動かないこともある。
String key = "key"; String ID = IDを取得する処理(); Set<String> set = Cache.get(key, Set.class); if (set == null) { set = new HashSet(); Cache.set(key, set); } if (!set.contains(ID)) { 処理(); set.add(ID); }
IDの重複チェックをして、重複していないときのみ処理を行うというようなロジック。デフォルトのJVM上にキャッシュする場合は正常に動くのに、Memcachedを使用する設定にすると何度通しても三行目でカラのSetが取れてしまう。一度目(nullの場合)は6行目でちゃんとセットしているしなぁ。二度目は何かしらaddされたSetが取れるはず。
これを…
String key = "key"; String ID = IDを取得する処理(); Set<String> set = Cache.get(key, Set.class); if (set == null) { set = new HashSet(); //Cache.set(key, set); } if (!set.contains(ID)) { 処理(); set.add(ID); } Cache.set(key, set);
このように、インスタンスに変更を加えたら改めてCacheにセットするようにしないと、反映されない。
これでちょっとつまづいたのでメモしておく。
また、開発モード(application.mode=dev)で動かしている際、コードに変更を加えて保存するとすぐに反映されるから便利なんだけど、Memcachedを使う設定にしているとCacheを取り扱うところでこけることがある。
play.exceptions.JavaExecutionException: Shutting down at play.mvc.ActionInvoker.invoke(ActionInvoker.java:290) at Invocation.HTTP Request(Play!) Caused by: java.lang.IllegalStateException: Shutting down at net.spy.memcached.MemcachedClient.checkState(MemcachedClient.java:240) at net.spy.memcached.MemcachedClient.addOp(MemcachedClient.java:255) at net.spy.memcached.MemcachedClient.asyncGet(MemcachedClient.java:729) at play.cache.MemcachedImpl.get(MemcachedImpl.java:92) at play.cache.Cache.get(Cache.java:172) ...
こういうときはPlayを再起動すると直る。めんどくさいので開発中はMemcachedを使うのはよそう。
Eclipse使用時のCRUDとSecureのコンパイルエラー回避について
PlayではCRUDやSecureという機能を使用することができる。使用方法はそれぞれのページにあるようにすればいいだけなのだが、その通りにやってもEclipse上でエラーが出てしまう。
たとえば以下のようなクラスを書くと赤文字の部分でエラーと判断される。
package controllers; import play.mvc.With; @With(Secure.class) public class Hoges extends CRUD { }
Eclipse上でエラーが出ているだけで動作には問題ないのだが気持ち悪いし、サーバを起動するたびにダイアログが出るのでやはりエラーは消しておきたい。
こういう場合は、プロジェクトに対して再度 play eclipsify
を行うことで、ソースがリンクされてコンパイルエラーが解消される。
CRUD使用時の注意
CRUDを使おうとしてしばらく迷ってしまったことがあって、あまり目にしないことだとは思うが一応メモ。
CRUDは「モデルクラスの複数形」となるクラス名をつけること、とある。ここで、「Message」というモデルクラスが既にあった場合、このテーブル用のCRUDクラスは以下のようになる。
package controllers; public class Messages extends CRUD { }
ところが、この状態で起動するとエラーが発生してしまう。原因は「Messages」というクラス名。CRUDクラスの内部で「play.i18n.Messages」を使用しようとして、上記のMessagesを呼んでしまうためエラーが発生していた。
なので、そういう場合は仕方なく…
package controllers; import models.Message; @CRUD.For(Message.class) public class Msgs extends CRUD { }
このように、CRUDクラス名をぶつからないものに変更して、対象のモデルクラスを明示的に指定する必要がある。
ちなみに、CRUDやSecureのクラスは、ドキュメントでは「controllers」パッケージ直下に置くようになってるけど実はどこでもいい。Modelも同様。CRUDでビューをカスタマイズするときは、app/views 以下の階層をパッケージと合わせること。
カスタムテンプレートタグ
カスタムテンプレートタグは、ドキュメントによると
#{hello /}
などと書くと「app/views/tags/hello.html」(拡張子はxmlだったりjsonだったりいろいろ)を見に行く。実際そうなのだが、tagsの中をフォルダ分けしたい場合は、「app/views/tags」をルートに見立て、フォルダの階層をパッケージのようにピリオドでつなげて記述する。つまり、
app/views/tags/sample/html/hello.html
を読み込みたい場合は
#{sample.html.hello /}
と記述する。
OAuth関連
Twitterと連携するアプリを作ろうとしたときのこと。同梱のサンプル通り作ってみたんだけど、認証はうまくいくもののどうも投稿の部分でコケてしまう。
String response = WS.url(url).oauth(TWITTER, getUser().getTokenPair()) .post().getString();
Responseを見ると「Incorrect signature」とかいうメッセージが。クエリも文字化けしている様子。
半角英数字だけ入力すると通るのでエンコードの問題かなー、などと一時間以上試行錯誤したけど結局ダメだったので、play.libs.WSを使うのを諦めてoauth.signpost~を直に使うことにした。
OAuthConsumer consumer = new DefaultOAuthConsumer(consumerkey, consumersecret); consumer.setTokenWithSecret(accesstoken, accesssecret); URL url = new URL("http://api.twitter.com/statuses/update.xml?status=" + URLEncoder.encode(status, "utf-8")); HttpURLConnection request = (HttpURLConnection) url.openConnection(); request.setRequestMethod("POST"); consumer.sign(request); if (request.getResponseCode() != HttpURLConnection.HTTP_OK) { // エラー処理 }
一発でうまくいく。なんじゃそりゃ!
追記
※1.2 ではTokenPairがdeprecatedになっているので注意。