12.

Spring Bean 名衝突 (conflicts with) の原因と対処

編集
この記事の要点
  • Bean 名衝突エラーは、同名の Bean を Spring DI コンテナに複数登録しようとした際に発生
  • 主因: 同じ単純クラス名 (xxxController) で別パッケージに @Component / @Service / @Controller / @Repository が存在
  • 対処1: 衝突する片方に @Component("explicitName") で明示的なビーン名を付与
  • 対処2: @Primary でデフォルト解決対象を指定、@Qualifier("name") で利用側を限定
  • Spring Boot 2.1+ は Bean 定義の上書きがデフォルト禁止。緊急回避のみ spring.main.allow-bean-definition-overriding=true

エラー全文

Caused by: org.springframework.context.annotation.ConflictingBeanDefinitionException:
Annotation-specified bean name 'userController' for bean class
[com.example.admin.UserController] conflicts with existing,
non-compatible bean definition of same name and class
[com.example.user.UserController]
    at org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider
    ...

Spring の DI コンテナは Bean 名 (ID) で 1 つだけ管理します。@Controller / @Service / @Component はデフォルトで クラスの単純名 (先頭小文字) を Bean 名にするため、別パッケージで同名クラスを定義するとぶつかります。

典型的な発生パターン

// パッケージ A
package com.example.user;
@RestController
public class UserController {              // Bean 名: "userController"
    @GetMapping("/users") ...
}

// パッケージ B (機能拡張で作った)
package com.example.admin;
@RestController
public class UserController {              // ← 同じく "userController"
    @GetMapping("/admin/users") ...
}

// 起動時に ConflictingBeanDefinitionException

原因の切り分け

原因確認対処
異なるパッケージで同名クラスエラーメッセージの 2 つのクラス FQCN明示命名 (対処1) または改名
同じパッケージのスキャン範囲が重複@ComponentScan の basePackagesbasePackages を 1 か所に (対処3)
jar 依存で同名 Bean が含まれる依存ライブラリ調査@Primary / @Qualifier (対処2)
同名 @Bean メソッドが複数 @Configuration に@Bean メソッド名メソッド名変更 or @Bean("name")
テスト時にプロダクションの Bean と衝突@TestConfiguration@Primary でテスト側優先

対処1: Bean 名を明示

// パッケージ A
@RestController("userController")               // 明示
public class UserController { ... }

// パッケージ B
@RestController("adminUserController")          // 別名
public class UserController { ... }

// 他のアノテーションも同様
@Service("userServiceV1")
@Component("oldRepo")
@Repository("legacyUserRepo")

命名規則は 「役割を含む + 一意」 に。例: adminUserController, publicUserService, v2OrderRepository

対処2: @Primary と @Qualifier の使い分け

同じインターフェースを実装する Bean が複数あるとき、利用側で「どれを使うか」を制御します。

public interface NotificationService { void send(String msg); }

@Service
@Primary                                       // ★ デフォルト解決対象
public class EmailNotificationService implements NotificationService {
    public void send(String msg) { /* メール送信 */ }
}

@Service
public class SmsNotificationService implements NotificationService {
    public void send(String msg) { /* SMS 送信 */ }
}

@Service
public class SlackNotificationService implements NotificationService {
    public void send(String msg) { /* Slack 送信 */ }
}

// 利用1: 何も指定しなければ @Primary の Email が注入
@Service
public class AlertService {
    private final NotificationService notifier;
    public AlertService(NotificationService notifier) { this.notifier = notifier; }
}

// 利用2: 明示的に SMS を指定
@Service
public class TwoFactorAuthService {
    private final NotificationService notifier;
    public TwoFactorAuthService(@Qualifier("smsNotificationService") NotificationService n) {
        this.notifier = n;
    }
}

対処3: コンポーネントスキャンの範囲縮小

// ❌ 広すぎる
@SpringBootApplication                        // = @ComponentScan(basePackages = "com.example")
public class Application { ... }

// ✅ 必要なパッケージだけスキャン
@SpringBootApplication(scanBasePackages = {
    "com.example.web",
    "com.example.service",
    "com.example.repository"
})
public class Application { ... }

// ✅ 特定のクラスを除外
@SpringBootApplication
@ComponentScan(
    basePackages = "com.example",
    excludeFilters = @ComponentScan.Filter(
        type = FilterType.ASSIGNABLE_TYPE,
        classes = LegacyUserController.class
    )
)
public class Application { ... }

対処4: @Bean メソッド名衝突

@Configuration
public class DataSourceConfig {
    @Bean
    public DataSource dataSource() { ... }            // Bean 名: "dataSource"
}

