きり丸の技術日記

技術検証したり、資格等をここに残していきます。

Springで同一クラス別コンポーネントをDIしたい(nameGeneratorとFullyQualifiedAnnotationBeanNameGenerator)

Springで開発していると、別のコンポーネントだが、同一のクラス名を作りたくなることがあります。この同名クラスをDIしない場合は、特に問題は発生しません。しかし、同一クラス名をDIしてしまうと、BeanDifinitionStoreExceptionが発生してしまいます。

これは、クラスをパッケージプライベートにして可視性を狭めても、発生してしまいます。

クラス名を変更する、Beanの名前を変更する等々で簡単に回避することはできますが、クラス名が長くなったり、DIする際に変更した名前を指定する必要もあり、非常に面倒です。

それを簡単に解決できるのが、nameGeneratorFullyQualifiedAnnotationBeanNameGeneratorです。

当記事では、nameGeneratorを使用して別パッケージに配備した同一クラスの別コンポーネントを簡単にDIすることを目標とします。

環境

  • Java
    • 15
  • SpringBoot
    • 2.4.5

ゴール

  • 別パッケージに同一のクラス名を定義しても、エラーが発生しない

対応しない場合は次のエラーが発生するので、それを回避します。

org.springframework.beans.factory.BeanDefinitionStoreException: Failed to parse configuration class [kirimaru.DemoApplication]; nested exception is org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'conflictService' for bean class [kirimaru.biz.domain.conflict.update.ConflictService] conflicts with existing, non-compatible bean definition of same name and class [kirimaru.biz.domain.conflict.register.ConflictService]

...中略...

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException: Annotation-specified bean name 'conflictService' for bean class [kirimaru.biz.domain.conflict.update.ConflictService] conflicts with existing, non-compatible bean definition of same name and class [kirimaru.biz.domain.conflict.register.ConflictService]

要約

  • デフォルトのBean名を変更する
    • ComponentScanのnameGeneratorにFullyQualifiedAnnotationBeanNameGeneratorを指定する

パッケージ構成

registerとupdateパッケージの下に、Bean名が被るように同一クラス名のConflictService.javaが存在させます。

- Appllication.java
  - registerパッケージ
    - ConflictService.java
  - updateパッケージ
    - ConflictService.java

ConflictServiceの中身

DIができるように、Serviceアノテーションを付けただけのファイルを作成します。DI対象に含められれば良いので、Controller, Componentでも問題ありません。

@Service
public class ConflictService {
}

対応

SpringBootApplicationを指定しているクラスに、ComponentScanを指定します。

次に、ComponentScanのnameGeneratorにFullyQualifiedAnnotationBeanNameGenerator.classを指定します。

@SpringBootApplication
@ComponentScan(nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

説明

デフォルトのBean名はクラス名になります。今回の場合、ConflictServiceです。

今回のFullyQualifiedAnnotationBeanNameGeneratorを指定することで、生成されるBean名が完全修飾クラス名になります。今回の場合、register.ConflictServiceupdate.ConflictServiceの2つに分かれます。

Javaでは同一パッケージ、同一クラスは配備できないので、どのパターンでも対応できると思います。

なお、FullyQualifiedAnnotationBeanNameGeneratorはSpringFrameworkの5.2.3に導入されました。このリリースは2020年1月14日と最近のリリースです。もしSpringのアップデートできないが、同様の処理をしたいという場合にはこちらの方の記事を参考にしてください。

備考

もし、SpringBootApplicationにscanBasePackagesを指定している場合は、ComponentScanのbasePackagesにも同様に指定してください。

こちらを指定しないと、探索するパッケージが異なるために目的通りのDIができないことがあります。

@SpringBootApplication(scanBasePackages = {"kirimaru"})
@ComponentScan(basePackages = "kirimaru", nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }
}

テストする方法

※ ここからは一般論ではなく、私のローカル環境で発生したものです。職場で導入したら別のエラーで動きませんでした…。


FullyQualifiedAnnotationBeanNameGeneratorを使用し、Bean名が変更されたので、DIできるように設定する必要があります。

私の場合は、こちらのアノテーションを追加すると、読み込むことができるようになりました。

  • @AutoConfigureWebClient

WebMvcTestを使用していれば、次のアノテーションを含んでいるので意識して追加する必要は少ないと思います。

  • @AutoConfigureCache
  • @AutoConfigureWebMvc
  • @AutoConfigureMockMvc

意識的に読み込まないといけないので、読み込むアノテーションの数が増えたら、新しいアノテーションを作ってもいいかもしれません。


SpringJUnitConfigを使用している場合は、ComponentScanを併用していると思いますので、こちらにnameGeneratorを追加すると動くようになると思われます。

@ComponentScan(value = {"kirimaru.biz.domain"}, nameGenerator = FullyQualifiedAnnotationBeanNameGenerator.class)
static class Config {
}

ソースコード

実装コード https://github.com/hirotoKirimaru/cucumber-sample/blob/36d3cec61c97321dadb744ce70a15b5f35e67730/src/main/java/kirimaru/DemoApplication.java

終わりに

テストができなくて記事にできませんでしたが、teratailで回答いただけでようやくテストができるようになりました。WebMvcTestのアノテーションを使っていたので、十分インポートできていると思っていたのですが、まだ足りなかったとは…。

この辺の設定周りは苦手ですね。精進していきたいです。


この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。

経緯

FullyQualifiedAnnotationBeanNameGeneratorを知った経緯。いろふさんありがとうございました。

twitter.com

twitter.com

参考

【Java・Spring Boot】別パッケージにて、同じクラス名でDIしたいとき 【Java・Spring Boot】別パッケージにて、同じクラス名でDIしたいとき | 地方に引越し予定の30歳男がプログラマーを続けるためのブログ

teratail: デフォルト以外のBean名生成をしつつ、WebMvcTest等でDIを行うテストをしたい Spring - デフォルト以外のBean名生成をしつつ、WebMvcTest等でDIを行うテストをしたい|teratail

Spring JavaDoc: FullyQualifiedAnnotationBeanNameGenerator FullyQualifiedAnnotationBeanNameGenerator (Spring Framework 5.3.14 API)

ImportAutoConfigurationのパッケージ クラス org.springframework.boot.autoconfigure.ImportAutoConfiguration の使用 (Spring Boot 2.6.1 API) - Javadoc

自動構成アノテーションのテスト Spring Boot テスト自動構成アノテーション - リファレンス

f:id:nainaistar:20210504221725p:plain