AfterBeanDiscoveryイベント:Beanの登録

今回はBeanの登録処理について書きます。

Beanとは

BeanはCDI仕様の中で中心的なコンセプトです。Beanのインスタンスはコンテキスト上でライフサイクルが管理され、インジェクションがサポートされます。普通のクラスもBeanとして扱われますので、Managed BeanやEJBを含むほとんどすべてのクラスはBeanとして扱われます。

CDIコンテナ内部ではBean<T>というインタフェースによってBeanを管理します。Beanインタフェースは図1のような階層から構成されます。BeanのサブインタフェースとしてInterceptor<T>やDecorator<T>もありますので、それらもBeanとして扱われます。Beanインタフェースの詳細については、Javadocを参照してください。

図1. Bean<T>の階層
Beanの登録

CDI拡張モジュールとしてBeanを提供するのは簡単です。単にMETA-INF/beans.xmlを含むJARを作ればよいからです。しかし、CDIに関係ない既存のJARに含まれるクラスをCDIのBeanとしてコンテナに登録する必要がある場合にはちょっと工夫が必要になります。

CDI拡張モジュールSPIでは、BeforeBeanDiscoveryイベントとAfterBeanDiscoveryイベントの間にアプリケーションアーカイブに含まれるBeanのスキャン(Bean Discovery Process)が行われます。BeforeBeanDiscoveryイベントのハンドラでは、スキャンの前に実装されるべき処理を、AfterBeanDiscoveryイベントのハンドラでは、スキャンの完了後に実装されるべき処理を記述します。

アプリケーションアーカイブに含まれないようなクラスをBeanとして登録するには、AfterBeanDiscovery::addBean()メソッドを使います。このメソッドを使ってBeanインタフェースの無名クラスを定義し、そのインスタンスをAfterBeanDiscoveryイベントに追加します。

次のコードはMyBeanというクラスのインスタンスをBeanとしてCDIコンテナに登録するための例です。MyBean.classはCDI拡張モジュールとは別の既存のJARによって提供されることを想定しています。このJARにはMETA-INF/beans.xmlが含まれていないため、AfterBeanDiscovery::addBean()メソッドを使って拡張モジュール内で明示的に登録しています。

package org.tanoseam.examples;

import org.tanoseam.beans.MyBean;
import javax.annotation.*;
import javax.enterprise.context.*;
import javax.enterprise.event.Observes;
import javax.enterprise.inject.*;
import javax.enterprise.inject.spi.*;
// (略)

public class BeanRegistration implements Extension
{

public BeanRegistration() {
 log("BeanRegistration constructor");
 }

public void afterBeanDiscovery(@Observes AfterBeanDiscovery event,
 BeanManager bm) {
 log("BeanRegistration::AfterBeanDiscovery");

AnnotatedType<MyBean> at = bm.createAnnotatedType(MyBean.class);
 final InjectionTarget<MyBean> it = bm.createInjectionTarget(at);

Bean<MyBean> bean = new Bean<MyBean>() {
 @Override
 public Class<?> getBeanClass() {
 return MyBean.class;
 }

@Override
 public Set<InjectionPoint> getInjectionPoints() {
 return it.getInjectionPoints();
 }

@Override
 public String getName() {
 return "myBean";
 }

@Override
 public Set<Annotation> getQualifiers() {
 Set<Annotation> qualifiers = new HashSet<Annotation>();
 qualifiers.add( new AnnotationLiteral<Default>() {} );
 qualifiers.add( new AnnotationLiteral<Any>() {} );
 return qualifiers;
 }

@Override
 public Class<? extends Annotation> getScope() {
 return SessionScoped.class;
 }

@Override
 public Set<Class<? extends Annotation>> getStereotypes() {
 return Collections.emptySet();
 }

@Override
 public Set<Type> getTypes() {
 Set<Type> types = new HashSet<Type>();
 types.add(MyBean.class);
 types.add(Object.class);
 return types;
 }

@Override
 public boolean isAlternative() {
 return false;
 }

@Override
 public boolean isNullable() {
 return false;
 }

@Override
 public MyBean create(CreationalContext<MyBean> ctx) {
 log("Bean<MyBean>::create");
 MyBean instance = it.produce(ctx);
 it.inject(instance, ctx);
 it.postConstruct(instance);
 return instance;
 }

@Override
 public void destroy(MyBean instance,
 CreationalContext<MyBean> ctx) {
 log("Bean<MyBean>::destroy");
 it.preDestroy(instance);
 it.dispose(instance);
 ctx.release();
 }
 };

event.addBean(bean);
 }

void log(String messages) {
 System.out.println("CDI: " + messages);
 }
}

