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)

まとめ

CDI1.2によるbean-discovery-modeの見直し

2014年4月にCDI仕様のマイナーアップデート CDI1.1 -> CDI1.2 *1が行われ、@java.inject.Singletonが付与されたクラスはデフォルトではインジェクションできるBean対象から外れました。

後方互換性のない変更であるため、以下にまとめます。

default bean-discovery-modeの見直し

CDI1.1 (Java EE7) から導入されたbean-discovery-modeについての詳細は過去の記事にまとめていますが、簡単にいうと、以下の場合にどのクラスをインジェクション対象としてスキャンするかの仕様が見直されました。

  • warやearにbeans.xmlを含めなかった場合
  • またはbeans.xmlbean-discovery-mode="annotated"を明示していた場合

@java.inject.Singletonがスキャン対象から除外された

冒頭に書いたとおり、@javax.inject.Singletonはデフォルトのスキャン対象から除外されたため、以下のようなコードはCDI1.2よりデプロイ時にエラーになります。

@Path("/echo")
@RequestScoped
public class EchoResource {
  @Inject
  EchoService service;
    
  @GET
  public String echo(@QueryParam("name") String name) {
    return service.echo(name);
  }
}

@java.inject.Singleton
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

CDI1.2に準拠しているWildFly8.2.0で試すと、デプロイ時に以下のような例外メッセージが出力されます。@Singletonがスキャン対象から外されたため、依存性の解決例外になっています。

2015-01-11 16:12:54,944 ERROR [org.jboss.msc.service.fail] (MSC service thread 1-1) MSC000001: Failed to start service jboss.deployment.unit."CDITest.war".WeldStartService: org.j
boss.msc.service.StartException in service jboss.deployment.unit."CDITest.war".WeldStartService: Failed to start service
        ...
Caused by: org.jboss.weld.exceptions.DeploymentException: WELD-001408: Unsatisfied dependencies for type EchoService with qualifiers @Default
シングルトンをインジェクションしたい場合の対処

思い当たる範囲で、4つの対処があります。

対処案1. インジェクション先を@Singletonにする

インジェクション先のクラスのスコープを@Singletonにすることで、依存先のServiceクラスに『インジェクション先にスコープを合わせる』ことを示す@Dependentを付与すると、Serviceはシングルトンになります。

この案が使えるのは、インジェクション先(例ではJAX-RSエンドポイント)が状態を持たない場合のみです。以下のように、フィールドに状態を持っていた場合、@Singletonでは最初のリクエスト時にしかパラメータが設定されません。

@Path("/echo")
@javax.inject.Singleton
public class EchoResource {
  @Inject
  EchoService service;

  // ↓ 最初の1回目のリクエストしかパラメータが反映されない
  @QueryParam("name")
  String name;
    
  @GET
  public String echo() {
    return service.echo(name);
  }
}

@Dependent
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

JAX-RSCDIを組み合わせる場合はこの案でもなんとかなりますが、フィールドにフォーム入力値などの状態を持つ事が多いJSFと組み合わせる場合は対処案1は使えません。

追記: 書いてはみましたが、Serviceをシングルトンにしたいだけなのに、JAX-RSエンドポイントを変更するこの案はあんまりお勧めできません。

対処案2. @Stereotypeを使う
CDI1.2より@Steraotypeによって自作したアノテーションが付与されていた場合、デフォルトのスキャン対象となっているため、この仕組みを利用します。

ステレオタイプとは、スコープや@Transactionalなど、Beanの機能を示すアノテーションをまとめる仕組みです。JSF2ユーザは、一度は@Modelを使ったことがあると思いますが、@ModelのJavadocを見ると、@Stereotypeによって、@RequestScopedと@Namedを組み合わせたアノテーションであるとわかります。

@Singleton単体ではスキャン対象となりませんが、@Stereotypeが付与されたアノテーションが付与されたクラスはスキャン対象となるため、以下のように@Singletonをラップしたアノテーションを作ります。

@Singleton
@Stereotype
@Target(TYPE)
@Retention(RUNTIME)
public @interface Service {
}

作成したアノテーションを@Singletonの代わりに付与します。Spring Framework風に@Serviceにしてみました。

@Service
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

対処案1と比べて一手間掛かりますが、デメリットは特にありません。

対処案3. @ApplicationScopedを使う

リクエスト毎にインスタンス生成させない方法として、アプリケーションスコープを使う案もあります。今まではアプリケーションで共有のコンフィグ情報やキャッシュなどを持たせる印象が強いアプリケーションスコープですが、1つのインスタンスを共有する観点ではシングルトンと同じように使えます。

@ApplicationScoped
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

対処案4. ServiceはEJBで作る

スコープに困ったら、Serviceクラスのようなトランザクション境界となるクラスはJavaEE6時代と同様にEJBで作るのもありだと思います。

@Stateless
public class EchoService {
  public String echo(String name) {
    return "Hello " + name;
  } 
}

個人的には@ApplicationScopedはアプリケーション全体の状態を持たせる場合に使いたいため、案2の@Stereotypeを使う案が好きです。シンプルに使う場合は案3の@ApplicationScopedがおすすめです。

何故この変更が盛り込まれたのか

