概要
本来はJava EE環境(Java EE サーバ上)で動くCDIを、Java SE環境で動かしてみます。
CDIの参照実装であるWeldのSE版を使います。
今回作成したサンプルコードは、GitHubにアップしてあります。
https://github.com/sumeragizzz/weldse-examples
環境
Windows 10 Pro 64bit
Java SE 8 Update 102 64bit
Weld SE 2.4.0.Final
準備
Maven
ビルドツールにMavenを使用します。
pom.xmlに以下の依存情報を追加します。
<dependency>
<groupId>org.jboss.weld.se</groupId>
<artifactId>weld-se</artifactId>
<version>2.4.0.Final</version>
</dependency>
Mavenリポジトリにはweld-se-coreというのもありますが、こちらは自分で依存関係を解決する必要があるので、通常はweld-seでいいと思います。
beans.xml
Weld SEでCDIを有効にするにはbeans.xmlが必要なようです。
Java EE 7では無くても有効になっているので、ちょっと違いますね。
実際にbeans.xml無しで実行してみたところ、以下の例外が発生しました。
java.lang.IllegalStateException: WELD-ENV-000016: Missing beans.xml file in META-INF
なので、以下にbeans.xmlを配置します。
/[project-root]/src/main/resources/META-INF/beans.xml
ファイルの存在有無しかみていないらしく、0byteファイルでも有効化されました。
ですが、本来は妥当なXMLにしておくべきだと思います。
実装
インスタンス取得
まずはシンプルにコンテナからCDI Beanのインスタンスを取得してみます。
サンプルとして、文字列を返すだけのBeanを用意しました。
public class HelloWorldBean {
public String execute() {
return "Hello World";
}
}
WeldからWeldContainerを生成して、クラス指定で取得できます。
WeldContainerはAutoClosableを実装するので、try-with-resourcesが使えます。
Weld weld = new Weld();
try (WeldContainer container = weld.initialize()) {
HelloWorldBean bean = container.select(HelloWorldBean.class).get();
System.out.println(bean.execute());
}
コンテナがCDI Beanを生成、管理しているのが分かります。
Hello World
DI
CDI Beanに別のCDI BeanをInjectしてみます。
フィールドインジェクション
一番手っ取り早いフィールドインジェクションから。
@Inject
private HelloWorldBean helloWorldBean;
public String execute() {
return String.format("*** %s ***", helloWorldBean.execute());
}
取得するCDI Beanのみ変えて、先ほどと同じように実行します。
Weld weld = new Weld();
try (WeldContainer container = weld.initialize()) {
FieldInjectionBean bean = container.select(FieldInjectionBean.class).get();
System.out.println(bean.execute());
}
DIしたCDI Beanが返す文字列が含まれているので、DIできていることが分かります。
*** Hello World ***
コンストラクタインジェクション
次はコンストラクタ経由でDIします。
イミュータブルなクラス設計にできたりするメリットがありますね。
private HelloWorldBean helloWorldBean;
@Inject
public ConstructorInjectionBean(HelloWorldBean helloWorldBean) {
this.helloWorldBean = helloWorldBean;
}
public String execute() {
return String.format("\"%s\"", helloWorldBean.execute());
}
実行方法は先ほどと同様。
期待通りにDIできています。
"Hello World"
セッターインジェクション
あまり存在意義を感じないので割愛します。
複数コンテナ
複数のコンテナを生成して使い分けることができます。
下記サンプルでは、IDを振り、自動検出を無効化、指定したクラスが属するパッケージを管理対象として、コンテナを生成しています。
Weld#shutdown()を呼ぶと、そのWeldインスタンスから生成した全てのWeldContainerをclose()してくれます。
AutoClosableではないので、finallyで呼び出すようにしました。
Weld weld = new Weld();
try {
WeldContainer container1 = weld.containerId("1").disableDiscovery().packages(HelloWorldBean.class).initialize();
WeldContainer container2 = weld.containerId("2").disableDiscovery().packages(OtherPackageBean.class).initialize();
System.out.println(container1.select(HelloWorldBean.class).get().execute());
System.out.println(container1.select(ConstructorInjectionBean.class).get().execute());
System.out.println(container2.select(OtherPackageBean.class).get().execute());
// UnsatisfiedResolutionException発生
// System.out.println(container1.select(OtherPackageBean.class).get().execute());
// System.out.println(container2.select(HelloWorldBean.class).get().execute());
} finally {
weld.shutdown();
}
Hello World
"Hello World"
Other Package
管理対象ではないCDI Beanを取得しようとすると、実行時にUnsatisfiedResolutionExceptionが発生します。
コメントアウトしているコードで例外が発生するので、コンテナごとに管理対象が異なることが分かります。
インターセプター
まず、インターセプターを実装します。
実行時間を計測して、標準出力するようにしてみました。
@Interceptor
@ElapsedTimeBinding
public class ElapsedTimeInterceptor {
@AroundInvoke
public Object invoke(InvocationContext context) throws Exception {
long start = System.nanoTime();
Object result = context.proceed();
long end = System.nanoTime();
System.out.format("%,dns\r\n", end - start);
return result;
}
}
CDI Beanにインターセプターを適用する為の@InterceptorBindingなアノテーションを作ります。
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface ElapsedTimeBinding {
}
これをCDI Beanに付与します。
一瞬で終わってしまうと計測時間の結果がイマイチなので、1秒ほどスリープさせてみました。
@ElapsedTimeBinding
public class InterceptedBean {
public String execute() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getClass().getName();
}
}
これで実行しましたが、インターセプターが動きません・・・
beans.xmlにインターセプターを定義しておく必要がある、という情報が参考サイトにありました。
0byteファイルにしてましたが、ちゃんと定義するようにしました。
<?xml version="1.0" encoding="UTF-8"?>
<beans
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/beans_1_1.xsd"
bean-discovery-mode="all">
<interceptors>
<class>examples.weldse.interceptor.ElapsedTimeInterceptor</class>
</interceptors>
</beans>
これで実行したところ、期待通りにインターセプターが動作しました。
なにげにCDI BeanのインスタンスがProxyになっているのが見えますね。
1,000,119,111ns
examples.weldse.bean.InterceptedBean$Proxy$_$$_WeldSubclass
@InterceptorBindingを使う場合は上記のようにbeans.xmlに定義が必要なようです。
@Interceptorsを使えば、beans.xmlの定義有無は関係なく動作したので、気軽に使うのであればこちらも選択肢ですね。
まとめ
これくらい手軽なら、ちょっとしたコンソールアプリケーションを作る際に採用してもいいと思います。
個人的には、インターセプターを使えるのが嬉しいです。
@Transactionalを使いたいところですが、JTAが無いので、このままではダメですね。
これがどうにかなるのかは、今後の課題としておきます。
参考
http://docs.jboss.org/weld/reference/latest/en-US/html_single/
http://d.hatena.ne.jp/Kazuhira/20140208/1391851068
http://qiita.com/opengl-8080/items/2f03ab496e871cf32f54
http://qiita.com/opengl-8080/items/431de9175dca33a09ba8
http://enterprisegeeks.hatenablog.com/entry/2015/12/07/075311