CLOVER🍀

That was when it all began.

CDIでコンストラクタインジェクションしたい

最近、ちょっと気になっているネタです。

CDIでコンストラクタインジェクションがしたい、という話でちょっといろいろ試してみました。

とりあえず、動作確認はJava SE、Weld SEで行うものとします。

Maven設定

pom.xmlの設定は、こんな感じ。
pom.xml

<?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;
    }
}

CDI有効化のため、beans.xmlも用意します。

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アノテーションって、何なのでしょうかね?

こうなると、ちょっと@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アノテーションを使えばよい、ということだそうですが…。

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