@Serviceはある。なぜ見つからない? 定義・探索・読み込みをつなぐ。

広告 実装・エラー解決

NoSuchBeanDefinitionExceptionの原因|XML・@Serviceがあるのに登録されないとき

@Serviceを付けたクラスがあるのに、起動するとNoSuchBeanDefinitionExceptionになる。XMLにも探索の指定があるなら、まず確かめるのはそのXMLを、例外を出したApplicationContextが読んだかです。正しい設定ファイルがプロジェクトにあっても、別の入口から起動すれば、その設定でBeanが登録されるとは限りません。

この記事でわかること

  • ログから要求型と候補数を拾い、「探索先のずれ」と「XML未読込」を分ける。
  • 同じSomeServiceで候補0件・1件・2件を再現し、直す設定を決める。
  • 候補が複数なら、既定の実装を選ぶ設定と、呼び出し側で個別に選ぶ設定を使い分ける。

ログから要求型を抜き、候補0件と複数件を分ける

保守中の問い合わせ画面で、新しい処理がSomeServiceを必要とするようになったとします。起動エラーの先頭にControllerの名前が出ても、直す対象がControllerとは限りません。例外の原因をたどり、「どのクラスの何番目の引数が」「どの型、または名前を要求し」「候補がいくつ見つかったか」を控えます。SomeServiceとだけメモすると、同じ短い名前の別パッケージを開くことがあります。型はパッケージ名まで合わせてください。

同じ型を取得する場合、候補0件ならNoSuchBeanDefinitionException、候補が複数で一つに決められない場合はNoUniqueBeanDefinitionExceptionです。後者は前者の派生型なので、catch (NoSuchBeanDefinitionException ...)に入ったという事実だけで0件と判断すると誤ります。ログにexpected single matching bean but found 2と出ているなら、すでに登録はされています。新しく@Serviceを足す前に、実際に返された例外クラスと候補名を読んでください。

この記事の検証はcontext.getBean(SomeService.class)を使います。実アプリのコンストラクタ注入では、外側にUnsatisfiedDependencyExceptionなどが出ることがあります。今回の出力は原因の切り分けに必要な部分を取り出したものです。Controllerが起動することや、すべての自動設定の組み合わせを試したものではありません。

同じクラスで設定だけを変え、直す場所を絞る

検証では要求型をinfo.syachiku.inquiry.SomeServiceに固定し、クラスを書き換えず、次の条件を一つずつ変えます。名前で明示して取得するB06以外は型による取得です。たとえばB02とB03は同じ0件でも、開くファイルが違います。

表は横にスクロールできます。

ケースコンテナの条件結果確認する場所
B01scan.xmlを読み、正しいパッケージを探索1件、someServiceを取得正常な基準
B02wrong-scan.xmlを読み、別パッケージを探索0件、NoSuchBeanDefinitionExceptionbase-package
B03XMLを読まず、空のコンテナを起動0件、同じ例外起動入口・import経路
B04duplicate.xmlで同じ型を別名で二つ登録2件、NoUniqueBeanDefinitionException候補名と選択条件
B05primary.xmlで一方を既定に指定2件のまま、型でserviceAを取得既定を選ぶ契約
B06B04のXMLのまま、要求側で名前と型を指定2件のうちserviceAを取得この要求だけの選択

対象クラスは次の全文です。戻り値の文字列は、取得したBeanを実際に呼べることを確かめるための目印です。このクラスにさらにアノテーションを追加して、ケースごとの結果を作り分けてはいません。

package info.syachiku.inquiry;

import org.springframework.stereotype.Service;

@Service
public class SomeService {
    public String doSomething() {
        return "Hello from SomeService!";
    }
}

XMLが読まれているなら、package宣言と探索先を突き合わせる

B01ではscan.xmlを読み込んでinfo.syachiku.inquiryを探索します。コンポーネントスキャンが@Service付きのクラスを候補として見つけ、今回は既定の名前someServiceで登録します。XML断片だけを貼り付けると名前空間の宣言がなくて別のエラーになるため、配布ファイルにはヘッダーも含めています。