登録対象となるクラスMyBeanの定義は次のようになります。

package org.tanoseam.beans;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class MyBean {

public MyBean() {
 log("MyBean::constructor");
 }

@PostConstruct
 public void initialize() {
 log("MyBean::initialize");
 }

@PreDestroy
 public void destroy() {
 log("MyBean::destroy");
 }

public String toString() {
 return "MyBean String";
 }

void log(String messages) {
 System.out.println("CDI: " + messages);
 }

}

このMyBeanが実際にインジェクトできることを確認するため、weldのexamples/jsf/loginプロジェクトのクラスManagedBeanUserManager内でMyBeanのインジェクションを宣言し、getUsers()メソッドの先頭でbeanのtoString()を呼び出すようにしています(インジェクションだけではproxyが作成されるだけなので、MyBean::toString()を呼び出しています)。

@Inject
 private MyBean bean;

// (略)
 public List<User> getUsers() throws Exception
 {

System.out.println("bean=" + bean);

weld-login.warのWEB-INF/ilbには以下のJARを含めるようにします。

  • tanoseam-beans-0.1.jar: このJARはMyBeanを提供します。
  • tanoseam-examples-0.1.jar: このJARはBeanRegistrationを提供します。

以上にように修正したweld-login.warをJBoss AS7にデプロイし、http://localhost:8080/weld-login/を開くと次のような画面が表示されます。

この画面のView usersのリンクを実行すると次のようなログが出力されます。

22:07:57,466 INFO  [stdout] (MSC service thread 1-4) CDI: BeanRegistration::AfterBeanDiscovery
...
22:08:07,646 INFO  [stdout] (http--127.0.0.1-8080-1) CDI: MyBean::constructor
22:08:07,666 INFO  [stdout] (http--127.0.0.1-8080-1) CDI: Bean<MyBean>::create
22:08:07,666 INFO  [stdout] (http--127.0.0.1-8080-1) CDI: MyBean::constructor
22:08:07,667 INFO  [stdout] (http--127.0.0.1-8080-1) CDI: MyBean::initialize
22:08:07,667 INFO  [stdout] (http--127.0.0.1-8080-1) bean=MyBean String
23:23:04,993 INFO  [stdout] (ContainerBackgroundProcessor[StandardEngine[jboss.web]]) CDI: BeanBean<MyBean>::destroy
23:23:04,994 INFO  [stdout] (ContainerBackgroundProcessor[StandardEngine[jboss.web]]) CDI: MyBean::destroy

それではソースコードと比較しながらログの内容を確認しましょう。まず、Beanのcreate()とdestroy()の呼び出しのタイミングを確認してみてください。さらに、MyBean::constructorが2回呼び出されているのも確認できるかと思います。これはこのFAQに書かれているようにCDIコンテナがBeanのインジェクションにproxyを使っていることが原因です。このことからもわかるようにBeanのコンストラクタでは初期化処理を記述しないようにしてください。

広告

CDI拡張モジュール用のMaven設定

これからCDI拡張モジュールのサンプルを作っていきますが、モジュールのビルドはMavenを使うことにしましょう。

MavenのバージョンはMaven 3にしてください。Mavenの環境設定(settings.xml)についてはSeamFramework.orgのこのページを参考にしてください。

サンプルCDI拡張モジュールのソースプロジェクトの構造は次のようになります。プロジェクトの雛形を作るにはEclipseやIntelliJ IDEAなどのIDEを使うと簡単です。

.
|-- pom.xml
|-- src
|   |-- main
|   |   |-- java
|   |   |   `-- org
|   |   |       `-- tanoseam
|   |   |           `-- examples
|   |   |               `-- ContainerLifecycleEvents.java
|   |   `-- resources
|   |       `-- META-INF
|   |           `-- services
|   |               `-- javax.enterprise.inject.spi.Extension
|   `-- test
|       `-- java
`-- README.txt

次の設定は拡張モジュールのpom.xmlになります。できるだけシンプルにするために依存ライブラリとしては、最初はcd-apiしか使いません。でも、サンプルの作成に慣れてきたら、徐々にSeam 3のモジュールも使いましょう。

<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemalocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelversion>4.0.0</modelversion>

    <groupid>org.tanoseam.examples</groupid>
    <artifactid>tanoseam-examples</artifactid>
    <version>0.1</version>

    <dependencies>
        <dependency>
            <groupid>javax.enterprise</groupid>
            <artifactid>cdi-api</artifactid>
	        <version>1.0-SP4</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

</project>

こうして作成した拡張モジュールはアプリケーションのpom.xmlの依存ライブラリとして以下を指定することで簡単に取り込むことができます。

      <dependency>
         <groupId>org.tanoseam.examples</groupId>
         <artifactId>tanoseam-examples</artifactId>
         <version>0.1</version>
      </dependency>

CDIコンテナイベントの分類

CDIコンテナのライフサイクルイベントを表1にまとめてみました。

私はこれらのイベントはフェーズや用途によって以下のグループに分けられると思います(今後、CDIを使いこなしていくうちにもっと用途を細かく追記できるでしょうが、まずはここからスタートします)。

A) コンテナのライフサイクルフェーズごとに送信されるもの

A-1) コンテナへの各種情報の登録

  • BeforeBeanDiscovery
  • AfterBeanDiscovery

A-2) 拡張モジュールの初期化と終了処理

  • AfterDeploymentValidation
  • BeforeShutdown

B) コンテナがアーカイブ内のBeanをスキャン中に送信されるもの

B-1) コンテナが管理するメタモデルを置き換え/上書きする

  • ProcessAnnotatedType
  • ProcessInjectionTarget

B-2) スキャン中にBeanの情報を収集する

  • ProcessBean
  • ProcessObserverMethod
  • ProcessProducer

AグループはJSFのリクエストライフサイクルとも似ていて、コンテナからのコールバックと考えると直感的にはわかりやすいと思います。A-1では、Beanに指定するアノテーションを追加したり(BeforeBeanDiscovery)、アーカイブ外部のクラスをCDIのBeanとして登録したり、新規コンテキストを登録する(AfterBeanDiscovery)ことが可能です。

BグループはコンテナがBeanをスキャンするときに送信されるものです。B-1はコンテナの挙動をカスタマイズするためにメタモデルを再定義するためのものです。B-2はBeanのスキャンごとにBeanの種類毎に送信されるイベントで、このイベントを受信することでBeanの形式をチェックしてデプロイエラーにしたり、Beanの情報を収集したりできます。

次回以降は個々のCDIイベントを使いながら用途や応用を考えていきます。

表1:CDI 1.0 ライフサイクルイベント
イベント 説明
BeforeBeanDiscovery このイベントタイプはBeanのディスカバリプロセスが開始する前にコンテナによってスローされます。
AfterBeanDiscovery このイベントタイプはBeanのディスカバリプロセスが完了し、発見されたBeanに関連した定義エラーが一つも無く、発見されたBeanのためのBeanとObserverMethodオブジェクトが登録されたときにコンテナによってスローされます。そして、その後、コンテナはデプロイの問題を検出します。
AfterDeploymentValidation このイベントタイプはデプロイの問題が一つもないことが検証された後にコンテナによってスローされます。その後、コンテキストを作成したり、リクエストの処理に入ります。
BeforeShutdown このイベントタイプはリクエストの処理を終了し、すべてのコンテキストを破壊した後に発火します。
ProcessAnnotatedType コンテナはBeanアーカイブ内で発見したJavaクラスまたはインタフェースのためにこのタイプのイベントを発火し、その後、宣言されたアノテーションを読みます。
ProcessInjectionTarget コンテナは、コンテナによって実行時に生成可能なインジェクションをサポートする各Java EEコンポーネントクラスのためにこのタイプのイベントを発火します。javax.annotation.ManagedBeanを使って定義された管理Bean、EJBセッション、メッセージ駆動Bean、使用可能なBean、使用可能なインターセプタ、使用可能なデコレーターを含みます。
ProcessBean コンテナはBeanアーカイブ内にデプロイされた使用可能なBean、インターセプター、デコレーターのためにこのタイプのイベントを発火します。使用可能な管理Beanに対してのみ発火するProcessManagedBean、使用可能なセッションBeanに対してのみ発火するProcessSessionBeanもあります。
ProcessObserverMethod コンテナは使用可能なBeanのオブザーバーメソッドのためにこのタイプのイベントを発火し、その後、そのオブザーバーメソッドオブジェクトを登録します。
ProcessProducer コンテナは、リソースを含むそれぞれの使用可能なBeanのプロデューサーメソッドまたはメソッドのためにこのタイプのメソッドを発火します。使用可能なプロデューサーメソッドに対してのみ発火するProcessProducerMethodや使用可能なプロデューサフィールドに対してのみ発火するProcessProducerFieldがあります。

コンテナのライフサイクルイベント

前回紹介したExtentionの定義方法では、肝心の拡張モジュールの中身は空でした。今回は、拡張モジュール内でCDIコンテナとやりとりをする方法について書きます。

CDI JavaDocのjavax.enterprise.inject.spiパッケージを見ると、CDIコンテナのライフサイクルイベントのインタフェースが定義されています。CDIのコンテナはアプリケーションの初期化時に内部的に各種イベントを発行します。CDI拡張モジュールはこれらのイベントをコンテナから受信することによって拡張モジュール内部で処理を記述します。

CDIではオブジェクト間でイベントの送受信をすることができます。イベントの送信側はEventオブジェクトのfire()メソッドを呼び出します。イベント受信側はイベントを受信するメソッドのメソッドパラメーターに@Observesアノテーションをつけることによって指定した型のイベントを受信することができます(CDIイベントについてはWeldドキュメントのこのあたりを参考にしてください)。

さて、前置きはこのくらいにして本題に戻ります。次のクラスはContainerLifecycleEventsはCDIコンテナの発行するイベントを受信して標準出力にログを書くだけの単純な拡張モジュールです(代表的なイベントだけ書いています)。

package org.tanoseam.examples;

import javax.enterprise.event.Observes;
import javax.enterprise.inject.spi.*;

public class ContainerLifecycleEvents implements Extension
{
 public ContainerLifecycleEvents() {
 log("ContainerLifecycleEvents constructor");
 }

public void beforeBeanDiscovery(@Observes BeforeBeanDiscovery event) {
 log("BeforeBeanDiscovery");
 }

public void afterBeanDiscovery(@Observes AfterBeanDiscovery event,
 BeanManager bm) {
 log("AfterBeanDiscovery");
 }

public void afterDeploymentValidation(@Observes AfterDeploymentValidation event,
 BeanManager bm) {
 log("AfterDeploymentValidation");
 }

public void beforeShutdown(@Observes BeforeShutdown event,
 BeanManager bm) {
 log("BeforeShutdown");
 }

public <t> void processAnnotatedType(@Observes ProcessAnnotatedType<t> event) {
 log("ProcessAnnotatedType=" + event);
 }

public <t> void processInjectionTarget(@Observes ProcessInjectionTarget<t> event) {
 final AnnotatedType at = event.getAnnotatedType();
 log("InjectionTarget=" + at);
 }

void log(String messages) {
 System.out.println("CDI: " + messages);
 }
}

これを見ると各メソッドに@Observesがついていて、パラメータの型の名前がCDIコンテナのライフサイクルイベントであることが確認できます。例えば、最初のメソッドはBeforeBeanDiscoveryというイベントを受信します。この拡張モジュールをWeld examplesのjsf/loginに組み込んだ場合の標準出力は次のようになります。

22:42:15,150 INFO [org.jboss.weld] (MSC service thread 1-4) Processing CDI deployment: weld-login.war 
22:42:15,181 INFO [stdout] (MSC service thread 1-4) CDI: ContainerLifecycleEvents constructor 
22:42:15,665 INFO [org.jboss.weld] (MSC service thread 1-3) Starting Services for CDI deployment: weld-login.war 22:42:16,498 INFO [stdout] (MSC service thread 1-3) CDI: BeforeBeanDiscovery 
22:42:16,550 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public@Entity class org.jboss.weld.examples.login.User 
22:42:16,567 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public@Named @Default @RequestScoped class org.jboss.weld.examples.login.Credentials 
22:42:16,569 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public abstract interface class org.jboss.weld.examples.login.UserManager 
22:42:16,592 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public class org.jboss.weld.examples.login.Resources 
22:42:16,610 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public@Named @Alternative @Stateful @RequestScoped class org.jboss.weld.examples.login.EJBUserManager 
22:42:16,627 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public@Named @RequestScoped class org.jboss.weld.examples.login.ManagedBeanUserManager 
22:42:16,649 INFO [stdout] (MSC service thread 1-3) CDI: ProcessAnnotatedType=public@SessionScoped @Named class org.jboss.weld.examples.login.Login 
22:42:17,185 INFO [stdout] (MSC service thread 1-3) CDI: InjectionTarget=public class org.jboss.weld.examples.login.Resources 
22:42:17,205 INFO [stdout] (MSC service thread 1-3) CDI: InjectionTarget=public@SessionScoped @Named class org.jboss.weld.examples.login.Login 
22:42:17,217 INFO [stdout] (MSC service thread 1-3) CDI: InjectionTarget=public@Named @Default @RequestScoped class org.jboss.weld.examples.login.Credentials 
22:42:17,297 INFO [stdout] (MSC service thread 1-3) CDI: InjectionTarget=public@Named @RequestScoped class org.jboss.weld.examples.login.ManagedBeanUserManager 
22:42:17,320 INFO [stdout] (MSC service thread 1-3) CDI: InjectionTarget=public@Entity class org.jboss.weld.examples.login.User 
22:42:17,356 INFO [stdout] (MSC service thread 1-3) CDI: AfterBeanDiscovery 
22:42:17,439 INFO [stdout] (MSC service thread 1-3) CDI: AfterDeploymentValidation

この出力結果を見ると、

  1. 拡張モジュールのコンストラクタ
  2. BeforeBeanDiscoveryイベント
  3. ProcessAnnotatedTypeイベントの繰り返し
  4. ProcessInjectionTargetイベントの繰り返し
  5. AfterBeanDiscoveryイベント
  6. AfterDeploymentValidationイベント
の順でCDIコンテナの処理が進むことがわかります。ステップ6の後にアプリケーションの処理が開始されます。ログ上では拡張モジュールのコンストラクタは1度しか呼ばれておらず、CDIコンテナは拡張モジュールのインスタンスを維持しているだろうということが読み取れます。
拡張モジュールの処理自身は受信したイベントを使って記述します。次回は各イベントの概要について書くつもりです。

CDI拡張モジュールの構造

今回はCDI拡張モジュールのアーカイブの作り方について書きます。

CDI拡張モジュールを定義するには次のようにします。

  • インタフェースjavax.enterprise.inject.spi.Extensionを実装した拡張モジュールのクラスを定義します
  • この拡張モジュールクラスの完全修飾名をMETA-INF/services/javax.enterprise.inject.spi.Extensionというテキストファイルに書きます

例えば、org.tanoseam.examples.MyExtensionという拡張モジュールのクラスを定義したとします。その場合、次のようにMyExtensionはjavax.enterprise.inject.spi.Extensionを実装する必要があります。

package org.tanoseam.examples;

import javax.enterprise.inject.spi.Extension;

public class MyExtension implements Extension
{
 public MyExtension() {
 System.out.println("**** MyExtension constructor");
 }
...
}

拡張モジュールはアプリケーションのアーカイブファイル内にアプリケーションコードと一緒に含めることも可能です。しかし、これから作るCDI拡張モジュールは任意のアプリケーションアーカイブに簡単に取りこめるようにjarとして作ることにします。

MyExtensionを含むjarファイルの構造は次のようになります。META-INF/services/javax.enterprise.inject.spi.Extensionのファイルの内容としてorg.tanoseam.examples.MyExtensionと書いておきます。

|-- META-INF
|   |-- MANIFEST.MF
|   `-- services
|       `-- javax.enterprise.inject.spi.Extension
`-- org
    `-- tanoseam
        `-- examples
            `-- MyExtension.class

この拡張モジュールのjarはCDIアプリケーションのアーカイブファイルのクラスパスに含めます。仮にCDIアプリケーションがwarファイルであればWEB-INF/libの下に拡張モジュールのjarファイルを配置します。以上で、warファイルをアプリサーバーにデプロイすると拡張モジュールがCDIコンテナによって認識されるようになります。

CDI拡張モジュールを作ろう

これからしばらくの間、CDI (Contexts and Dependency Injection)仕様の Portable Extensionsをテーマに書いていこうと思います。

Portable Extensions SPIを使うことによって、コンテナに新規機能を作り込んだり、外部モジュールとJavaEEを透過的に連携させることが可能になります。実際、ResinのEJB 3.1実装はCDIの上に作られているようです(このあたりの解説はTSSの記事にわかりやすくまとめられています)。

Portable Extensions を調べるための資料としては以下があります。

CDIのJavaDocとWeldドキュメントがあれば概要は理解できると思います。仕様の細部について確認するときにはCDI仕様を参照することも必要になるかもしれません。残念ながら、現時点では、日本語の資料はありません(だからこそ、このブログを書いているのです!)。

Portable Extensionsを使った拡張モジュールの作成方法を理解するには、すでに作成済みのモジュールのソースコードを参照するのが良いでしょう。WeldのexamplesにはPortable Extensionsのサンプルは含まれていませんが、きっとSeam 3 モジュールのソースが参考になるでしょう。

このSPIは拡張モジュールとCDIコンテナ間のインタフェースを定めたもので、CDIコンテナはBeanを管理するための各種メタモデルを定義しています。このメタモデルの意味がわからないと拡張モジュールを作るのは難しそうです。まずは、SPIが提供するメタモデルとその振る舞いについて基本を確認する必要がありそうです

Weld 1.1.2 サンプル

Weld 1.1.2.FinalのexamplesをJBoss AS7上で動かしてみました。AS7にはWeb ProfileとEverythingの2種類のディストリビューションがありますが、使うのはフルセットの方です。動かしてみたのはexamples/jsf以下のサンプル5つ。

  • `jsf/numberguess` (a simple war example for JSF)
  • `jsf/login` (a simple war example for JSF)
  • `jsf/translator` (a simple EJB example for JSF)
  • `jsf/pastecode` (a more complex EJB example for JSF)
  • `jsf/permalink` (a more complex war example for JSF)
AS7へのデプロイは管理コンソールからが簡単。左のメニューでDeploymentsを選択し、右上のAdd Contentのボタンを押します。あとは、mavenでビルドした結果のwar/earをアップロードします。Deploymentのリストにデプロイしたモジュールが見えたらEnableボタンを押します。デプロイに成功すればEnabledの列に緑の丸が表示されます。
これらのサンプルはAS6で動くように設定されているので、AS7で動かすためには若干設定の変更が必要です。具体的にはlogin, pastecodeのpersistence.xmlにおいてデータソースのJNDI名をjava:jboss/datasources/ExampleDSに書き換えます。
<persistence-unit name="loginDatabase">
<provider>org.hibernate.ejb.HibernatePersistence</provider>
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source><properties>
   <property name="hibernate.hbm2ddl.auto" value="create-drop">
   <property name="hibernate.show_sql" value="false">
</properties>
</persistence-unit>
残念ながら現時点では動作しないものも2つあります。translatorはAS7での@EJBに関わるバグにより失敗します(https://issues.jboss.org/browse/AS7-1269, Stauts:Closed)。pastecodeはEJB TimerServiceがAS7上でまだ動作しないためにデプロイに失敗します(https://issues.jboss.org/browse/AS7-474, Status:AS7.1実装予定)。失敗の原因はわかっているので、それを踏まないようにすればよいでしょう。

※JBoss AS 7.0.1で修正済みです(2011/08/18追記)

%d人のブロガーが「いいね」をつけました。