最近、ちょっと気になっているネタです。
CDIでコンストラクタインジェクションがしたい、という話でちょっといろいろ試してみました。
とりあえず、動作確認はJava SE、Weld SEで行うものとします。
Maven設定
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.littlewings</groupId> <artifactId>cdi-constructor-injection</artifactId> <packaging>jar</packaging> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.jboss.weld.se</groupId> <artifactId>weld-se</artifactId> <version>2.2.10.SP1</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.0.0</version> <scope>test</scope> </dependency> </dependencies> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> </project>
Weld SEが入っていれば、とりあえずOK。あとは、テスト用です。
確認用のCDI管理Bean
続いて、動作確認用のCDI管理Beanを用意します。
中身は適当。Java 8のString系の追加機能を普通に使えば、こういう定義は不要に映ると思いますが、簡単な例が思い浮かばなかったので…。
ちなみに、普段Java EE系のコードは別の言語で書いているのですが、今回はそれだと主題に対してちょっと読むのが難しくなるので、今回はJavaにしました。
String#joinのラッパークラス。
src/main/java/org/littlewings/javaee7/cdi/StringJoinService.java
package org.littlewings.javaee7.cdi; import javax.enterprise.context.ApplicationScoped; @ApplicationScoped public class StringJoinService { public String join(String delimiter, String... tokens) { return String.join(delimiter, tokens); } }
そのラッパーを、さらに包むクラス。依存する管理Beanを、コンストラクタインジェクションします。
src/main/java/org/littlewings/javaee7/cdi/DecorationService.java
package org.littlewings.javaee7.cdi; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.context.Dependent; import javax.inject.Inject; @ApplicationScoped public class DecorationService { private StringJoinService joiner; @Inject public DecorationService(StringJoinService joiner) { this.joiner = joiner; } public String join(String prefix, String suffix, String... tokens) { return prefix + joiner.join(", ", tokens) + suffix; } }
src/main/resources/META-INF/beans.xml
テストコード
単純なテストコードを用意。
src/test/java/org/littlewings/javaee7/cdi/ConstructorInjectionTest.java
package org.littlewings.javaee7.cdi; import static org.assertj.core.api.Assertions.*; import javax.enterprise.inject.spi.CDI; import org.jboss.weld.environment.se.Weld; import org.junit.Test; public class ConstructorInjectionTest { @Test public void testCdiConstructorInjection() { Weld weld = new Weld(); weld.initialize(); DecorationService decorator = CDI.current().select(DecorationService.class).get(); assertThat(decorator.join("*** ", " ***", "Hello", "World")) .isEqualTo("*** Hello, World ***"); weld.shutdown(); } }
確認
では、実行してみます。
$ mvn test
すると、実行時エラーになります。
org.jboss.weld.exceptions.UnproxyableResolutionException: WELD-001435: Normal scoped bean class org.littlewings.javaee7.cdi.DecorationService is not proxyable because it has no no-args constructor - <unknown javax.enterprise.inject.spi.Bean instance>.
メッセージを見ると、「引数なしのコンストラクタがないので、プロキシインスタンスが作れないよ」と言っています。
Normal scoped bean class [クラス名] is not proxyable because it has no no-args constructor
今回のコンストラクタインジェクションを使用しているクラスには、引数ありのコンストラクタを定義したのでデフォルトコンストラクタはなくなっていますからね。
そうなのですか、では、先ほどのDecorationServiceに、引数なしのコンストラクタを追加してみましょう。
@Inject public DecorationService(StringJoinService joiner) { this.joiner = joiner; } public DecorationService() { }
テスト実行。
$ mvn test
すると、
Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.377 s [INFO] Finished at: 2015-04-12T22:07:33+09:00 [INFO] Final Memory: 16M/236M [INFO] ------------------------------------------------------------------------
テストが通りました。エラーになりませんでしたね、めでたしめでたし。
ちょっと待った
でも、ちょっと納得いかない気もします。そう思ってちょっと資料を眺めていたところ、以下のようなものを見つけました。
CDI Essential Recipes at Java Day Tokyo 2015
http://www.slideshare.net/OracleMiddleJP/cdi-essential-receipe-at-java-day-tokyo-2015/82
デフォルトコンストラクタなしで、コンストラクタインジェクションしているように見えますね?
違いは、スコープを決めているアノテーションだけですね…。
では、先ほどのクラスをこう修正してみましょう。
// スコープアノテーションを変更 // @ApplicationScoped @Dependent public class DecorationService { private StringJoinService joiner; @Inject public DecorationService(StringJoinService joiner) { this.joiner = joiner; } // デフォルトコンストラクタは削除 // public DecorationService() { }
スコープアノテーションを@ApplicationScopedから@Dependentに変更して、デフォルトコンストラクタは削除しました。
再度、テスト実行。
$ mvn test
テストにパスしました。
Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.510 s [INFO] Finished at: 2015-04-12T22:12:33+09:00 [INFO] Final Memory: 16M/206M [INFO] ------------------------------------------------------------------------
@Dependentアノテーションって、何なのでしょうかね?
こうなると、ちょっと@Dependentアノテーションの存在が気になってきます。
だいたい説明が書かれているところを見ると、
- CDI 1.0でのデフォルトスコープ
- 擬似スコープと呼ばれる
- インジェクション先のライフサイクルに準ずる
あたりを見ることになります。
が、個人的にはこれだけだとちょっと正体不明です。少し、JSRを見てみます。
JSR 346: Contexts and Dependency Injection for Java EE 1.1
https://jcp.org/en/jsr/detail?id=346
コンストラクタ引数を持つ管理Beanは、以下の扱いになるみたいです。
3.15. Unproxyable bean types
The container uses proxies to provide certain functionality. Certain legal bean types cannot be
proxied by the container:classes which don’t have a non-private constructor with no parameters
〜以降、省略〜
つまり、クライアントプロキシが作れないタイプになりますよっと。
※とか言いながら、インターセプターを使ったりすると…
(追加)@Typedアノテーションを使う
うらがみさんに教えていただいた解法です。@Typedアノテーションを使えばよい、ということだそうですが…。
@kazuhira_r 関係ないですが、これ URL はインターフェース切ってクラスの方に[at]Typed(切ったインターフェース.class)すればクライアントプロキシが作られるのでコンストラクタインジェクションできると思いますよー
2015-04-12 23:05:52 via YoruFukurou to @kazuhira_r
@kazuhira_r デフォルトだと該当クラスと祖先となるクラスたち、実装しているインターフェースがすべてそのCDI管理ビーンのビーンタイプとして登録されるんですが、[at]Typedでビーンタイプを限定するという事ですねー。そうすればコンストラクタ引数を持つクラスでも
2015-04-12 23:13:20 via YoruFukurou to @kazuhira_r
@kazuhira_r (続き)クラスでも、ノーマルスコープに出来ます。ちなみに私、以前実務でドハマりしたところですwww
JSR-346では、「2.2.2. Restricting the bean types of a bean」を見ればよいみたいです。@Typedアノテーションに指定したインターフェースのClassの型で、管理Beanの型を絞るということみたいです。
というわけで、先ほどまでの定義はバッサリコメントアウト。
/* // スコープアノテーションを変更 // @ApplicationScoped @Dependent public class DecorationService { private StringJoinService joiner; @Inject public DecorationService(StringJoinService joiner) { this.joiner = joiner; } // デフォルトコンストラクタは削除 // public DecorationService() { } public String join(String prefix, String suffix, String... tokens) { return prefix + joiner.join(", ", tokens) + suffix; } } */
そして、インターフェースと実装を分離します。
public interface DecorationService { String join(String prefix, String suffix, String... tokens); } @Typed(DecorationService.class) @ApplicationScoped class DecorationServiceImpl implements DecorationService { private StringJoinService joiner; @Inject public DecorationServiceImpl(StringJoinService joiner) { this.joiner = joiner; } @Override public String join(String prefix, String suffix, String... tokens) { return prefix + joiner.join(", ", tokens) + suffix; } }
この時、実装クラスに@Typedアノテーションと、その引数にインターフェースのClassクラスを指定します。
実装体の定義をパッケージプライベートな形にしているのは、ご愛嬌…。
ちなみに、import文はこちら。
import javax.enterprise.inject.Typed;
これで、引数ありのコンストラクタであり、@ApplicationScopedのようなNormal Scopeのものでも大丈夫になります。
ご指摘いただいたうらがみさん、ありがとうございました!
で…
今回はとりあえずここまでにしましたが、ちゃんとJSRを読んだ方がよさそうですね。続きは、また今度。
今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/javaee7-scala-examples/tree/master/cdi-constructor-injection