Play! Framework 1.2.4 謎の NullPointerException
Play! Framework でぬるぽが出たためコードを確認してみたのだが、どう考えても null 参照が発生することはないフローだった。念のため発生場所の直前で null チェックを入れて再実行すると、チェックをすり抜けて NullPointerException が発生した。
謎の現象。Javassist のバイトコード書き換えが何か悪さをしているのだろうか。再現性があるだけマシなのだがどう直せば良いのか困ります。
以下はコンソールに出ていたスタックトレース。
Execution exception (In /app/controllers/Auth.java around line 288) NullPointerException occured : null play.exceptions.JavaExecutionException at play.mvc.ActionInvoker.invoke(ActionInvoker.java:231) at Invocation.HTTP Request(Play!) Caused by: java.lang.NullPointerException at controllers.Auth.getAccountFromTwitter(Auth.java:288) at controllers.Auth.oauth(Auth.java:231) at controllers.Auth.twitter(Auth.java:113) at play.mvc.ActionInvoker.invokeWithContinuation(ActionInvoker.java:548) at play.mvc.ActionInvoker.invoke(ActionInvoker.java:502) at play.mvc.ActionInvoker.invokeControllerMethod(ActionInvoker.java:478) at play.mvc.ActionInvoker.invokeControllerMethod(ActionInvoker.java:473) at play.mvc.ActionInvoker.invoke(ActionInvoker.java:161) ... 1 more
実行に使用している Play! Framework のバージョンは 1.2.4。
C:\Users\torao\workspace\uract>play version ~ _ _ ~ _ __ | | __ _ _ _| | ~ | '_ \| |/ _' | || |_| ~ | __/|_|\____|\__ (_) ~ |_| |__/ ~ ~ play! 1.2.4, http://www.playframework.org ~ 1.2.4
Java 非同期 I/O のデザインパターン (クライアント編)
New I/O の非同期処理を実装する時にいつも使うパターンの個人的設計まとめ。非同期 I/O はアプリ要件によって設計を柔軟に変える必要があるので定石というわけではありませんし、安全に組み替えるにはそれなりの知識が必要です。
この記事が説明のベースにしているサンプルソースは My Design Pattern for Asyncronous I/O · GitHub に置いてあります。
非同期 I/O といえば 1 つのスレッドで複数のソケット I/O を管理する方法です。
非同期 I/O を生かした設計というものは必ず Producer/Consumer 型、イベント駆動型設計となります。これはパフォーマンスと引き換えにオブジェクト指向の汎用設計化を低下させ、特定の困難なバグを生む余地を増やします。個人的には C10K 問題を想定する必要がない程度 (同時に扱うソケット数が 1000 やそこいら程度) であればソケット数分のスレッドで同期 I/O を使用する方法をお勧めしております。
仮定要件と設計
この記事で仮定するクライアントの挙動は以下の通り:
- 送信した 1 行分のデータをそのまま返信してくる echo サービスに対するクライアント。つまりデータ単位が「行」でその区切りは改行 (CRLF, CR, LF 兼用)。
- クライアントの実装は 1 秒ごとに任意の文字列を送信し、それが返ってくることを期待する (任意のタイミングで送信データが発生する事を想定)。
- あるデータに対する応答が返ってくるまで次の行の送信は行わない (パイプライン化を行わない)。
- 1スレッドで複数のクライアント (通信端点; ソケット) を操作する。クライアントは任意のタイミングで参加が可能。
- クライアントの処理中に例外が発生したらそのクライアントのみを切り捨てる。全体をダウンさせない。
- Client
- 送信データが発生した時の通知と、送信データの参照、データ受信時の通知をアプリケーション側で実装するための抽象クラス。インスタンス内部に SocketChannel と I/O 用のバッファを保持している。通信端点。
- Worker
- Selector を保持し送受信可能になった Client に対して送受信処理を行わせるためのスレッド。
Play! の OAuth2 不具合いろいろ
Play! Framework 1.2.4 の play.libs.OAuth2 を Instagram に対して使ってみて見付けた不具合いろいろ。結果的にアクセストークンの取得部分が POST に対応していないため Instagram には使えませんでした。
- retrieveVerificationCode(): 認証 URL (コンストラクタの第1パラメータ) に対して強制的に '?' から始まるリクエストパラメータを連結しているため、OAuth2 プロバイダ (今回は Intagram) から提供された URL にパラメータが含まれていると正しく機能しない。例えば "http://api.instagram.com/oauth/authorize/?response=code" のような場合、"http://api.instagram.com/oauth/authorize/?response=code?client_id=xxxx&redirect_url=http://myserver.." といった予期しないパラメータでリクエストが行われる。
回避策として client_id パラメータをあらかじめ認証 URL に梅こみ URL の最後に '&' を付けることで何とかなる (かもしれない)。つまり "http://api.instagram.com/oauth/authorize/?client_id=xxxx&response=code&" を指定すれば "http://api.instagram.com/oauth/authorize/?client_id=xxxx&response=code&?client_id=xxxx&redirect_url=http://myserver.." となる。ゴミパラメータが入るが。 - retrieveVerificationCode(): リクエストパラメータをエスケープしていない。リダイレクト URL にパラメータ部分が入る場合は呼び出し側で URL エスケープする必要がある。
- retrieveAccessToken(): GET のみに対応。Instagram のように POST 指定がされている OAuth2 プロバイダには使用できない。
Play! Framework による OpenID の参照
Play! Framework の OpenID 機能を使用して Google アカウントとリンクする方法について。
OpenID は認証統合やシングルサインオンのためのアーキテクチャです。こちらのサービスのアカウントを Google の OpenID に関連付けておけば利用者はログイン作業なしでサービスを利用できます。詳しくはggrks
例として想定する動作。
- 画面上で [Google] のアイコンをクリック。
- サーバから Google の URL へ OpenID を要求する。
- Google からリダイレクトで認証レスポンスの呼び出し。認証に成功していれば OpenID を取得。
- OpenID と紐付けてこちらのサービスのアカウント情報を保存する。
ビューの作成
Google アイコンがクリックされると Auth コントローラの google() メソッドを呼び出すようなビューを作成します。
<a href="@{Auth.google()}"> <img src="@{'/public/images/google.png'}" width="32" height="32"/>Google</a>
コントローラの作成
Google アカウントは https://www.google.com/accounts/o8/id に対して OpenID.id().verify() を呼ぶだけ。認証レスポンス時にユーザ情報を取得できます。
このアクションはリンクをクリックした時と認証レスポンス時 (OpenID.isAuthenticationResponse() == true) の 2 度呼び出されます。リンククリック時には URL へ認証要求を出し、認証レスポンス時には認証結果の評価を行います。
public class Auth extends Controller { public static void login(){ render(); } public static void google(){ authenticate("https://www.google.com/accounts/o8/id"); } private static void authenticate(String url){ // 認証完了のレスポンスでなければ OpenID の検証を要求する if(! OpenID.isAuthenticationResponse()){ OpenID.id(url).verify(); return; } // 認証処理の結果を評価 UserInfo user = OpenID.getVerifiedID(); if(user == null){ flash.put("error", "認証に失敗しました"); login(); return; } // 認証完了 session.put("userid", user.id); // 本当はこちらのアカウントと関連付け... redirect("Application.index"); // 適当なアクションにリダイレクト return; } }
これだけ。超簡単。
Play! Framework クイックスタート
Play! Framework は Java VM 上で動作するフルスタックの軽量 Web フレームワーク。Scala にも対応しており、Java の静的型システムや豊富なライブラリ、高速性を生かしつつ Ruby on Rails を更にシンプルにしたような手軽さといった印象です。
個人的にはプレゼンテーション層には RoR のような変わり身の速いもの、ビジネスロジック層には Java のようなロバストネスなものが適していると考えてるので、Play! が RoR と同等の手軽さなら実行環境の面で RoR よりベターな選択です。Rails もバージョンが進んでかなりモノリシックでヘビーなフレームワークになってしまいましたし。
以下 Play! の特徴など。
- RoR で見たような使い方やファイルの配置。
- 修正後のコンパイルやデプロイ、リスタートが不要。
- Groovy ベースのテンプレートエンジン。ビューの記述が手軽。
- データベースへのアクセスは JPA ベースで SQL の記述が不要。
- Hibernate, OpenID, memcached などに対応したフルスタックなプラグインシステム。
- Stateless で REST。
- 非同期 I/O、WebSockets に対応。
- バンドルに jQuery も入ってる。
- gems のようにモジュールを追加できる。
このようなイマドキの Web 開発に必須の要件を満たすため敢えて Servlet API を捨てたそうです (WAR 化して通常のサーブレットコンテナで動かすことは可能です)。また controllers と言ったパッケージ命名や public なインスタンス変数など Java の"こうあるべき"慣例がいくつか覆されています。ここらへんは賛否あるかと思いますが、Web 周辺のような汎用性の低い部分では行き過ぎた抽象化や再利用性を求めなくても良いかなと思っています。
スタートアップページまでの手順
- Getting Started with Play Framework から最新版 play-1.2.4.zip をダウンロード。
- 例として C:\Program Files に解凍。つまり C:\Program Files\play-1.2.4 が Play! Framework のインストールディレクトリとします。
- 環境変数 PATH に C:\Program Files\play-1.2.4 を追加。
- コマンドプロンプトから play myapp と入力するとカレントディレクトリの myapp/ 以下に Play! の Web アプリが作られます。
- myapp ディレクトリに移動し play run を実行。ブラウザから http://localhost:9000 へアクセスするとスタートアップページが表示されます。
協調型分散システムのための RPC 設計
協調型分散システムに必要な Peer to Peer (以下PTP) RPC サービスインフラストラクチャを構築するための通信端点の設計について。コンポーネントと役割ベースでの記述となります。プロトコルは別途。
抜けている点など多々存在していると思いますのでご意見などをコメント頂けると幸いです。
概要
SOAP, Thrift, RMI など従来の RPC の多くはクライアント-サーバ型で構成されています。これは主 (Server) の提供するサービスを従 (Client) から利用するという一方通行の利用形態です。
この構成はサーバ側からクライアントへ通知を行う必要があるようなシステム設計とはうまく適合しません。例えばサーバで行なっている非同期処理の結果通知が必要な場合、ロングポーリングのような実装で回避するか、さもなくばサーバからクライアントへ逆方向の RCP 接続を行う必要があります。
分散システムにおける PTP 型の構成ではそれぞれのノードが互いに協調してサービスを提供します。前述のような C/S 型 RPC でも 2 接続を使えば双方向のサービス提供が実現できますが、 2 ノード間で通信するだけで 2 接続を使用するのはあまりスマートではありません。この記事では 1 または限られた数の TCP/IP 接続上で互いのサービスを呼び出すことのできる RPC 設計について記述します。
sbt を入れてみた
sbt (Simple Build Tool) は Scala 界隈でよく使われているビルドツールです。DSL の本領発揮といった黒魔術書のようなビルド定義に腰が引けていましたが、Kestrel のビルドに必要なので入れてみます。始める sbt - ようこそ も参照。
インストールに必要なものは Java 6 以降の実行環境と sbt-launchar.jar のみ。sbt-launchar.jar は TypeSafe のダウンロードサイトから適当なバージョンをダウンロードして下さい。あとは起動シェルを書いて実行権限を与えるだけです。
torao@safran$ wget "http://typesafe.artifactoryonline.com/typesafe/ivy-releases/org.scala-tools.sbt/sbt-launch/0.11.2/sbt-launch.jar" --2012-01-02 19:24:33-- http://typesafe.artifactoryonline.com/typesafe/ivy-releases/org.scala-tools.sbt/sbt-launch/0.11.2/sbt-launch.jar typesafe.artifactoryonline.com をDNSに問いあわせています... 50.19.229.208 typesafe.artifactoryonline.com|50.19.229.208|:80 に接続しています... 接続しました。 HTTP による接続要求を送信しました、応答を待っています... 200 OK 長さ: 1041753 (1017K) [application/java-archive] `sbt-launch.jar' に保存中 100%[================================================>] 1,041,753 204K/s 時間 6.2s 2012-01-02 19:24:40 (165 KB/s) - `sbt-launch.jar' へ保存完了 [1041753/1041753] torao@safran$ mv sbt-launch.jar ~/bin/ torao@safran$ cat > ~/bin/sbt java -Xmx512M -jar `dirname $0`/sbt-launch.jar "$@" torao@safran$ chmod 755 ~/bin/sbt
早速起動してみるとものすごい勢いでダウンロードが行われ環境構成が始まります。インタラクティブモードになったようなので exit で終了します。
torao@safran$ sbt Getting net.java.dev.jna jna 3.2.3 ... downloading http://repo1.maven.org/maven2/net/java/dev/jna/jna/3.2.3/jna-3.2.3.jar ... [SUCCESSFUL ] net.java.dev.jna#jna;3.2.3!jna.jar (4263ms) :: retrieving :: org.scala-tools.sbt#boot-app confs: [default] 1 artifacts copied, 0 already retrieved (838kB/81ms) Getting Scala 2.9.1 (for sbt)... downloading http://repo1.maven.org/maven2/org/scala-lang/scala-compiler/2.9.1/scala-compiler-2.9.1.jar ... [SUCCESSFUL ] org.scala-lang#scala-compiler;2.9.1!scala-compiler.jar (30201ms) downloading http://repo1.maven.org/maven2/org/scala-lang/scala-library/2.9.1/scala-library-2.9.1.jar ... [SUCCESSFUL ] org.scala-lang#scala-library;2.9.1!scala-library.jar (31420ms) … [SUCCESSFUL ] org.scala-tools.sbt#cache_2.9.1;0.11.2!cache_2.9.1.jar (1515ms) downloading http://repo1.maven.org/maven2/org/scala-tools/testing/test-interface/0.5/test-interface-0.5.jar ... [SUCCESSFUL ] org.scala-tools.testing#test-interface;0.5!test-interface.jar (797ms) :: retrieving :: org.scala-tools.sbt#boot-app confs: [default] 37 artifacts copied, 0 already retrieved (7324kB/336ms) [info] Set current project to default-f67fa0 (in build file:/home/torao/workspace/kestrel/kestrel/project/build/) > exit
実行後は $HOME の下に .sbt/ と .ivy2/ が出来上がっていました。.sbt/ 以下には Scala 2.9.1 と JNA のライブラリが入っています。.ivy2/ は Apache Ivy のリポジトリでしょうかね。