正常なscan.xmlの全文を開く
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <context:component-scan base-package="info.syachiku.inquiry"/>
</beans>

B02のwrong-scan.xmlは、同じヘッダーでbase-packageだけを次の値に変えています。パッケージの綴りや階層が違えば、同じ@Serviceでもこの探索には入りません。直すなら、対象クラスが属するパッケージを含むように、必要な範囲だけ指定します。

    <context:component-scan base-package="info.syachiku.missing"/>

「ひとまずアプリ全体を探索する」という直し方なら、探していた一つは見つかるかもしれません。一方で、別用途の実装や別系統の設定まで取り込むと、今度は候補が複数になったり、想定外の設定が有効になったりします。既存の探索範囲を広げるなら、必要なクラスと余計に登録されるクラスを分けて確認してください。この例であえてinfo.syachiku.inquiryだけを探索するのは、その境界が読めるようにするためです。

既存の登録方法は探索だけではありません。@BeanやXMLの<bean>で同じ型を作っていることもあります。既存コードでは「どの仕組みで作る予定だったか」を一つ見つけ、そこから調べます。アノテーションの有無だけで不具合と判定しないのがポイントです。

探索先が合っているなら、XMLを読み込む入口まで戻る

B03では正しいscan.xmlが同じサンプル内にあり、クラスにも@Serviceがあります。しかし、入口がnew GenericApplicationContext()の後にrefresh()するだけなら、そのXMLの探索指示は使われません。B01との違いはクラスやXMLの中身ではなく、どの設定をコンテナへ渡して起動したかです。

package info.syachiku.bean;

import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.context.support.GenericApplicationContext;

public final class BeanContexts {
    private BeanContexts() {}

    public static ClassPathXmlApplicationContext fromXml(String resource) {
        return new ClassPathXmlApplicationContext(resource);
    }

    public static GenericApplicationContext withoutXml() {
        GenericApplicationContext context = new GenericApplicationContext();
        context.refresh();
        return context;
    }
}

fromXml("scan.xml")はクラスパス上のXMLを指定してClassPathXmlApplicationContextを作ります。一方、withoutXml()はXMLを一枚も指定していません。B03を直す操作は、対象を探索する正しいXMLを入口から読み込むことです。XMLの保存場所をresourcesへ移すだけでは、読み込むコードや設定が増えない限り変化しません。

現場では、まず実際に使った起動クラス、テストの設定、サーブレット設定などを特定し、その入口からXMLの<import>やJava設定の@ImportResourceをたどります。指定しているリソース名と実際に配布されたクラスパス上のファイルを照合してください。ファイルを明示していて存在しない、またはXMLが壊れている場合は、読み込み時点で別のエラーになります。「何も指定していないため0件」のB03とは分けて読みます。

本体とテストで起動入口が違う場合は、テスト側だけでBeanが足りないことがあります。親子のApplicationContextがある場合も、例外を出した側から参照できる登録先を確認します。今回の候補数は親なしの小さなコンテナで数えています。実環境でgetBeanNamesForTypeの結果だけを見て、親からの参照や注入条件まで含めた候補数と決めつけないでください。

XMLを使わないSpring Bootで「No qualifying bean of type」が出て候補が0件なら、実装のスキャン範囲、@Beanによる登録、Profile、テストが読み込む設定を順に確認します。

関連記事Spring Bootの「No qualifying bean of type」原因|DIできないときの対処法 XMLを使わないBoot構成なら、起動クラスやテストで読み込む範囲、Profileを順に確認します。 記事を読む

候補が二つなら、登録を増やさず選択の仕様を決める

B04は探索ではなく、同じクラスをserviceAとserviceBという別名で二つ登録しています。今回はBean名の衝突ではありません。型だけを指定するとSpringが一方を決められないことを、例外の型・候補数・候補名までJUnitで確かめます。二つのBeanは同じクラスでも別のインスタンスです。