@Configuration
public class TestDataSourceConfig {
    @Bean
    public DataSource dataSource() { ... }            // ← 衝突
}

// ✅ メソッド名で区別
@Bean
public DataSource primaryDataSource() { ... }

// ✅ または @Bean に名前指定
@Bean("primaryDataSource")
public DataSource dataSource() { ... }

対処5: Bean 定義の上書きを許可 (非推奨)

Spring Boot 2.1+ では Bean 定義の上書きはデフォルト禁止 (致命的な事故防止のため)。やむを得ず一時的に許可する場合:

# application.properties (非推奨、最終手段)
spring.main.allow-bean-definition-overriding=true

# こうすると後勝ちになるが、どちらが採用されたか分かりにくく
# 本番事故の温床。基本は対処1〜4 で根本解決すべき

対処6: 条件付き Bean 登録

「環境ごとに別の Bean を採用したい」場合は条件アノテーションで衝突を避ける:

// プロファイル別
@Service
@Profile("prod")
public class RealPaymentService implements PaymentService { ... }

@Service
@Profile("dev | test")
public class MockPaymentService implements PaymentService { ... }

// プロパティ条件
@Service
@ConditionalOnProperty(name = "feature.payment.v2", havingValue = "true")
public class PaymentServiceV2 implements PaymentService { ... }

@Service
@ConditionalOnProperty(name = "feature.payment.v2", havingValue = "false", matchIfMissing = true)
public class PaymentServiceV1 implements PaymentService { ... }

// 他の Bean が無いときだけ登録
@Bean
@ConditionalOnMissingBean(PaymentService.class)
public PaymentService fallbackPayment() { ... }

デバッグ: どの Bean が登録されているか確認

@SpringBootApplication
public class Application implements CommandLineRunner {

    @Autowired ApplicationContext ctx;

    public static void main(String[] args) { SpringApplication.run(Application.class, args); }

    @Override
    public void run(String... args) {
        // 全 Bean 名
        for (String name : ctx.getBeanDefinitionNames()) {
            System.out.println(name + " -> " + ctx.getBean(name).getClass().getName());
        }
        // 型から逆引き
        Map map = ctx.getBeansOfType(NotificationService.class);
        map.forEach((k, v) -> System.out.println(k + " : " + v.getClass()));
    }
}

// または Actuator
// management.endpoints.web.exposure.include=beans
// GET /actuator/beans で全 Bean 一覧

FAQ

Q: 名前を変えるとリファクタが大変。一括解決したい
A: 命名規則を導入。例: パッケージ名をプレフィックスにする。@Service("admin.userService") のようにドット区切りでも OK。

Q: ライブラリ jar 内の Bean と衝突した
A: ライブラリ側を @Conditional や除外フィルタで無効化、自分の Bean を @Primary に。直接修正できない場合は ComponentScan 除外。

Q: 起動時に複数の同名 Bean がいるが NoUniqueBeanDefinitionException になる
A: それは「Bean 自体は登録できているが、注入時にどれを選ぶか不定」のエラー。@Primary / @Qualifier で解決。本記事の ConflictingBeanDefinitionException とは別物。

編集
Post Share
子ページ

子ページはありません

同階層のページ
  1. java.lang.IllegalStateException: CGLIB is required to process @Configuration classes
  2. Error creating bean with name 'org.springframework.aop.config.internalAutoProxyCreator': Instantiation of bean failed; nested exception is java.lang.NoClassDefFoundError: Could not initialize class org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator
  3. No mapping found for HTTP request with URI ... in DispatcherServlet with name ...
  4. An internal error occurred during: "Building UI model". com/google/common/base/Function
  5. No identifier specified for entity : ...
  6. org.hibernate.hql.internal.ast.QuerySyntaxException: table_name is not mapped
  7. No compiler is provided in this environment
  8. java.sql.SQLException: The server time zone value ' ... ' is unrecognized or represents more than one time zone. You must configure either the server or JDBC driver (via the serverTimezone configuration property) to use a more specifc time zone value if you want to utilize time zone
  9. Caused by: java.lang.RuntimeException: Executing an update/delete query
  10. Not supported for DML operations
  11. Field ... required a bean of type ... hat could not be found.
  12. Annotation-specified bean name 'xxxController' for bean class [com.xxx.controller.xxxController] conflicts with existing, non-compatible bean definition of same name and class [com.xxx.xxxController]
  13. Whitelabel Error Page This application has no explicit mapping for /error, so you are seeing this as a fallback.
  14. Exception in thread "main" java.lang.UnsupportedClassVersionError