Spring BootアプリケーションでSpring Cloud Feignを使用している際に、以下のようなエラーが発生することがあります。
Description:
The bean 'external-catalog-service.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
このエラーは、アプリケーションコンテキスト内で同じ名前を持つFeignClientSpecificationというBeanが複数登録されようとしたときに発生します。特にSpring Boot 2.1以降では、Bean定義の上書きがデフォルトで無効になっているため、このような衝突がBeanDefinitionOverrideExceptionとして顕在化します。
原因の分析
この問題の根源は、Spring Cloud Feignが@FeignClientアノテーションが付与されたインターフェースをスキャンし、各クライアントに対して内部的にFeignClientSpecificationというBeanを登録するメカニズムにあります。
@EnableFeignClientsアノテーションを使用すると、SpringはFeignClientsRegistrarクラスを通じてアプリケーション内の@FeignClientインターフェースを検索します。各@FeignClient定義が見つかると、FeignClientsRegistrarは対応するFeignClientSpecification Beanを生成し、SpringのBeanファクトリに登録します。この際、FeignClientSpecification Beanの名前は、@FeignClientアノテーションのname属性(または、name属性が指定されていない場合はインターフェース名から派生する名前)と.FeignClientSpecificationを結合した形式で構成されます。
例えば、@FeignClient(name = "my-service")と定義されたクライアントの場合、内部的にmy-service.FeignClientSpecificationという名前のBeanが登録されます。
もし、異なるパッケージやモジュールに存在する複数の@FeignClientインターフェースが、偶然にも同じname属性(または同じデフォルト名)を持つ場合、Springは同じ名前のFeignClientSpecification Beanを複数登録しようとします。これがデフォルトで無効になっているBean定義の上書き機能に抵触し、上記のエラーが発生するのです。
具体的な問題発生例
以下に示すように、異なる場所にある二つのFeignクライアントインターフェースが、同じname属性を指定している場合にこの問題が発生します。
// com.example.module1.clients.ProductCatalogClient.java
package com.example.module1.clients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "external-catalog-service") // <-- この名前が衝突の原因
public interface ProductCatalogClient {
@GetMapping("/products")
String retrieveProducts();
}
// com.example.module2.clients.ItemCatalogClient.java
package com.example.module2.clients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "external-catalog-service") // <-- こちらも同じ名前を使用
public interface ItemCatalogClient { // インターフェース名自体は異なる
@GetMapping("/items")
String fetchItems();
}
上記の例では、ProductCatalogClientとItemCatalogClientは異なるインターフェース名ですが、両方とも@FeignClient(name = "external-catalog-service")と指定しているため、内部でexternal-catalog-service.FeignClientSpecificationというBeanが二重に登録されようとし、エラーが発生します。
解決策
1. Bean定義の上書きを許可する (応急処置)
最も手軽な解決策は、Spring Bootアプリケーションの設定ファイル(例: application.propertiesまたはapplication.yml)でBean定義の上書きを許可することです。
spring.main.allow-bean-definition-overriding=true
この設定により、Springは同じ名前のBeanが既に存在する場合でも、新しい定義で上書きするようになります。ただし、これは潜在的な問題を一時的に回避するものであり、意図しないBeanの置き換えが発生するリスクも伴うため、慎重な検討が必要です。特に、異なるクライアントが異なる設定(configuration属性など)を持つことが期待される場合には、この方法は推奨されません。
2. Feignクライアントにユニークな名前を割り当てる (推奨されるベストプラクティス)
より堅牢で推奨される解決策は、各@FeignClientアノテーションのname属性にユニークな値を指定することです。これにより、FeignClientSpecification Beanの名前衝突を防ぐことができます。
// com.example.module1.clients.ProductCatalogClient.java
package com.example.module1.clients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "product-catalog-api") // <-- ユニークな名前に変更
public interface ProductCatalogClient {
@GetMapping("/products")
String retrieveProducts();
}
// com.example.module2.clients.ItemCatalogClient.java
package com.example.module2.clients;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
@FeignClient(name = "item-catalog-api") // <-- ユニークな名前に変更
public interface ItemCatalogClient {
@GetMapping("/items")
String fetchItems();
}
このように、各Feignクライアントに意味のあるユニークな名前を付与することで、Bean定義の衝突を根本的に解決し、アプリケーションの堅牢性を高めることができます。