複数候補のduplicate.xmlを開く
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <bean id="serviceA" class="info.syachiku.inquiry.SomeService"/>
    <bean id="serviceB" class="info.syachiku.inquiry.SomeService"/>
</beans>

必要な実装が一つだけなら、重複登録の理由を調べて不要な定義を外します。二つとも必要で、型だけを指定した要求の既定がserviceAなら、B05のようにXMLでprimary="true"を指定できます。Java設定の@Primaryと同じく、ここでは既定の候補を選ぶために使います。候補0件を1件に増やす設定ではありません。二つの定義は残り、別名で取り出すこともできます。

    <bean id="serviceA" class="info.syachiku.inquiry.SomeService" primary="true"/>
    <bean id="serviceB" class="info.syachiku.inquiry.SomeService"/>

「この処理だけがAを使う」なら、全体の既定値を変えるより、要求側で明示する方が変更範囲を絞れます。B06では同じduplicate.xmlのまま、getBean("serviceA", SomeService.class)を実行します。コンストラクタ注入を使う実アプリなら、意味のある@Qualifierで選ぶ方法があります。B06はAPIの名前指定を試したものであり、@Qualifier付きの注入全体を試したわけではありません。

ログがNo bean named 'someService' availableなら、先に要求された名前を確認します。scan.xmlで登録するB01の名前はsomeServiceですが、duplicate.xmlに登録される名前はserviceAとserviceBです。このコンテナには要求型のBeanが2件あります。呼び出し側が指定した名前とXMLのidを照合します。

配布ZIPを展開した作業用コピーで、src/test/java/info/syachiku/bean/BeanLookupTest.javaのB06にあるgetBean("serviceA", SomeService.class)の名前だけをsomeServiceに変えます。pom.xmlのある場所でmvn "-Dtest=BeanLookupTest#B06_explicitNameChoosesFromTheDuplicateConfiguration" testを実行すると、NoSuchBeanDefinitionExceptionで失敗します。名前をserviceAに戻すと同じ1件が成功します。実際の修正では、どちらのBeanを使う仕様かを確認してから、呼び出し側の名前と登録名をそろえます。

既定値を決める場合でも、実際に選んだ実装が要件を満たすかを確認します。たとえば本番送信用と検証用の二つが必要な構成で、単に起動させたいから検証用を既定にすると、起動エラーは消えても送信処理が意図とずれることがあります。名前を見て選ぶのではなく、「どの入口で、どちらの処理が必要か」を先に決めます。

JUnit6件とmainを実行し、候補数と例外名を確認する

ZIPを展開し、pom.xmlのあるディレクトリで実行します。testで対象コードのコンパイル、XMLの読み込み、JUnitの比較を行います。2行目は同じ設定でmainを起動して、候補数をまとめて読むためのコマンドです。

mvn -B test
mvn -B org.codehaus.mojo:exec-maven-plugin:3.6.3:java -Dexec.mainClass=info.syachiku.bean.BeanLookupDemo
B01 scan: candidates=1, Hello from SomeService!
B02 wrong scan: candidates=0, NoSuchBeanDefinitionException
B03 XML unread: candidates=0, NoSuchBeanDefinitionException
B04 duplicate: candidates=2, NoUniqueBeanDefinitionException
B05 primary: candidates=2, Hello from SomeService!

JUnitはB01〜B06の6件が通過しました。B02とB03は「原因が異なるが、同じ0件になる」こと、B04はassertThrowsExactlyで親型ではなく実際の例外型、B05は候補数が2件のまま選んだインスタンスが一致することを確認します。B06はテストだけで確認するため、上のmain出力にはありません。

起動して5状態を出力するBeanLookupDemo.javaの全文を開く
package info.syachiku.bean;

import info.syachiku.inquiry.SomeService;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import org.springframework.context.ConfigurableApplicationContext;

