Takuji->find;

株式会社はてなでアプリケーションエンジニアやってます、技術的な記事を書いているつもり

JSR330のQualifierを付与したAnnotation classを作る場合はちゃんとTargetを指定した方がよい

Kotlin+Dagger2で起きた問題、おそらくJavaではほとんど起こらないだろう問題ではあるが全く起きないわけではなさそう。

前提

Dagger2(に限らずJSR330に準拠したDIコンテナー)で目的の違う同じ型のインスタンスを注入したい場合には @Named を使うか、 @Qualifier アノテーションを付与したアノテーションを定義してやる必要がある。

Dagger2の場合、Moduleの @Provides を付与した依存提供用のメソッドに @Qualifier を付与したアノテーションを付与する

data class User(val name: String)

@Qualifier
annotation class PrimaryUser

@Qualifier
annotation class SecondaryUser

class Module {
  @Provides
  @PrimaryUser
  fun providePrimaryUser() : User = User("takuji31")

  @Provides
  @SecondaryUser
  fun provideSecondaryUser() : User = User("takuji24884")
}

この提供されている依存をpropertyに注入する時、次のように書きたくなるはずである。

class UserActivity : AppCompatActivity() {
  @Inject
  @PrimaryUser
  lateinit var primaryUser: User

  @Inject
  @SecondaryUser
  lateinit var secondaryUser: User
}

こうすることで、fieldがJavaからはpublicに見えるので(internalにしてpackage privateにした方がよいかもしれないが)、Dagger2のfield injectionが使えるはず。

ところがこれをビルドすると、どちらも依存が解決できないと言われてエラーになる。

何が起こっているか

Kotlinのpropertyは基本的に backing field + getter + setter が組み合わさってできている。

そこで、propertyにアノテーションを付与するとコンパイラーはどこに指定すればいいか分からず、getterにアノテーションを付与するのである。

getterの次にfield、最後にsetterになるようだ。

この結果はstub生成の様子を見ると分かる build/tmp/(kapt|kapt3)/stub)

今回はfield injectionを想定しているので、fieldに @PrimaryUser@SecondaryUser が付与されている必要があるのだが、これだとfieldではなくgetterにアノテーションが付与されてしまうので、依存が解決できずビルドエラーになるのだ。

回避策

もしこのアノテーションがライブラリーで用意されているものだとしたら、リポジトリに修正のPRを投げつつ以下のようにするとよいだろう。

class UserActivity : AppCompatActivity() {
  @field:[Inject PrimaryUser]
  lateinit var primaryUser: User

  @field:[Inject SecondaryUser]
  lateinit var secondaryUser: User
}

こうすることで明示的にfieldにアノテーションを付与できる。

根本的な解決

そもそもQualifierを付与したアノテーションを付与できる先を限定しておくべきである(意図しないところで使われる可能性がある)ので、そのために @Target を指定しておくべきである。

@Target(AnnotationTarget.FIELD, AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FUNCTION)
@Qualifier
annotation class PrimaryUser

ここで使っている @Target は Kotlin側で用意されているもので、Javaのものより柔軟にターゲットを指定できる。

この場合はフィールド、メソッドやコンストラクターの引数、関数(メソッド、setter、getter)そのものに指定できるようになる。

なお、AnnotationTargetの指定順に付与される模様?

最後に

今回はQualifierに限ったような書き方になったが、基本的にアノテーションのターゲットはちゃんと指定してやるべきだろう。