Spring Cloud FeignクライアントのBean定義衝突:`FeignClientSpecification`登録エラーの解決

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();
}

上記の例では、ProductCatalogClientItemCatalogClientは異なるインターフェース名ですが、両方とも@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定義の衝突を根本的に解決し、アプリケーションの堅牢性を高めることができます。

タグ: SpringCloud Feign SpringBoot BeanDefinitionOverrideException Microservices

8月4日 20:54 投稿