public final class BeanLookupDemo {
    public static void main(String[] args) {
        show("B01 scan", BeanContexts.fromXml("scan.xml"));
        show("B02 wrong scan", BeanContexts.fromXml("wrong-scan.xml"));
        show("B03 XML unread", BeanContexts.withoutXml());
        show("B04 duplicate", BeanContexts.fromXml("duplicate.xml"));
        show("B05 primary", BeanContexts.fromXml("primary.xml"));
    }

    private static void show(String label, ConfigurableApplicationContext context) {
        try (context) {
            int count = context.getBeanNamesForType(SomeService.class).length;
            try {
                SomeService service = context.getBean(SomeService.class);
                System.out.println(label + ": candidates=" + count + ", " + service.doSomething());
            } catch (NoUniqueBeanDefinitionException e) {
                System.out.println(label + ": candidates=" + count + ", NoUniqueBeanDefinitionException");
            } catch (NoSuchBeanDefinitionException e) {
                System.out.println(label + ": candidates=" + count + ", NoSuchBeanDefinitionException");
            }
        }
    }
}
6ケースのJUnitテスト全文を開く
package info.syachiku.bean;

import info.syachiku.inquiry.SomeService;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.NoUniqueBeanDefinitionException;
import static org.junit.jupiter.api.Assertions.*;

class BeanLookupTest {
    @Test void B01_xmlLoadsTheAnnotatedService() {
        try (var context = BeanContexts.fromXml("scan.xml")) {
            assertArrayEquals(new String[]{"someService"}, context.getBeanNamesForType(SomeService.class));
            assertEquals("Hello from SomeService!", context.getBean(SomeService.class).doSomething());
        }
    }

    @Test void B02_wrongScanPackageLeavesZeroCandidates() {
        try (var context = BeanContexts.fromXml("wrong-scan.xml")) {
            assertEquals(0, context.getBeanNamesForType(SomeService.class).length);
            assertThrowsExactly(NoSuchBeanDefinitionException.class, () -> context.getBean(SomeService.class));
        }
    }

    @Test void B03_annotationAloneDoesNotLoadXml() {
        try (var context = BeanContexts.withoutXml()) {
            assertEquals(0, context.getBeanNamesForType(SomeService.class).length);
            assertThrowsExactly(NoSuchBeanDefinitionException.class, () -> context.getBean(SomeService.class));
        }
    }

    @Test void B04_twoCandidatesCannotBeChosenByType() {
        try (var context = BeanContexts.fromXml("duplicate.xml")) {
            assertEquals(Set.of("serviceA", "serviceB"), Set.of(context.getBeanNamesForType(SomeService.class)));
            var error = assertThrowsExactly(NoUniqueBeanDefinitionException.class, () -> context.getBean(SomeService.class));
            assertEquals(2, error.getNumberOfBeansFound());
            assertEquals(Set.of("serviceA", "serviceB"), Set.copyOf(error.getBeanNamesFound()));
        }
    }

    @Test void B05_primaryChoosesOneWithoutDeletingTheOther() {
        try (var context = BeanContexts.fromXml("primary.xml")) {
            assertEquals(2, context.getBeanNamesForType(SomeService.class).length);
            assertSame(context.getBean("serviceA"), context.getBean(SomeService.class));
            assertNotSame(context.getBean("serviceA"), context.getBean("serviceB"));
        }
    }

    @Test void B06_explicitNameChoosesFromTheDuplicateConfiguration() {
        try (var context = BeanContexts.fromXml("duplicate.xml")) {
            assertEquals("Hello from SomeService!", context.getBean("serviceA", SomeService.class).doSomething());
            assertNotSame(context.getBean("serviceA"), context.getBean("serviceB"));
        }
    }
}
pom.xmlの全文を開く
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion><groupId>info.syachiku.samples</groupId><artifactId>xml-bean-discovery</artifactId><version>1.0.0</version>
  <properties><maven.compiler.release>17</maven.compiler.release><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties>
  <dependencies>
    <dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>6.2.19</version></dependency>
    <dependency><groupId>org.junit.jupiter</groupId><artifactId>junit-jupiter</artifactId><version>5.12.2</version><scope>test</scope></dependency>
  </dependencies>
  <build><plugins>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-resources-plugin</artifactId><version>3.2.0</version></plugin>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version></plugin>
    <plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>3.5.6</version></plugin>
  </plugins></build>
