JavaOne 2015 2日目メモ (10/26)

今日から各種テクニカルセッションの開始。jigsawのセッションにいくつか参加。

[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の状況であるため、今後変更される可能性あり。

JSON-P1.1: ストリームAPIの適用のアイディア

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-RSCDIの導入時のような大きな変化はないが、マイナーアップデートレベルで徐々に進んでいる。

[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.sqljava.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;
}

ボトムアップアプローチ:

  • jacksonをモジュール対応させ、モジュール化したjacksonにアプリのモジュールが依存するアプローチ
  • トップダウンと同じように jdepsコマンド でjacksonが依存するモジュールやjarを抽出
  • jdeps -genmoduleinfo src *.jar でmodule-info.javaを自動生成?
    • 自動生成したmodule-info.javaは、全てのパッケージがexportsされているため、モジュールプライベートにしたいパッケージは消す
  • リフレクションで依存していた場合については、聞き取れなかった

[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にする?
    • @ApplicationScopedにして、マルチスレッド呼び出しを許容すると、ユーザはスレッドセーフの意識が必要となる
    • では呼び出し毎にインスタンスを分けるなら、いくつプールする?
    • @MaxInstance(10) のようなアノテーションCDI管理Beanに付与して、プール数を指定する?
  • 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)は残っている
    • クラスローダごとに読まれるモジュールが違う。
    • 例えばbootstrapクラスローダでは、JDKのコアモジュールとなるjava.baseがロードされる
  • モジュール間の循環参照はいけない。依存関係のグラフ解決ができない。

JavaOne 2015 1日目メモ (10/25)

昨年よりも約1ヶ月遅いJavaOneが始まった。初日はコミュニティ主体のセッションであるユーザグループフォーラムと、キーノートがサンフランシスコのモスコーンセンタにて行われた。以下、参加メモ。

[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...

いくつかのサンプルコードがgithubjavamoney-examplesで公開されている。
複数の通貨を扱うアプリケーションでは、いままでBigDecimalBigDecimalの数値が表す通貨単位情報を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で一般的にプロキシといったら、以下のような手段が取られると思う。

  • java.util.reflect.Proxy によるプロキシ実装
  • objenesiscglibなどのライブラリ

JDK付属プロキシでは、インタフェースに対してプロキシを設定するため、CDIのようにインタフェースなしのクラスに対してプロキシを設定できず、利用できない。また、Weldのpom.xmlを見ても、objenesisやcglibへの依存が見られないため、ライブラリも使っていないようである。

WeldのProxyクラスは一体どこからやってくるのか?

WeldのProxyの生成はどうやっているか

org.jboss.weld.bean.proxy.ProxyFactoryで、プロキシの生成は行われている。
大まかな処理の流れは以下の通り。

  1. ProxyFactory.create(BeanInstance beanInstance)
  2. ProxyFactory.run()
    • java.security.PrivilegedActionの実装。この後の処理でリフレクションAPIを実行するため、java.security.*によるアクセス制御対象になっている。
  3. ProxyFactory.getProxyClass()
  4. ProxyFactory.getProxyClass()
    • 対象のプロキシクラスが既にクラスローダにロード済みであれば、newInstanceメソッドで生成。
    • 未ロードされていない場合は、createProxyClass()を呼び出して、動的にプロキシクラスの生成とロードをする
  5. ProxyFactory.createProxyClass()

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メソッドの呼び出しを行っている。

まとめ

  • WeldのClientProxyは、$Proxy$_$$_WeldClientProxyを接尾子としたプロキシクラスのバイトコード生成とクラスロードをjava.util.reflect.Proxyや外部OSSライブラリを利用せずに独自に実装している。
  • org.jboss.weld.bean.proxy.ProxyFactoryから追いかけていくと流れがよくわかる。

*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が使えない

JSFAjax機能を使う時によく使う、同一ビューへのリクエストであれば値を保持し続ける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を定義できるアノテーションが導入されていますが、いずれの場合でもステートレスビューと組み合わせることはできません。

  • javax.faces.bean.ViewScoped (JSF2.0導入, JSFの@ManagedBeanと使う)
  • javax.faces.view.ViewScoped (JSF2.2導入, CDIの@Namedと使う)
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であることを意識しながら設定するのはそれなりに大変です。

WEB-INF/faces-config.xml

<?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">

まとめ

  • mojarra2.1.19(EE6系)および2.2.0-m10(EE7系)より、ステートレスビューが利用可
  • JSFステートレスビューには制約事項もある
    • ViewScopeが使えないので、Ajax機能が使いにくい
    • javax.faces.ViewStateによるCSRF対策ができないため、別の対策が必要
  • 制約事項を考えると、特に困ってないなら無理にステートレスビューを使う必要はないかと思う。遅いビューに部分的に使いたい。

JSF (mojarra2.x) のデバッグログ出力

多くのアプリケーションのログカテゴリには、ログを出力しているパッケージ名(jp.co.test... など)が使われていますが、JSFの参照実装であるmojarra2.xでは少し異なる付与規則となっているため、以下にまとめます。

mojarraのログカテゴリ定義

mojarraのログカテゴリはjavax.enterprise.resource.webcontainer.jsfから始まります。mojarraの実装パッケージはcom.sun.facesから始まっていますが、com.sun.facesをログカテゴリに設定してログレベルを上げても何も出力されません。

カテゴリの一覧はソースコード上のcom.sun.faces.util.FacesLoggerenumとして定義されています。以下のようなカテゴリ定義があります。

  • javax.enterprise.resource.webcontainer.jsf.application
  • javax.enterprise.resource.webcontainer.jsf.resource
  • javax.enterprise.resource.webcontainer.jsf.config
  • javax.enterprise.resource.webcontainer.jsf.context
  • javax.enterprise.resource.webcontainer.jsf.facelets
  • javax.enterprise.resource.webcontainer.jsf.lifecycle
  • javax.enterprise.resource.webcontainer.jsf.managedbean
  • javax.enterprise.resource.webcontainer.jsf.renderkit
  • javax.enterprise.resource.webcontainer.jsf.taglib
  • javax.enterprise.resource.webcontainer.jsf.timing

JSFライフサイクルのロギング

javax.enterprise.resource.webcontainer.jsf.lifecycleカテゴリのログレベルをFINE以上に変更すると、JSFライフサイクル(Restore View -> Apply Request Values ..)の開始・終了時間のロギングが可能です。PhaseListenerによるロギングのようにアプリケーション自体をリビルドしなくても、APサーバの設定変更のみでログ出力が可能です。

ログ出力例

WildFly8.2.0でjavax.enterprise.resource.webcontainer.jsf.lifecycleのログレベルをDEBUGに変更すると、以下のようなログが出力されます。

2015-06-14 16:31:34,393 FINE  [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-2) execute(com.sun.faces.context.FacesContextImpl@5f6d3a9e)
2015-06-14 16:31:34,393 FINE  [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-2) Entering RestoreViewPhase
2015-06-14 16:31:34,417 FINE  [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-2) Postback: restored view for /index.xhtml
2015-06-14 16:31:34,417 FINE  [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-2) Exiting RestoreViewPhase
2015-06-14 16:31:34,418 FINE  [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-2) Entering ApplyRequestValuesPhase
2015-06-14 16:31:34,419 FINE  [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-2) Exiting ApplyRequestValuesPhase
...

長くなるため完全な出力例は省略しますが、ログレベルさらに上げると、RenderResponsePhaseのロギングとして、以下のようにUIViewRootをルートとしたコンポーネントツリーの状態をロギングすることもできます。

2015-06-14 16:31:30,817 FINEST [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-1) +=+=+=+=+=+= View structure printout for /index.xhtml
2015-06-14 16:31:30,822 FINEST [javax.enterprise.resource.webcontainer.jsf.lifecycle] (default task-1) id:j_id1
type:javax.faces.component.UIViewRoot
...
APサーバ毎のログレベル設定方法

GlassFish4の場合の設定方法
mojarraのログカテゴリは、デフォルトのlogging.propertiesに既に定義されているため、デフォルトのINFOから好きなログレベルに変更するだけです。ログレベルの表記はSEVEREからFINESTまでのjava.util.logging形式です。一覧はLevelクラスのjavadocに定義されています。

glassfish4/glassfish/domains/domain1/config/logging.properties

javax.enterprise.resource.webcontainer.jsf.lifecycle=FINE

WildFly8 (JBoss) の場合の設定方法
CLIまたは、standalone.xmlを編集してログカテゴリ設定を追加します。java.util.loggingとJBoss Loggingのログレベルの対応表は、jbossのwikiに記載があります。

CLIの場合

${WILDFLY_HOME}/bin/jboss-cli.sh -c
/subsystem=logging/logger=javax.enterprise.resource.webcontainer.jsf.lifecycle:add(level=DEBUG)

standalone.xmlを編集する場合 (jacorbの下に追加)

<logger category="jacorb.config">
    <level name="ERROR"/>
</logger>
<logger category="javax.enterprise.resource.webcontainer.jsf.lifecycle">
    <level name="DEBUG"/>
</logger>
<root-logger>

まとめ

  • mojarra2.xのログカテゴリはcon.sun.facesのような実装パッケージ名でなく、javax.enterprise.resource.webcontainer.jsfから始まる。
  • この仕組みを使ってJSFライフサイクルのデバッグができる

書籍「Javaパフォーマンス」を読んで

監訳者の@cero-tさんから頂きました。@cero-tさん、ありがとうございます。

Javaパフォーマンス

Javaパフォーマンス

Javaトラブルシューティングに関する仕事に関わっていると、まだ切り分けができていない性能遅延の原因について、GCが疑われることが良くあります。『自動で動く』ことによるブラックボックス感によりGCは疑われやすくなっていると思います。

しかし、実際に色々な案件の解析を繰り返すと、性能遅延の要因は多種多様です。過去に遭遇した代表的なものには、GC以外にも以下のような遅延要因があります。

  • アプリケーションの不効率なロジック (ループ過多、オブジェクト生成過多)
  • 過度なロック、同期漏れ/volatile漏れから無限ループ発生によるCPU消費
  • ローカルJVM内でリモートEJBを呼び出すことによるシリアライズ遅延
  • JDBC executeBatch()未使用でのバルク更新
  • JPAのフェッチ設定誤り(N+1問題)による大量SQL発行
  • 初期化コストの高いFactoryの多重初期化 (EntityManagerFactory, パーサのFactory)

本書 Javaパフォーマンス のスコープは非常に広く、上記のようなJVMJava SE APIJava EEの広い範囲の性能課題について触れています。本書のような内容が日本語では今までなかったため、トラブル時にはJava SeriesのJava Performanceで苦戦しながら調べていましたが、本書も机の上に置いておこうと思います。

以下、個人的に嬉しかった部分をまとめます。

JITに関する言及が嬉しい

JITがどのようなコードをコンパイル対象としていて、どのようなタイミングでコンパイルされるのか、各種閾値やOn Stack Replacement、エスケープ分析について書籍で見かけたことがなかったため、仕組みを理解する上で役立ちました。
トラブルが起こると『どういう仕組みで問題が起こったのか』の説明が必要となるケースがよくあります。コードキャッシュの溢れが起こった場合に『そもそもJITって?』の説明を求められた場合に参考にしようと思います。

具体的なGCログが嬉しい

CMS-GCのpromotion failedやconcurrent mode failure、G1GCのto-space overflowやFullGCの発生など、何らかのチューニングを行った方が良い状況を示すGCログが具体的に示されているため、トラブル時にすぐに使える内容になっています。

新しい解析ツールの解説が嬉しい

HotSpotとJRockitの統合に伴い、Java7以降に様々なツールがOracleJDKに付属されるようになっています。jcmd、Java FlightRecorder、Native Memory Trackingについて本書では解説されており、FlightRecorderについてはセットアップ方法だけでなく、収集対象イベントの調整や、JavaヒープのEdenへの割り当てを高速化させるTLAB(thread local allocation buffer)が十分かの考察方法にまで踏み込んで記載しています。

まとめ

Javaパフォーマンス、以下のような方におすすめです。

  • Java性能問題が起こるポイントを網羅的に把握したい方
  • JDK付属の各種解析ツールのリファレンスとして
  • JITGCの仕組み、volatile、SoftReference/WeekReferenceなどの一歩踏み込んだJavaを知りたい方

PostgreSQL JDBCドライバのタイムアウト設定

OracleJDBCドライバと同様に、PostgreSQLJDBCドライバにも同様のタイムアウト設定が用意されています。@yamadamnさんがWebLogicServer + Oracle JDBC向けにまとめた資料Oracle JDBCドライバプロパティの活用を参考に、WildFly + PostgreSQL版のタイムアウト設定を以下にまとめます。

データベース接続時のタイムアウト

PostgreSQLのデータベース接続時のタイムアウトには2種類のパラメータがあります。いずれもデフォルトは未設定で、Javaのレイヤではタイムアウトの設定はされず、NW障害時やDBハングアップ時にはOSのTCP接続タイムアウトまで待ちます。

loginTimeout=<秒>

このタイムアウト設定はTCP接続のタイムアウトではなく、ログイン処理全体のタイムアウトを示します。PostgreSQLへのログイン処理は、大まかに4つのステップに分かれていますが、これら4ステップの完了までがタイムアウトの範囲です。

PostgreSQL JDBCのログイン処理の流れ
1. java.net.Socket.connectによるTCP接続
2. Startup messageの送信
(ユーザ名、接続先DB、クライアント側の文字エンコーディング送信など)
3. 認証要求
4. Initial Queryの実行 (SET extra_float_digits = 3 の実行)

loginTimeoutの値を超えてもログイン処理が完了しない場合、タイムアウト監視スレッドがログイン処理のスレッドに割込みを掛けて、以下のような例外が出力されます。

org.postgresql.util.PSQLException: 接続試行がタイムアウトしました。
	at org.postgresql.Driver$ConnectThread.getResult(Driver.java:374)
	at org.postgresql.Driver.connect(Driver.java:286)
	at java.sql.DriverManager.getConnection(DriverManager.java:664)
	at java.sql.DriverManager.getConnection(DriverManager.java:208)
	at net.agetsuma.jdbc.Main.main(Main.java:32)
connectTimeout=<秒> (9.3-1103より有効)

2015/01/02にリリースされたVersion 9.3-1103より有効なパラメータで、loginTimeoutと異なり、TCP接続時のみに着目したタイムアウトです。

具体的には、PostgreSQLに対して接続する時に実行するjava.net.Socket.connectメソッドの引数に渡しています。

// org.postgresql.core.PGStreamのコンストラクタから抜粋
Socket socket = new Socket();
socket.connect(new InetSocketAddress(hostSpec.getHost(), hostSpec.getPort()), timeout);
loginTimeoutとconnectTimeoutのどっちを使う?

loginTimeoutはconnectTimeoutの範囲を兼ねるため、NW障害やDB過負荷を考慮したタイムアウトとしてはloginTimeoutのみで十分と思います。

ソケット読み込み時のタイムアウト

Oracleと同様に、setQueryTimeoutでは救えないSQL実行中のDBハングアップなどの障害を考慮して、ソケット読み込みタイムアウトを設定することが可能です。

socketTimeout=<秒>

このタイムアウトを超えると以下のような例外が投げられます。

org.postgresql.util.PSQLException: An I/O error occurred while sending to the backend.
	at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:281)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.execute(AbstractJdbc2Statement.java:562)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.executeWithFlags(AbstractJdbc2Statement.java:420)
	at org.postgresql.jdbc2.AbstractJdbc2Statement.executeQuery(AbstractJdbc2Statement.java:305)
	at nagetsu.jdbc.Main.main(Main.java:39)
Caused by: java.net.SocketTimeoutException: Read timed out
	at java.net.SocketInputStream.socketRead0(Native Method)
        ...

JDBCプロパティのWildFlyデータソース設定方法

JDBCプロパティはCLIによって設定可能です。以下の例では、データソース名PostgresDSに対してloginTimeoutを60秒、socketTimeoutを600秒に設定しています。

data-source add --name=PostgresDS --jndi-name=java:/jboss/PostgresDS --driver-name=postgresql-9.3-1103.jdbc41.jar --connection-url=jdbc:postgresql://192.168.1.1/test --max-pool-size=xx --min-pool-size=xx --initial-pool-size=xx --pool-prefill=true --check-valid-connection-sql="SELECT 1" --set-tx-query-timeout=true --user-name=postgres --password=postgres --enabled=true
/subsystem=datasources/data-source=PostgresDS/connection-properties=loginTimeout:add(value=60)
/subsystem=datasources/data-source=PostgresDS/connection-properties=socketTimeout:add(value=600)

まとめ