2013年のGlassFish Advant Calanderで書いた記事に詳細をまとめていますが、GuavaなどのCDI1.1が存在する前からあったライブラリが、CDI1.1に準拠したAPサーバ上で動かなくなったことが@Singletonをスキャン対象から外した要因です。

スキャン対象の拡大

@Singletonがスキャン対象から外れた修正以外として、CDI1.2では新たなスキャン対象の追加も行われています。

ステレオタイプについては前述の通りです。特に@Interceptorが地味に有効で、インターセプタ実装クラスにスコープアノテーションを付ける必要がなくなりました。

@InterceptorBinding
@Target({TYPE,METHOD})
@Retention(RUNTIME)
public @interface Logging {   
}

// @Dependent <<== CDI1.2からはスコープがなくてもスキャンされる
@Interceptor
@Logging
@Priority(APPLICATION + 10)
public class LoggingInterceptor {
    @AroundInvoke
    public Object log(InvocationContext ic) throws Exception {
        System.out.println("logging ...");
        return ic.proceed();
    }
}

@Service
public class EchoService {
  @Logging
  public String echo(String name) {
    return "Hello " + name;
  } 
}

インターセプタのスコープは?と言われても違和感があったので、嬉しい修正です。

CDI1.2 APサーバの準拠状況

OSS製品では、JavaEE7準拠の代表的なサーバは既にCDI1.2に対応しています。

  • GlassFish4.1 (2014/9/9 リリース)
  • WildFly8.2.0 Final (2014/11/20 リリース)

WebLogicやJBossEAP等の商用サーバは、まだJavaEE7準拠版がリリースされていないため何とも言えませんが、恐らくリリース時にはCDI1.2に対応させてくると思います。

まとめ

  • CDI1.2 マイナーバージョンアップが2014/4に行われた
    • beans.xmlがない場合のスキャン対象Beanが変更
      • @javax.inject.Singletonはスキャン対象から除外された
      • @Interceptor/@Decorator/@Stereotypeがスキャン対象に追加された
  • この修正はJavaEE7準拠サーバにも反映されている
    • GlassFish4.1、WildFly8.2.0はCDI1.2に対応済み

-XX:+DisableExplicitGCに関するJDK7とJDK8の違い

この記事は JVM Advent Calendar 2014の12/5分の記事*1です。昨日は
@jyukutyoさんのJITWatchでJITコンパイルを見よう!でした。


HotSpotには-XX:+DisableExplicitGCというオプションがありますが、この挙動がJDK8のリリースから変わっていたのでまとめます。

-XX:+DisableExplicitGC とは

System.gc()GCリクエストされても無視するオプションです。アプリケーション中でSystem.gc()を実行されるとメジャーGCが発生します。HotSpotに任せた場合はマイナーGCで十分回収できたかもしれないので、アプリケーションでSystem.gc()を呼び出すのはあまり好ましいことではありません。

無視するといっても内部的にそんな難しいことは行われておらず、OpenJDK8のソースを見るとif文でオプションが有効だったらGCしないだけです。

jdk8u/hotspot/src/share/vm/prims/jvm.cpp

451 JVM_ENTRY_NO_ENV(void, JVM_GC(void))
452   JVMWrapper("JVM_GC");
453   if (!DisableExplicitGC) {
454     Universe::heap()->collect(GCCause::_java_lang_system_gc);
455   }
456 JVM_END

JDK8よりjcmd <pid> GC.runに対しても有効になる

JDK7までは -XX:+DisableExplicitGC が有効化されていても、jcmd GC.runでGCリクエストが受け付けられていたのですが、JDK8より以下のメッセージ『Explicit GC is disabled, no GC has been performed.』が返ってきてGCは動きません。

$ jcmd 18952 GC.run
18952:
Explicit GC is disabled, no GC has been performed.

ソースを見ると、JDK8よりDisableExplicitGCフラグのチェックがjcmd <pid> GC.runの実装に追加されています。

jdk8u/hotspot/src/share/vm/services/diagnosticCommand.cpp

258 void SystemGCDCmd::execute(DCmdSource source, TRAPS) {
259   if (!DisableExplicitGC) {
260     Universe::heap()->collect(GCCause::_java_lang_system_gc);
261   } else {
262     output()->print_cr("Explicit GC is disabled, no GC has been performed.");
263   }
264 }
この修正による影響を受けるシステム

例えば、以下のようなシステムで影響を受けると思います。

  • 意図的にcronなどで定期的にjcmd <pid> GC.runを実行しているシステム
  • 連続運転テストをしてもメジャーGCが発生しないので、テスト終了時にjcmd <pid> GC.runを実行してJavaヒープにリーク傾向がないか考察しているシステム
回避策

JDK8において-XX:+DisableExplicitGC有効時にコマンドラインGCリクエストしたい場合は、jcmd <pid> GC.class_histogramによるヒストグラム取得の副作用を利用してGCを発生させることができます。

この場合、GCログのGC Causeに以下のように表示されます。

[Full GC (Heap Inspection Initiated GC)  39999K->37578K(250368K), 0.5085624 secs]

まとめ

  • JDK8より起動オプション-XX:+DisableExplicitGC有効時にはjcmd <pid> GC.runは無視されます
  • jcmd <pid> GC.class_histogramによってメジャーGCのリクエストをすることは可能です

*1:ええ、申し込んだのは2015/1/7ですがせっかく空いてたので