</project>

期待どおり通るだけでなく、配布版とは別のコピーでscan.xmlの探索先をinfo.syachiku.missingへ変えました。B01が「候補1件のはずが0件」を検出して失敗したため、テストが探索設定のずれを見逃さないことも確認しました。

検証環境はJava 17.0.8、Maven 3.9.5、Spring Framework 6.2.19、JUnit 5.12.2です。既存キャッシュを使ってテストとmainを実行しました。依存がない環境での初回取得、Webアプリの起動、Profile別の構成、本番の起動設定は未検証です。

レビューには「要求型・登録方法・読み込み元」を残す

例:SomeServiceを型で要求しています。クラスには@Serviceがあり、scan.xmlの探索先も一致しています。テストが作るコンテナではXMLの読み込みがなく0件でした。本体と同じXMLを指定すべきテストかを確認し、該当する場合だけ起動設定を直します。

この報告なら、「クラスの探索が必要なのか」「設定を読み込む入口がないのか」「そもそもテストがその依存を持つべきか」を次の担当者が判断できます。必須依存を任意にしたり、newで直接作ったりすれば、起動エラーが見えなくなる場合があります。ただしSpringが依存を渡すことや、トランザクションなどの管理が必要な設計なら、その要件を置き去りにした修正です。

  • 要求した完全修飾型名、名前指定の有無、候補数を控えたか。
  • 対象を登録する方法と、設定を読む実際の入口を分けて確認したか。
  • 候補2件なら、重複の解消と既定・個別選択のどれが必要か説明できるか。
  • 修正後に起動するだけでなく、想定した実装が選ばれたことを確認したか。

Beanの定義・登録・注入を、それぞれの責務から学びたい場合に。配布サンプルを先に動かし、どの段階で候補が消えたかを考えてから読むと理解しやすくなります。

書籍「Spring徹底入門 第2版」の表紙
商品画像:Amazon.co.jp
PR 入門後の参考書|Spring Framework 5向け

Spring徹底入門 第2版

株式会社NTTデータ (著), 株式会社NTTデータグループ (編集)

アプリを作った後に、DIやDB更新の仕組みを調べる。

DI、MVC、データアクセス、テスト、セキュリティまで幅広く確認できます。小さなWebアプリを一通り作り、Springの仕組みを詳しく学びたい人に向く参考書です。

  • Springで小さなWebアプリを作ったことがある
  • DIやトランザクションの仕組みを確認したい

Spring Framework 5向けです。Boot 3の実装に使うコード・設定は、採用している版の公式資料と照合してください。

当サイトはAmazonアソシエイト・プログラムの参加者です。価格・在庫・配送条件はAmazonでご確認ください。

関連記事Spring Bootの@Qualifierと@Primaryの違いは?Beanが複数ある時の使い分け 候補が複数あるとき、@Qualifierで対象を指定するか@Primaryで既定を決めるかを比較します。 記事を読む 関連記事Springの@Component・@Service・@Repositoryの違い|どれを付けるべき? @Component・@Service・@Repositoryの役割と、アノテーションを付けたクラスが登録される条件を整理します。 記事を読む

候補0件なら登録まで、複数件なら選択までたどる

探索XMLが正しくても、起動するコンテナがそのXMLを読まなければ候補は増えません。候補0件なら要求型→登録方法→読み込む入口をたどり、候補が複数なら業務の要件から選択方法を決めます。クラスを変える前に、今回のB01〜B04のどこに当たるかを書いてみてください。

参考:Spring Framework 6.2のコンポーネントスキャン、NoUniqueBeanDefinitionException API。

-実装・エラー解決
-, ,