Javaバッチ処理のNFS向けファイルI/O
この記事は Java EE Advent Calendar 2015の12/7分の記事です。
明日は@btnrougeさんです。
Java EEのAPIが直接関連する話ではなくて恐縮ですが、サーバサイドJavaでファイルI/Oを含むバッチ処理の性能Tipsをまとめます。
テーマはjava.io.BufferedWriterクラスのバッファサイズについてです。
デフォルトは8KBでBufferedWriterのコンストラクタにおいて変更可能ですが、javadocには以下の記載があります。
バッファのサイズは、デフォルト値のままにすることも、特定の値を指定することもできます。デフォルト値は、通常の使い方では十分な大きさです。
http://docs.oracle.com/javase/jp/8/docs/api/java/io/BufferedWriter.html
あまり変更する機会もないせいか、Java SE 7で導入された便利なFiles.newBuffertedWriterメソッドにはバッファサイズを設定する引数がありません。
しかし、NFSへの書き込み時においては、mountオプションnoac*1の有効時にバッファサイズ拡大が効果的なケースがあります。
Javaのバッチ処理のシステム連携において、NFSサーバにファイルを置くファイル連携方式は、業務システムで見かける構成かと思います。
効果測定
手元の仮想マシンでバッファサイズ変更時の書き込み性能を実測してみます。
/nfs_export/batch 192.168.xxx.xxx(rw)
NFSクライアント: マウントオプションnoac
mount -o noac 192.168.xxx.xxx:/nfs_export/batch /nfs
NFSクライアント: /proc/mounts
192.168.xxx.xxx:/nfs_export/batch /nfs nfs4 rw,sync,relatime,vers=4.0,rsize=131072,wsize=131072,namlen=255, acregmin=0,acregmax=0,acdirmin=0,acdirmax=0,hard,noac, proto=tcp,port=0,timeo=600,retrans=2,sec=sys, clientaddr=192.168.xxx.xxx,local_lock=none,addr=192.168.xxx.xxx 0 0
検証コード
ローカルファイルシステム上のddで生成した100MBファイルのダミーデータ(test.src)を、NFSマウント上のパスにコピーするシンプルな処理です。
public class Fcopy { private static final String SRC = "/home/test/input/test.src"; private static final String DST = "/nfs/test.dst"; public static void main(String ... args) throws IOException { int bufSize = Integer.valueOf(args[0]); byte[] buf = new byte[8192]; try ( BufferedInputStream bis = new BufferedInputStream(Files.newInputStream(Paths.get(SRC))); BufferedOutputStream bos = new BufferedOutputStream(Files.newOutputStream(Paths.get(DST)), bufSize)) { long start = System.currentTimeMillis(); for (int readSize = bis.read(buf); readSize >= 0; readSize = bis.read(buf)) { bos.write(buf); } long time = start - System.currentTimeMillis(); System.out.println("BufferedStream bufSize: " + bufSize + " Time(millisec): " + time); } } }
測定結果
以下のような結果となりました。デフォルトのNFSクライアントの非同期書き込みには及ばないものの、BufferedWriterのバッファサイズ変更で、数倍の処理時間差が見られます*2。
BufferedWriterのバッファサイズ | 処理時間(ミリ秒) |
---|---|
8192 (8KB, デフォルト) | 36857 |
65536(64KB) | 18452 |
131072(128KB) | 7888 |
(参考) 8192/NFSクライアント側の非同期書き込み | 407 |
なぜ早くなったか
ここから先はNFS実装の知識が足りず、若干自信なしです。
/proc/mountsの結果を振り返ると、wsizeが131072です。man nfsを見ると、wsizeはNFSクライアントがNFSサーバに一度に書き込むサイズを示しています。wsizeは明示的に指定しない場合、クライアントサーバ間で適切な値を自動的に決定します。
192.168.56.101:/nfs_export/batch /nfs nfs4 rw,sync,relatime,vers=4.0,rsize=131072,wsize=131072 ...
デフォルトの非同期NFSクライアントの場合は、ある程度まとまったデータをwsize単位(例では128KB)でNFSサーバに送ります。しかし、noacにより同期書き込みになった場合は、writeシステムコールの都度サーバに書き込まれるため、JavaのBufferedWriterの単位でNFSサーバに書き出されます。tcpdumpで見ると、noacオプションを付けてマウントした場合はLen:が示すWRITE RPCのサイズがJava側のバッファサイズと同じです。
BuffertedWriterのバッファサイズが8192の場合
11 0.029289000 192.168.56.102 -> 192.168.56.101 NFS 1222 V4 Call WRITE StateID: 0x6366 Offset: 0 Len: 8192 14 0.035469000 192.168.56.101 -> 192.168.56.102 NFS 202 V4 Reply (Call In 11) WRITE ...
BuffertedWriterのバッファサイズが131072の場合
53 0.012420 192.168.56.102 -> 192.168.56.101 NFS 458 V4 Call WRITE StateID: 0x8849 Offset: 0 Len: 131072 55 0.015583 192.168.56.101 -> 192.168.56.102 NFS 202 V4 Reply (Call In 53) WRITE ...
また、nfsstatの結果からも、writeを示すRPC発行回数が減っています。
(データは1MBのファイルコピー処理時のもの)
BuffertedWriterのバッファサイズが8192の場合
(writeが128回。8192B * 128回 = 1MB)
nfsstat -c # ファイルコピー処理前 null read write commit open open_conf 0 0% 0 0% 133228 93% 7915 5% 149 0% 13 0% # 処理後 null read write commit open open_conf 0 0% 0 0% 133356 93% 7915 5% 150 0% 14 0%
BuffertedWriterのバッファサイズが131072の場合
(writeが8回。131072B * 8回 = 1MB)
# ファイルコピー処理前 null read write commit open open_conf 0 0% 0 0% 133356 93% 7915 5% 150 0% 14 0% # 処理後 null read write commit open open_conf 0 0% 0 0% 133364 93% 7915 5% 151 0% 15 0%
JavaのBufferetedWriterのデフォルト8KBの単位での書き出しでは、NFSのwsize(128KB)と書き込み単位が合わず、サイズの小さい断片化したRPCを繰り返し発行していたことが、バッファ拡大による性能向上の理由と思います。
バッファ拡大の注意点
NFSクライアントマシンの不測なクラッシュに備えた同期書き込みを目的にnoacオプションでマウントしていた場合、JavaレイヤでのバッファリングはNFSレイヤでの同期書き込みの意味をなくし、データ損失の可能性を高めるため注意が必要です。
以下のようなケースでは、BufferetedWriterのバッファ拡大もありかと思います。
まとめ
BuffertedWriterのバッファ拡大により、性能差分が発生するあまり見かけないケースをまとめました。実測したのはNFSv4だけですが、ネットワーク経由でアクセスする他のファイルシステムも、同様の注意が必要と思います。
JavaOne 2015 4日目メモ (10/28)
JavaOne 4日目の夜には、毎年トレジャーアイランドでOracle Appreciation Event(オラクルによる感謝祭?)が行われ、有名な方のライブが行われる。今年はエルトンジョンとBECKだった。毎年寒いので、薄手のダウンなど暖かい服装が必須。
4日目は以下のセッションに参加。
- [CON2709] The New HTTP Client API, Including HTTP/2 and WebSocket
- [CON2809] Deep Dive into Top Java Performance Mistakes in 2015
- [CON6446] WebSocket in Enterprise Applications
- [CON6856] Saving the Future from the Past
[CON2709] The New HTTP Client API, Including HTTP/2 and WebSocket
JEP110で提案されている、Java SEに盛り込む新しいHTTPクライアントについて。JDK9には、HttpUrlConnectionに代わるHTTPクライアントを盛り込む計画があり、HTTP1.1/HTTP2/WebSocketのクライアントがサポートされる予定である。
セッションの前半はHTTP2の説明と、Java8から導入されたComputableFutureの振り返りであったため省略。後半から、具体的なAPIのアイディアが紹介された。いずれもまだOpenJDK9のリポジトリに含まれていないため、まだ変更が入る可能性が高い。
HTTP GETし、結果を文字列(HttpResponse.asStringメソッド)で取得:
HttpResponse resp = HttpRequest .create(new URI("http://www.foo.com/") .GET() .response(); if (reap.statusCode() == 200) { String responseBody = resp.body(HttpResponse.asString()); System.out.println(System.out.println(responseBody); }
POSTする場合は、以下のようにbodyメソッドに設定する:
HttpResponse resp = HttpRequest .create(new URI("http://www.foo.com/") .body(HttpRequest.fromString("param1=1,param2=2") .POST() .response();
非同期実行。レスポンスボディをファイルに書き出す:
ComputableFuture<Path> future= HttpRequest.create(uri) .GET() .responseAsync() .thenApplyAsync(resp -> { if (reap.statusCode() == 200) { return reap.body(asFile(path)); } else { throw new UncheckedIOException(new IOException()); } }); //=> このメソッドチェーンの返り値はComputableFuture<Path> future.join(); //=> thenApplyAsyncが完了するまでブロック
ExecutorServiceを渡せるので、Java EE環境でもComputableFutureが使いやすい:
(APサーバが管理しているスレッドプールManagedExecutorが渡せる)
HttpClient client = HttpClient.create()
.executorService(Executors.newCachedThreadPool())
.build();
HttpRequest request = client.request(URI.create("https://www.foo.com/")).GET();
HTTP2でリクエストするときは、メソッドチェーンに追加する:
HttpRequest request = client.request(URI.create("https://www.foo.com/"))
.version(HttpClient.Version.HTTP_2)
.GET();
セッションの中ではリソースクローズに関する言及がなかったが、HttpUrlConnectionのように、クリーンアップをどうやるかわかりくいAPIにならないことを願っている。
[CON2809] Deep Dive into Top Java Performance Mistakes in 2015
性能解析ツールのdynatrace社の方による、どんなメトリクス(ページサイズ、HTTPエラーレスポンス数などの指標)が、性能遅延に直結しやすいか実例を交えながら紹介するセッション。セッションではdynatraceによる以下のようなメトリクス収集結果のデモが紹介された。
メトリクス1: 画像などのリソース数、サイズに注意
- アメフトのスーパーボウルのページは、1ページに434の画像などのリソース、合計20MBのサイズによりレスポンス遅延が発生していた
- FIFAワールドカップのページで、faviconアイコンが370KBを超え、そのほかにも150KBを超えるCSSが複数あって遅延
- 大きな背景画像もかっこいいが、サイズには注意が必要
- 遅いと思ったら、まずブラウザF12のデバッグツールで見てみる
メトリクス2: SQL大量row取得、同一SQLの繰返実行(N+1)、大量のコネクション生成
- Room Reservertion System(会議室予約?) で1リクエストで2万回以上のSQL発行 (典型的なORMのN+1問題)
- 1万以上のコネクション生成 (プールによるコネクション再利用漏れ)
- データキャッシュをHashTableに全て置いて、ブロック多発による性能劣化
メトリクス3: 外部サービスの呼び出し数、WebAPI実行時のN+1問題
- 100回以上の外部WebAPIの繰り返し呼出
- ORMのN+1問題と同様に、キーごとに毎回APIコールすると同じ問題が起こる
メトリクス4: ロギング量
メトリクス5: GC回数、ライブヒープの増加
- アプリケーションの改修に伴いライブヒープが一気に増加
- 低負荷時のレスポンスタイムに変化はないが、高負荷時にライブヒープ増加に伴いCPUネックが顕在化し、大幅なレスポンス低下
他にも確認した方が良いデータ:
ここからはセッション内容と関連しないメモ。有償であるdynatraceを使わずにELK(ElasticSearch+Logstash+Kibana)でメトリクスが収集できないか考える。
- フロントエンド
- DB関連
- slow queryはログから取れる。
- ログからのN+1の検知は難しそう。今まで通り、スロークエリが出てないがDBアクセスクラスで遅い場合は、デバッグ用途でORMのSQL出力を有効化するしかない?
- コネクション数も、例えばPostgreSQLであればpg_stat_databaseビューのnumbackendsを拾ってくればいけそう。
- 連携サービスのRPC呼び出し過多
- ロギング過多
- プロファイラ使わずにログからとなると dfによるディスク使用量ぐらい?
- GC回数、ライブヒープの増加
- OSリソース系
- ELK構成でsar, df, proc xxx を取り込む以外にも、RHEL7(および6.7)以上であれば、Performance Co-Pilot + Netflix vectorの選択肢がある。vectorがELKより便利そうかは要調査。
[CON6446] WebSocket in Enterprise Applications
WebSocketの導入セッション。ゲーム以外のWebSocketの使い道をイメージするために聴講。前半はLong polling/SSE/WebSocket/HTTP2の違いについてだったので省略。
WebSocketを何に使うか:
いくつかの例が紹介された。
- サーバプッシュ
- 頻繁にリクエスト&レスポンスがある処理。株トレーディングなど。
- リモートコントロール。リアルタイムでフィードバックを受けながら操作するもの。機械操作など。
- リアルタイムモニタリング。
- バックエンドサーバ間の高速なリクエスト&レスポンス。
- メモ: 高速RPC用途であれば、grpcなどの選択肢もあると思う
WebSocketのサブプロトコル:
WebSocket自体は、ブラウザの世界にソケット接続を持ってきたものなので、アプリケーション固有のjson以外にも、サブプロトコルがいくつか出ている。
WebSocketのクラスタ化:
複数のサーバに分かれてWebSocket接続する場合は、他のサーバで接続するWebSocketの存在を考慮する必要がある。たとえば一斉通知する場合、Java EEであればJMSやJCacheによってWebSocketのサーバ間連携を実現する。また、サーバサイドでのフェールオーバは難しいため、障害時はクライアントから再接続する。
[CON6856] Saving the Future from the Past
Javaの非推奨(@Deprecated)の歴史と今後について紹介するセッション。
JJUG CCC 2014 Springで講演されていた、オラクルのStuart Marksさんが"Dr. Deprecator"として白衣を着て発表。
Deprecatedの歴史:
- 歴史は古く、JDK1.1から既に非推奨は存在
- 当初はJDK1.1で新しく多くのAPIが導入されたために、古いものを捨てる(abandon)ため、代わりのもっと良いもの(supersede)が出ていることを、ユーザに知らせる仕組みとして導入された。
- ユーザに新しいものに移行したもらうためのものだった。
Deprecatedの意図の多様化:
当初は今後はメンテしない(abandon)、代わりに良いのがある(supersede)がDeprecatedの意図であったが、現在ではもっと多用な意図でDeprecatedが使われている。
- CONDEMNED: 今後削除される可能性がある
- DANGEROUS: データが壊れたり、デッドロックが起こる可能性がある。
- DANGEROUSの代表例はThread.stop()
Deprecatedこそ付いていないものの、Vectorなどの古いAPIは、ユーザにとって"非推奨"と見られている。
Deprecatedに気が付かないパターン:
コンパイル警告、JavaDoc、IDEの取り消し線でユーザに知らせているが、JDKの移行の仕方によっては気が付かないパターンもある。再コンパイルせずにバイナリ互換を活かしてJDKのバージョンアップをした場合はユーザはわからない。
APIのライフサイクル:
以下のようなフローが理想とされているが、Java SEの実態は異なる。
- 新しいAPIが導入され、古いAPIが非推奨になる
- しかし実態は、多くの人は非推奨警告を無視する
- ユーザは新しいAPIに移行する
- 実態は様々な理由からなかなか移行しない
- 移行が済んだころに、APIは削除される
- 実態は、JDK9となってもほとんどのAPIは削除されていない。
- APIの削除は究極の非互換である
- ソース互換性はコンパイルが通らず壊れる、バイナリ互換性もNoSuchMethodErrorで壊れる、振る舞い互換性もAPIがないのでもちろん壊れる
- なぜ無くなったのか、何がなくなったのかjavadoc化が困難
これからのDeprecated:
- 非推奨の理由カテゴリを再整理し、enum型で表現したい。
- UNSPECIFIED(特になし)
- CONDEMNED(将来的に削除予定)
- DANGEROUS(リスクあり)
- OBSOLETE(時代遅れで古い)
- SUPERSEDED(もっと良い代替がある)
- UNIMPLEMENTED(実装なし、呼ぶとUnSupportedOperationExceptionになるので注意)
- @Deprecatedのアノテーション要素として情報が補足できるようにしたい。
@Deprecated(value={DANGEROUS,SUPERSEDED}, replacement="String#getBytes()", since="1.1") public void getBytes(int srcBegin, int srcEnd, byte[] dst, int dstBegin)
- ランタイム時にも@Deprecatedの警告を出して、ユーザに通知したい
- javadocの"All Methods"から@Deprecatedのメソッドを削除して、ユーザに存在を知らせないようにしたい
- IDEが非推奨APIの利用をもっと自動置換してくれるようになって欲しい
- しかし、スレッドセーフを意図してHashTableを使っているなど、単純な置き換えはバグを招く
- これらの提案は JEP 277: Enhanced Deprecation にまとめられている。
- JDK9への盛り込みを目指して検討中。
JavaOne 2015 3日目メモ (10/27)
3日目も晴れている。今年は週間天気予報に雨マークが出てたから折りたたみ傘を持ってたが、4回目で今のところ傘が必要なほどの雨に振られたことがない。
今日は以下のセッションに参加。
- [CON6712] Enhanced Process APIs
- [CON11284] JDK 9 Language and Tooling Features
- [CON3239] Tuning JavaServer Faces
- [CON2483] Java SE 8 for Java EE Developers
- [CON2876] JSR 373: New Java EE Management API
- [BOF7768] Let’s Visualize Log Files for Troubleshooting Java Applications
[CON6712] Enhanced Process APIs
JDK9に向けて、ProcessBuilderの改善アイディアについて。
プロセスの情報をもっとJavaAPIで取得したい:
- allProcesses()でpsのようにマシン上のプロセス一覧を取得
- (OSのアクセス制限の範囲で)
- getCurrent()で現在のプロセス情報、of(pid)で対象pidの情報
- getPid(), info()のようにプロセス情報の収集
- onExit()でCoumputableFutureによる非同期ハンドリング?
ProcessHandle.Infoクラスから、対象のプロセス情報を収集する。ユーザ、実行したコマンド、コマンド引数、開始時刻、CPU時間など。取れる情報もあれば、OSの制限により取れない情報もあるので、返り値はOptionalでラップして、例えば Optional
以下の例はプロセスID、親プロセス、ユーザ、コマンドをロギング。
showProcess(ProcessHandle.current()); static void showProcess(ProcessHandler ph) { ProcessHandle.Info info = ph.info(); log.printf("pid: %d, parent: %s, user: %s, cmd: %s%n", ph.getPid(), ph.getParent(), info.user().orElse("none"), info.command().orElse("none")); }
Java8のストリームAPIへの対応:
以下の例は現在のユーザのプロセス一覧をプロセスID順にソートしてロギングする。
Optional<String> currUser = ProcessHandle.current(); ProcessHandle.allProcesses() .filter(p1 -> p1.info().user().equals(currUser)) .sorted(CodeSamples::parentComparator) .forEach(CodeSamples::showProcess); static int parentComparator(ProcessHandle p1, ProcessHandle p2) { return Long.compare(p1.parent().get().getPid(), p2.parent().get().getPid()); }
非同期による外部プロセスの起動:
Java8から入ったComputableFuture
以下の例はプロセスの起動と終了を複数の多重度でプールループで実行している。プロセス終了のタイミングでプロセスID、終了コードのロギングと、再度プロセスの起動を行っている。
Semaphore count = new Semaphore(11); CountDownLatch end = new CountDownLatch(1); // paralleism分の多重度でプロセスを繰り返し起動 for (int i = 0; i < paralleism; i++) start(pb1, count, end); // 並行で実行する全てのプロセスが起動するまでラッチで待機 end.await(); // paralleism分の多重度でプロセスが起動したら、 // Futureと同じような動きをするonExit.get()をプロセスごとに実行して、 // 各プロセスの完了までブロックする ProcessHandle.current() .children().forEach(CodeSample::waitForExit); static void start(ProcessBuilder pb, Semaphore count, CountDownLatch end) { try { if (count.tryAcquire()) { Process p = pg.start(); p.onExit() .thenAccept(CodeSamples::logExit) .thenRun(() -> start(pb, count, end)); } else { end.release(); } } catch (IOException ioe) { throw new RuntimeException("Process start failed", joe); } } static void logExit(Process p) { log.printf("exit: %d, status: %d%s", p.getPid(), p.exitValue()); } static void waitForExit(ProcessHandle p) { try { p.onExit().get(); } catch (Exception e) { ... } }
ProcessBuilderへのパイプの適用:
ls | fgrep duke
と同じようなパイプ繋ぎのコマンドを、Javaで実現する。
ProcessBuilder pb1 = new ProcessBuilder(); ProcessBuilder pb2 = new ProcessBuilder(); List<Process> processes = ProcessBuilder.startPipe(pb1, pb2); processes.forEach(p -> { try { int status = p.waitFor(); log.printf("status: %d%n", status); } catch (InterruptedException ie) { } }
他にも今までのProcessはInputStream/OutputStreamを返すが、NIOのChannelに対応させる案の紹介。ノンブロッキングIOに使われているselector()を使って、応答が返ってきたタイミングで通知を受ける案も紹介された。
[CON11284] JDK 9 Language and Tooling Features
Jigsaw以外のJDK9の変更点についてまとめたセッション。
対話式でjavaを実行するjshellの導入、javadocの検索フォームの導入以外はよくわからなかったので要復習。
セッション資料はここで公開されている。
[CON3239] Tuning JavaServer Faces
JSFでパフォーマンスに関する問題を起こさないために、どんなことに注意すべきか紹介するセッション。
- ページサイズ
- セッションのサイズ (コンポーネントツリーとアプリケーション分の両方)
- リクエストパラメータのサイズ
- EL式の評価
- EL式のメソッド呼び出しでDBリクエストしない
- JSFのコンフィグフラグ
- FacesContext.getCurrentContext()の繰り返し呼び出し
Bloated component tree対策:
panelタグや、Primefacesなどのコンポーネントにあるタブ機能を使うと、Bloadted component tree (広がりすぎたコンポーネントツリー)に陥りがちである。これは、output系のタグにrendered=false属性を指定しても、画面に表示されないだけで、コンポーネントとしては存在するので性能に影響を与える。以下のような観点で対処を行う。
- <panel>はdivやspanに置き換えられないか
- htmlで書けば、コンポーネント数の肥大化を抑えられる
- JSF2.2から導入されたステートレスビューの導入
- 動的ローディングの機能がないか探す
- たとえばタブであれば、遅延ロード機能が用意されているはず
セッションをなるべく使わない:
セッションの代わりに可能な限り以下を使い、ライブヒープによるGCコスト増加や、レプリケーションコストの増大を避ける。
id名を短縮してレスポンスデータ量削減:
標準では フォームID:テキスト入力フォームID のような形式でコロン区切りで長いIDが付く。このID名が長い場合は、<h:form id="f" ... のように、1文字IDを明示的に指定すると、テーブルの中にフォームがあるような、何度も繰り返し長い文字列のIDが表示されるケースでデータ量削減に有効。
PartialSubmitを使ってリクエストデータ量削減:
標準の<h:commandButton>は、テーブルの列にフォーム入力がある場合など、大量の変更可能な入力項目の1つを変えただけでも、フォームに含まれるすべてのテキストインプットの内容を再送信する。Primefacesなどのライブラリが用意するコンポーネントを使うと、変更したフォーム内容だけ再送信するPartialSubmit機能がある。
この機能により、リクエスト送信量の削減、送信されたパラメータを再処理するJSFサイクルコストの低減が図れる。
不必要なFacesContext.getCurrentInstance()呼び出しを避ける:
getCurrentInstance()のコストは少なくないため、たとえば同一メソッド内での複数呼び出しは避ける。
[CON2483] Java SE 8 for Java EE Developers
Java EE開発者に有効なJava SE 8の機能を紹介するセッション。
Java EE7でInstantを使う:
JPAの@Convertを使って、DateからData and Time APIのInstantに変換する。Java EE7はDate and Time APIに対応していないため、自作する。
public class DateConverter implements AttributeConverter<Instant, Date> { public Date convertToDatabaseColumn(Instant instant) { return Date.from(instant); } public Instant convertToEntityAttribute(Date date) { return date.toInstant(); } }
以下のようにエンティティクラスのフィールドに適用する。
@Convert(converter=DateConverter.class) @Temporal(TemporalType.instance); private Instant instance;
その他もろもろ:
- StringJoiner使おう
- ComparatorもComparator.comparingBy(Person::getLastName)のように簡潔に書く
- MapのcomputIfAbsent、getOrDefaultはEEアプリケーションでもよく使う
- Files.lines(path)でファイルは読む
- APサーバ上で間違ってparallelStream()が使われてしまうことへの対策
- System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "1")
JDK9 JEP269の話:
JEP269がJDK9に盛り込まれれば、EEの世界でもよく使うコレクション初期化がシンプルにできる。
List<String> strints = List.of("one", "two", "three", "four");
[CON2876] JSR 373: New Java EE Management API
Java EE 8に向けて検討が始められている、RESTベースの監視、管理APIの標準化について。結論からまとめると、決まっていることはまだないので、セッション参加者に色々アンケートととって、仕様策定においての悩みをユーザに共有セッション。
フィードバックフォームはここで公開されている。
[BOF7768] Let’s Visualize Log Files for Troubleshooting Java Applications
@cero_tさんと、Koji Ishidaさんのセッション。トラブルシューティングに欠かせないログの可視化について。
冒頭、logをGoogleイメージ検索した、大量の丸太の写真と絵で笑いを取る。海外でも笑いを取りにいく@cero_tさんはすごい、場が暖まる。
内容はホテルの予約サイトの性能、エラー多発トラブルの実例をベースに、どのように問題対処していったかについて。
まずはじめに、seleniumによるテストケース作成と、ソースコードリーディングで問題解析しようとしたが、コード量も多く、困っている問題事象はプロダクション環境でしか起こらなかったため、あまり有効でなかった。
プロダクション環境でしか起こらない問題は、プロダクション環境を対象に解析する必要がある。サービスに影響を与えられないのでログを活用した。ELK(ElasticSearch + Logstash + Kibana)のログ収集の基盤を作り、Apacheアクセスログからリクエスト遅延とMySQLのスロークエリに相関があることを見つけ、性能問題を対処した。
エラーの多発もLogstashで収集したログを、javaプログラムにロードして解析し、原因解析を行った(この部分聞き逃したので少し自信なし)。
ログを解析するにあたって、ElasticSearchでの可視化、cron+javaプログラムでのエラー解析、ファイルサーバに転送してエディタで目視するためには、ログを一度溜め込んで、各ツールに分配するLog Stream Hubが必要となった。今回の事例ではRedisをハブとした。
このようなトラブルログ分析基盤は開発者向けであり、お金が付きにくい。そこでkibanaを販売状況などのビジネスデータの可視化にも利用した。ELK構成はいずれもオープンソースなので、まずは小さくはじめて見ようとセッションを締めくくった。
JavaOne 2015 2日目メモ (10/26)
今日から各種テクニカルセッションの開始。jigsawのセッションにいくつか参加。
- [TUT4416] Preventing Errors Before They Happen
- [CON2554] Java EE 8 Work in Progress
- [CON5118] Introduction to Modular Development
- [CON6821] Advanced Modular Development
- [CON3942] What’s Coming in JMS 2.1
- [CON6823] Project Jigsaw: Under the Hood
[TUT4416] Preventing Errors Before They Happen
The Checker Frameworkを活用して、いかにコンパイル時にバグを検出するかを紹介するセッション。Checker Framework 聞き覚えがあるなぁと思っていたら、2013年のJJUG CCCで@kimuchi583さんによって紹介されていた。
セッションによると2013年の1年間で、$312 billionのコストが世界中のソフトウェアのバグによって発生しているとのこと。Javaの型システムによる安全性は思ったより抜け道が多く、ランタイム例外が発生しやすい。
例えば以下のコード。静的にチェックできそうだが、ランタイム例外になる。
// UnSupportedOperationException Collections.emptyList().add("one");
また、以下のようなSQLインジェクションの恐れがあるコードも、可能であればコンパイル時にチェックしたい。
dbStatement.executeQuery(userInput);
このように、実行時まで検出できない問題を、アノテーションの付与によってコンパイル時にチェックするツールがChecker Frameworkである。
例えばjava.util.Dateはミュータブルである。HashMapのキーにDateを利用し、意図しないメソッドがDateを変更すると、map.get(date)時にnullが返ってきてNullPointerExceptionの温床となる。このようなケースは@ImmutableをDateに付与する。Java8のタイプアノテーションの導入により、ローカル変数の型に対してもアノテーションを付与することができる。
public class App { public static void main(String ... args) { new App().date(); } private void date() { @Immutable Date date = new Date(); date.setHours(3); // コンパイル時にエラー検知 System.out.println(date); } }
他にも、@NonNullでは、nullが入ってきたときにコンパイル時エラーにできる。
public static void main(String ... args) { new App().print(null); // コンパイル時にエラー検知 } private void print(@NonNull String message) { System.out.println(message); }
CheckerFrameworkは、コンパイル処理の後に、型チェックプラグインとしてこのような処理を実装している。CheckerFrameworkをダウンロードすると、チェック機能が有効となるjavacが含まれている。-processorにチェックしたい種類のプロセッサを指定するらしく、@Immutableを読み込んでチェックしたい場合は以下のようにIGJCheckerを指定する。Dateに対する@Immutableの例では、date.setHoursするとjavacからエラーが返ってくる。
checker-framework-1.9.7/checker/bin/javac -Xlint:deprecation -processor org.checkerframework.checker.igj.IGJChecker App.java App.java:31: 警告: [deprecation] DateのsetHours(int)は非推奨になりました date.setHours(3); ^ 警告1個
他にもSQLインジェクションに繋がる可能性のあるパラメータは@Untainted、正しい正規表現文字列かの検証に@Regexなど、様々なチェックルールが用意されている。
Mavenなどのビルドツールへの組み込み方については公式ドキュメント参照。セッションでは、CheckerFrameworkのEclipseプラグインの紹介もあり、コードを書きながらバグを検知するデモが行われた。
FindBugsなどの既存ツールとの違いは、NPEの可能性を見逃さないこと。セッションでは、FindBugsでは検出できなかったNPEが、CheckerFrameworkなら検出できると紹介されていた。デメリットはアノテーションを付与しなければいけないことだが、@Nonnullなどを実際に付与するべき部分は意外と少なく、負担よりも見返りの方が大きいとのこと。GoogleやWall Streat Journalでも使われていることを紹介していた。
[CON2554] Java EE 8 Work in Progress
Java EE 8 の検討状況に関するセッション。内容は去年とあまり変わらず(去年の主な内容はJavaOne報告会2014のスライド参照)。去年見かけなかった気がするもののみまとめ。いずれもまだEarly Draft Reviewの状況であるため、今後変更される可能性あり。
JsonArray contacts = ...; JsonArray femaleNames = contacts.getValueAs(JsonObject.class).stream() .filter(x -> "F".equals(x.getString("gender")) .map(x -> x.getString("name")) .collect(JsonCollectors.toJsonArray());
JMS2.1: MDBのシンプル化のアイディア。
コネクションファクトリの取得、MessageListenerインタフェースの実装が不要となる。従来はMessage.getXXXでメッセージに含まれるパラメータを取得していたが、これもコールバックメソッドの引数に@MessagePropertyにより設定される。
@MessageDriven public class MyMDB { @JMSQueueListener(destinationLookup="java:global/requestQueue") public void myCallback(@MessageProperty("price") long price) { .... } }
Java EEセキュリティ: パスワードエイリアスの標準APIサポート。
APサーバ固有のAPIに依存せずに、APサーバに事前に登録されたパスワードエイリアスを利用する。
@DataSourceDefinition( name="java:/app/MyDataSource" className="com.example.MyDataSource" ... user="duke", password="${ALIAS=dulePassword}")
Java EEは今後どうなるかと思っていたが、JAX-RSやCDIの導入時のような大きな変化はないが、マイナーアップデートレベルで徐々に進んでいる。
[CON5118] Introduction to Modular Development
jigsawの導入セッション。
モジュールとは、パッケージを束ねた新しい単位。module -> package -> class -> field, Method の順番でグルーピングしている。モジュールのメタ情報は、jarファイルごとに含めるmodule-info.javaに定義する。ソースファイルのトップレベルに配置する。
module-info.java com/foo/bar/alpha/Alpha.java com/foo/bar/alpha/AlphaFactory.java com/foo/bar/beta/Beta.java
module-info.javaの中身は以下のように、依存するモジュール(requires)と、他のモジュールに公開するパッケージ(exports)が含まれる。例えば以下の例では、JDBCのモジュールに依存し、2つのパッケージを外部モジュールに参照可能とする。exportsに含まれていないパッケージはpublicであっても、他のモジュールから参照できない。 jigsawの登場により『public ≠ accessible』となった。
module com.foo.bar { requires java.sql export com.foo.bar.alpha: export com.foo.bar.beta; }
モジュールの推移的依存について。
例えば、java.sqlは java.logging モジュールに依存している。java.sqlモジュールに依存するアプリケーションのモジュールでもjava.sqlに依存する場合は、明示的にrequiresを書く必要はない。以下のjava.sqlのように『requires public』となっていると、このモジュールの参照元にもjava.loggingモジュールは公開される。
module java.sql {
requires public java.logging;
}
各モジュールはバージョンを持つ。例えばJDK同梱ライブラリの場合、java -listmodsでモジュールのバージョンが表示される。以下の結果は、JDK9 Early Access with Project Jigsawで実行した結果。
$ java -listmods java.activation@9.0 java.annotations.common@9.0 java.base@9.0 java.compact1@9.0 ...
コンパイル時には、module-info.javaと一緒にコンパイルする。このsrcの下にモジュール名のフォルダ(com.foo.baz)を切る構成は、jigsawのQuick Start Guideでも見かけた。Jigsaw利用時は一般的になるのだろうか。
javac -d mods/com.foo.baz src/com.foo.baz/module-info.java src/com.foo.baz/com/foo/baz/Bazooka.java
コンパイル時に依存モジュールがある場合は、-classpathではなく、-modulepath dir1:dir2:dir3 のようにモジュールが置かれているパスを指定する。-modulepathは-mpと省略記法にすることも可能。この点はクラスパスの-cpと似ている。
javac -modulepath mods -d mods/com.foo.bar src/com.foo.bar/module-info.java src/com.foo.bar/com/foo/bar/alpha/Alpha.java
実行時にも、クラスパスではなくモジュールの格納ディレクトリを-modulepathで指定。メインクラスを含むモジュールは-mで指定。
以下の例ではcom.foo.appモジュールの、メインクラスcom.foo.app.Mainを指定。
java -modulepath mods -m com.foo.app/com.foo.app.Main
JARパッケージングは今までと同じ方法も可能。module-info.classを一緒に含めること。module-infoが入っているjarに対して、jar -pすると、モジュール情報が表示される。
$ jar --file mlib/app.jar -p Name: com.foo.app Requires: com.foo.bar java.base [MANDATED] java.sql Main class: com.foo.app.Main
Java9より、jlinkというコマンドが含まれており、アプリのモジュールと実行可能なイメージとして配布可能。写真を取ってなかったので、以下の例はjigsaw Quick Startを試したときのもの。以下のようにjlinkすると、
jlink --modulepath $JAVA_HOME/jmods:mlib --addmods com.greetings --output greetingsapp
以下のようにjavaコマンドを含む、実行に必要なファイルが--outputに指定したディレクトリに出力される。
./greetingsapp ./greetingsapp/bin ./greetingsapp/bin/com.greetings ./greetingsapp/bin/java ./greetingsapp/bin/keytool ./greetingsapp/conf ...
以下のように、Unix系OSの実行ファイルのような感覚で起動することができる。
cd greetingsapp/bin/ # com.greetingsモジュールのmainメソッドの起動 ./com.greetings
他にも java -mp mlib -addmods com.foo.baz -cp app.jar com.foo.app.Main のように、クラスパスとモジュールパスの組み合わせの話が出たが、詳細は聞き取れなかった。
ここからは感想。jigsawはまずhttps://jdk9.java.net/jigsaw/からEarly Accessを取得して、Quick Startを試してみると良いと思った。
[CON6821] Advanced Modular Development
Jigsawに対応していないjackson-core/jackson-databind/jackson-annotationsの各jarに依存するアプリを、どのようにモジュール対応させていくかの話。聞き取れなかった部分も多かったため、後日復習してからまとめる。わかった範囲でメモ。
トップダウンアプローチ:
- jacksonの各jarはそのまま使い、アプリをモジュール対応させる場合のアプローチ
- まず、アプリが依存しているモジュールやjarを jdepsコマンド で抽出する
- 特にmodule-info.javaが含まれていないライブラリでも、Automatic modulesとしてモジュールとして扱える?
- その代わりjarに含まれるすべてのパッケージはexportsされる
- 今までのjarをモジュールとして流用した場合、すべてのモジュールに対してはrequiresになる? (ここ間違ってるかも)
- アプリのmodule-info.javaには以下のように記載
module myapp { requires mylib; requires java.base; requires java.sql; requires jackson.core; requires jacson.databind; }
ボトムアップアプローチ:
[CON3942] What’s Coming in JMS 2.1
前半はJMS2.0 (Java EE7) の振り返りであったため省略。
後半から、現在検討中のJMS2.1の話。前述の [CON2554] Java EE 8 Work in Progress で出てない話としては、以下のようなコード例があった。
@MDBが1クラス1リスナに対して、複数のキューに対するリスナを盛り込める案:
@MessageDriven public class MyFlexibleMDB { @JMSQueueListener(destionationLookup="java:global/queue1") public void myMessageCallback1(String messageText) { ... } @JMSQueueListener(destionationLookup="java:global/queue2") public void myMessageCallback2(Stromg messageText) { ... } }
再接続ポリシーなど、JMS実装固有のプロパティをアノテーションで指定する案:
GlassFishの再接続設定の場合。従来はasadmin経由でAPサーバに設定したが、コードに書けるようにすることを目的とする。
@MessageDriven public class MyFlexibleMDB { @JMSQueueListener(destinationLookup="java:global/queue1") @JMSListenerProperty(name="reconnectAttempts" value="10") @JMSListenerProperty(name="reconnectInterval", value="10000") public void myMessageCllback1(String messageText) { ... } }
CDI管理Beanでメッセージ受信することも検討しているらしいが、色々と課題がある。
- いつリスナクラスのインスタンスを生成する? @Dependentで毎回生成? @ApplicationScopedにする?
- CDIイベントの機能と似たようなものになってしまう (永続化の有無はあるが)
ここからは感想。個人的にJMSは1.1のイメージが抜けなくて、あまり使いたくなかったが、ここまでシンプルになるなら嬉しい。Java EEに大きな機能追加がなくても、小さなマイナーアップデートは続いてほしい。
[CON6823] Project Jigsaw: Under the Hood
Jigsawをより深く紹介するセッション。
聞き取れなかったので、スライド公開後に復習する。わかった分だけメモ。
- 今までのスコープはpublic/protected/パッケージ/private
- Jigsawによるモジュール化後は以下のようになる。publicが細分化。
- public for everyone (exportsしたもの)
- public but only to specific modules (特定のモジュールに対して公開。どうやる?)
- public only within a module (モジュール内のみ。exportsしてないpublicパッケージ)
- proceted / パッケージプライベート / private
- モジュールが入っても、JDK9には3層のクラスローダ(bootstrap/ext/application)は残っている
- モジュール間の循環参照はいけない。依存関係のグラフ解決ができない。
JavaOne 2015 1日目メモ (10/25)
昨年よりも約1ヶ月遅いJavaOneが始まった。初日はコミュニティ主体のセッションであるユーザグループフォーラムと、キーノートがサンフランシスコのモスコーンセンタにて行われた。以下、参加メモ。
- [UGF11082] WebSocket Perspectives
- [UGF11073] Who Wants to Be a Millionaire? Dealing with Money and Currency in Java Applications
- Java Keynote
[UGF11082] WebSocket Perspectives
スピーカーはニューヨークにあるJavaユーザグループNYJavaSIGのリーダFrank Grecoさん。WebSocketはあくまでトランスポートレイヤの仕組みで、WebにTCPを持ち込んだが、そもそもなんでWebにTCPが必要になったかの話。
その背景には、ここ10年で以下のような変化があったこと。
- WebはページからAPIサービスの塊になり、目に見えないものになった
- いまやカウント対象はページビューではなく、APIの呼び出し回数
- RESTで実装されるようになったが、リクエスト/レスポンスモデルは待ちが多い
- 非同期処理で待ちを軽減させていた。ReactiveXが解決策。
- マイクロサービスの普及。Webアプリケーションが1つの小さいプロセスの連携であるThe Unix Wayの考え方に近づいている。
- マイクロサービスのペナルティもリクエスト応答の待ち時間。
WebSocketはあくまでトランスポートレイヤであるため、例えばAMQPをWebSocket上でやり取りすることもできる。リクエスト/レスポンスモデルが主体のWebの世界に、public/subscribeモデルが持って来れる。
実際にWebSocket/AMQPを使ったいくつかのチャット、ゲームのデモアプリケーションを紹介。デモはKAAZING.orgに公開されている。公開されていないデモの紹介として、ドローンのWeb画面によるモニタリングアプリを紹介。高度、進行方向、プロペラ回転数などの状態は常に変化するため、WebSocketだからこそ実現できる。
ここからは感想。今までWebSocketの双方向通信の使い道はゲームぐらいしか思い当たらなかったが、ドローンが監視できることは、企業のシステムでも機械の監視がWebで出来るを思った。例えば、過加熱、消費電力、通信量など、常に変化して、今までは定点間のデータは捨てていた場合、WebSocketが使えるのでは。
[UGF11073] Who Wants to Be a Millionaire? Dealing with Money and Currency in Java Applications
JSR 354 - Currency and Moneyについて。お金を扱うAPIの標準化について。
たびたびJavaOneでも見かけたが、2015/5に仕様策定が完了したとのこと。Java SEをターゲットとした仕様だが、JDK9 or JDK10のいつ盛り込まれるかは未定。
内容はすべてライブコーディングによるAPIのデモ。
mavenのセントラルリポジトリにも既に登録されており、以下の依存性を追加すると実際に参照実装が試せるようになっている。
<dependency> <groupId>org.javamoney</groupId> <artifactId>moneta</artifactId> <version>1.0</version> </dependency>
お金がintやBigDecimalのような数値でなく、MonetaryAmountとして抽象化されている。MonetaryAmountはイミュータブル。MonetaryAmount.addなどすると、別のオブジェクトが返ってくる。
// 100円 CurrencyUnit yen = Monetary.getCurrency("JPY"); MonetaryAmount yen100 = Money.of(100, yen); CurrencyUnit yen = Monetary.getCurrency("JPY"); MonetaryAmount yen50 = Money.of(50, yen); System.out.println(yen100.add(yen50)); //=> JPY 150 // 10.25ドル CurrencyUnit dollar = Monetary.getCurrency("USD"); MonetaryAmount money = Money.of(10.25, dollar);
Stream APIを使って通貨ごとのサマリデータを取得することが可能。
CurrencyUnit yen = Monetary.getCurrency("JPY"); MonetaryAmount money = Money.of(100, yen); MonetaryAmount money2 = Money.of(200, yen); MonetaryAmount doller1 = Money.of(10.12, Monetary.getCurrency("USD")); MonetaryAmount doller2 = Money.of(20.12, Monetary.getCurrency("USD")); MonetaryAmount euro = Money.of(1, Monetary.getCurrency("EUR")); List<MonetaryAmount> moneys1 = Arrays.asList(money, money2, doller1, doller2, euro); // 日本円のサマリデータを取得 Map<CurrencyUnit, MonetarySummaryStatistics> summary = moneys1.stream().collect(MonetaryFunctions.groupBySummarizingMonetary()).get(); MonetarySummaryStatistics yenSummary = summary.get(yen); System.out.println(yenSummary.getAverage()); // JPY 150 System.out.println(yenSummary.getSum()); // JPY 300 System.out.println(yenSummary.getMax()); // JPY 200 System.out.println(yenSummary.getMin()); // JPY 100
為替情報も取得でき、異なる通貨値の変換ができる。wifiを落とすと例外がスローされたので、RIでは最新レート情報をWeb上に取得に行っているようだ。
// 為替レートの取得 ExchangeRateProvider exchangeRateProvider = MonetaryConversions.getExchangeRateProvider("IMF"); ExchangeRate rate = exchangeRateProvider.getExchangeRate("USD", "JPY"); System.out.println(rate.getFactor()); // => 120.93957... (2015/10/25のドル->円レート) // 1ドルを日本円に変換 CurrencyConversion dollarConversion = MonetaryConversions.getConversion("JPY"); MonetaryAmount oneDollar = Money.of(1, "USD"); System.out.println(oneDollar.with(dollarConversion)); // => JPY 120.9395779...
いくつかのサンプルコードがgithubのjavamoney-examplesで公開されている。
複数の通貨を扱うアプリケーションでは、いままでBigDecimalとBigDecimalの数値が表す通貨単位情報を1つのクラスにラップしていたと思うので、このAPIを使うとシンプルにコードが扱えるのでは。日本円だけを計算する場合の有効性はもう少し調べてみる必要がある。
Java Keynote
いろいろな所で紹介されると思うので詳細は省略。
jigsawが何を解決させるかについて、以下の3点を掲げていたのは去年のJavaOneでは説明されていなかったと思う。
- Is anything missing?
- NoClassDefFoundErrorが出ても、クラスパスに含まれる大量のjarから、どのjarが漏れてるかわからない
- Are the any conflicts?
- maven使っても、commons-binutil-1.7.0と1.8.0の両方が依存性に含まれることがある。別バージョンの競合
- Is it safe to change internal API?
- アプリ内の他パッケージに見せるためにpublicにしたのに、意図しないアプリ外から参照される。内部APIでも容易に変更できない。
classpathは依存関係が定義できないのが弱点。moduleは新しいグループの単位で、module->package->class->filed,method の順に位置付けされる。依存性の定義は、module-info.javaに定義される。
module com.greetings { // このモジュールが依存するモジュール requires test.common; // 他のモジュールからも参照されても良いパッケージ exports com.greetings.api; }
jigsawの具体的な使い方は明日以降のセッションで説明があるとのこと。
WeldのProxyクラスはどこからやってくるか
CDIの参照実装 Weld では @Dependent を除いて、全てのCDI管理Beanは直接インスタンスがインジェクションされずに、以下のようにプロキシが設定*1される。
test.Message$Proxy$_$$_WeldClientProxy
例外発生時のスタックトレースや、スレッドダンプでも見かける $Proxy$_$$_WeldClientProxy
だが、どこからやってきているのか調べてみた。
疑問に思っていた事
Javaで一般的にプロキシといったら、以下のような手段が取られると思う。
JDK付属プロキシでは、インタフェースに対してプロキシを設定するため、CDIのようにインタフェースなしのクラスに対してプロキシを設定できず、利用できない。また、Weldのpom.xmlを見ても、objenesisやcglibへの依存が見られないため、ライブラリも使っていないようである。
WeldのProxyクラスは一体どこからやってくるのか?
WeldのProxyの生成はどうやっているか
org.jboss.weld.bean.proxy.ProxyFactoryで、プロキシの生成は行われている。
大まかな処理の流れは以下の通り。
- ProxyFactory.create(BeanInstance beanInstance)
- ProxyFactory.run()
- ProxyFactory.getProxyClass()
- ProxyFactory.getProxyClass()
- 対象のプロキシクラスが既にクラスローダにロード済みであれば、newInstanceメソッドで生成。
- 未ロードされていない場合は、createProxyClass()を呼び出して、動的にプロキシクラスの生成とロードをする
- ProxyFactory.createProxyClass()
- Weldとは別モジュールのjboss-classwriterに含まれる、org.jboss.classfilewriter.ClassFileを使って、動的にバイトコード(クラスファイル)を生成。
- org.jboss.weld.util.bytecode.ClassFileUtils.toClassメソッドによって、動的に生成したバイトコードを、ClassLoader.defineClassメソッドをリフレクションで呼び出してクラスロード。
Proxyの生成に関連するところを太字にしたが、Weldは外部OSSライブラリに頼らずに、JBossの関連モジュールのみで、プロキシクラスの動的なバイトコード生成と、クラスローダ経由でのロードを行っている。
org.jboss.classfilewriter.ClassFileを見ると、以下のようにクラスファイル先頭に出てくるCAFEBABE
の出力が見られ、バイトコード作ってる感がある。
205 stream.writeInt(0xCAFEBABE);// magic 206 stream.writeInt(version); 207 constPool.write(stream);
Weldの場合、バイトコードの生成とクラスロードが別々のクラスに分かれているが、やっていることはobjenesisのClassDefinitionUtilsクラスと似たように見える。objenesisでも動的なバイトコード生成と、ClassLoader.defineClassメソッドの呼び出しを行っている。
まとめ
*1:Bean間の循環参照を許容するため、異なるスコープを持ったBeanの参照依存関係を分離するため
JSF (mojarra) ステートレスビューを試してみる
JSF2.2のビッグチケットとして盛り込まれたステートレスビューですが、HttpSessionへのアクセス量を減らせるメリットの一方、制約事項もあります。Wildfly-9.0.0.CR2(mojarra-2.2.11)で色々と試してみたので以下にまとめます。
ステートレスビューの使い方
<f:view transient="true">
で囲うだけで、対象のビューのフォーム値などの状態をセッションに保持しなくなります。
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:f="http://java.sun.com/jsf/core" xmlns:h="http://java.sun.com/jsf/html"> <f:view transient="true"> <h:head> <title>JSF Hello World</title> </h:head> <h:body> <h1>JSF Hello World</h1> <h:form> ... </h:form> </h:body> </f:view> </html>
いつから使える?
仕様としてはJSF2.2の新機能としてJAVASERVERFACES_SPEC_PUBLIC-1055として作成されましたが、JSFのRIであるmojarraではJAVASERVERFACES-2731の修正により、Java EE6世代の2.1系である2.1.19から使えるようになっています。
ステートレスビューのメリット
一般的には以下のようなメリットが得られると言われています。
- レスポンスタイム向上
- スケール性の向上・ヒープ使用量の低減
手元のHelloWorldアプリでは違いがよくわかなかったですが、スループット・レスポンスタイム向上度については、アプリケーションにも依存すると思うので、既存のアプリに<f:view transient="true">
を入れて測ってみるのがおすすめです。
mojarra-2.2.11の場合、シリアライズされたビュー状態をcom.sun.faces.renderkit.ServerSideStateHelper.LogicalViewMapという属性名でセッションに登録しているようですが、ステートレスビューにするとLogicalViewMapへの登録がなくなりました。あくまで仮説ですが、大規模にセッションレプリケーションしている環境では、HttpSessionへのアクセスが減ることでシステム全体のスループットにも好影響があるのかと思います。
ステートレスビューのデメリット
一方で、以下のようなデメリットもあります。
- ViewScopeが使えない
- CSRF対策が必要
ViewScopeが使えない
JSFのAjax機能を使う時によく使う、同一ビューへのリクエストであれば値を保持し続けるViewScopeが使えません。Primefacesなどの内部的にAjaxが使われているコンポーネント利用時にも注意が必要と思います。ステートレスビューとViewScopeを組み合わせると、アクセス時に以下のような警告ログが出力され、ViewScopeが設定されていてもRequestScopeと同様に振る舞います。
2015-06-21 20:27:23,737 WARNING [com.sun.faces.application.view.ViewScopeManager] (default task-2) @ViewScoped beans are not supported on stateless views
JSF2.2においてCDIでもViewScopeを定義できるアノテーションが導入されていますが、いずれの場合でもステートレスビューと組み合わせることはできません。
CSRF対策が必要
今までのステートフルビューでは、hiddenフィールドのjavax.faces.ViewState
が暗黙的なCSRF対策トークンとして機能しており、この正しいID値をPOSTしないとビューが復元できず、業務ロジックが実行される前のRESTORE VIEWフェーズで例外となっていました。
<input type="hidden" name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="1867897074285420267:8829522400179303592" autocomplete="off" />
ステートレスビューでは、ViewStateに以下のように固定的な文字列stateless
が割り当てられる為、CSRF対策トークンとして機能しません。
<input type="hidden" name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="stateless" autocomplete="off">
JSF2.2から盛り込まれた明示的なCSRF対策トークン付与機能によって以下のように設定することもできますが、CSRF対策したい部分(購入完了、決済完了などDB登録処理)のビューを個別に指定する手間は掛かります。JSFのフォームPOST先のURLは、現在表示しているViewであることを意識しながら設定するのはそれなりに大変です。
<?xml version="1.0" encoding="UTF-8"?> <faces-config xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-facesconfig_2_2.xsd" version="2.2"> <protected-views> <url-pattern>/inputform.xhtml</url-pattern> </protected-views> </faces-config>
この設定を加えてinputform.xhtmlにフォワードさせると、以下のようにformのaction属性の値にトークンが付与されます。このトークンの値がない/間違ってる状態でHTTP POSTされると、javax.faces.application.ProtectedViewException
がスローされ、RESTORE VIEWフェーズの途中で処理が終了します。
<form id="j_idt7" name="j_idt7" method="post" action="/jsftest/input.xhtml?javax.faces.Token=i0hAEnknIkohc0GnhQ%3D%3D" enctype="application/x-www-form-urlencoded">