Springで開発していると、別のコンポーネントだが、同一のクラス名を作りたくなることがあります。この同名クラスをDIしない場合は、特に問題は発生しません。しかし、同一クラス名をDIしてしまうと、BeanDifinitionStoreException
が発生してしまいます。
これは、クラスをパッケージプライベートにして可視性を狭めても、発生してしまいます。
クラス名を変更する、Beanの名前を変更する等々で簡単に回避することはできますが、クラス名が長くなったり、DIする際に変更した名前を指定する必要もあり、非常に面倒です。
それを簡単に解決できるのが、nameGenerator
とFullyQualifiedAnnotationBeanNameGenerator
です。
当記事では、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.ConflictService
とupdate.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 { }
ソースコード
終わりに
テストができなくて記事にできませんでしたが、teratailで回答いただけでようやくテストができるようになりました。WebMvcTest
のアノテーションを使っていたので、十分インポートできていると思っていたのですが、まだ足りなかったとは…。
この辺の設定周りは苦手ですね。精進していきたいです。
この記事がお役に立ちましたら、各種SNSでのシェアや、今後も情報発信しますのでフォローよろしくお願いします。
経緯
FullyQualifiedAnnotationBeanNameGenerator
を知った経緯。いろふさんありがとうございました。
twitter.comDIの命名グルーピングできないものか…。
— きり丸 (@nainaistar) 2021年3月26日
クラス名としては同一がいいけど、APIごとに違うバリデーションかけたいから、中身は別物、みたいな。
Aテーブルfor A APIみたいなクソ長クラス名は作りたくないんだよな
twitter.comSpringのbean名がデフォルトsimpleNameになるやつです? https://t.co/66oWUEDIy9 とかで完全名にできたりは。
— いろふ (@irof) 2021年3月26日
参考
【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 テスト自動構成アノテーション - リファレンス