プログラミング

実装・エラー解決

Springとlombokのallargsconstructorで「NoUniqueBeanDefinitionException」に対応する

この記事に書いてあること

前回の記事はこの記事を書くための前知識として作成しました。本題はこちらです。

関連記事Springで「NoUniqueBeanDefinitionException」が出たときの対策 同じインタフェースの実装が複数ありBeanを選べない場合に、@Qualifierで注入先を指定する基本例を確認できます。 記事を読む

親クラスへ渡すコンストラクタ引数に同じ型のBeanが二つあると、起動時に NoUniqueBeanDefinitionException が発生することがあります。Lombokの @AllArgsConstructor は、ここでは親クラスのコンストラクタを生成します。

コンストラクタが一つのBeanでは、そのコンストラクタを Spring が注入に使います。サンプルでは子クラスが受け取った値を親クラスのコンストラクタへ渡します。

Lombokの @AllArgsConstructor は各インスタンスフィールドを引数にしたコンストラクタを生成します。

前提

  • Spring Framework 4.xx
  • java8

元記事の環境は Spring Framework 4 系と Java 8 です。配布コードは Spring Framework 6.2.19、Lombok 1.18.36、JDK 17 で確認しました。

事象

以下を見てください。これは前回の記事のクラス図を修正したものです。この辺の記事にあるように「仕組み部分」を共通化しようとした時にこんなコードになります。

implementsとabstractを使用したクラス図の例

図の赤字のクラス名は AbstractLogic に読み替えてください。サンプルでは、子クラスの失敗例を AmbiguousLogic、修正例を SampleLogicImpl2 としています。

抽象クラス AbstractLogic の common() は、SampleClass の接頭辞と SampleUtil が返した文字列をつなげます。子クラスの run() が common() を呼びます。

実行用サンプル

子クラスの引数を解決できない場合と、Qualifierで選んだBeanを親の処理に渡せる場合をJUnitで確かめます。 ZIPにはソース全文・テスト・pom.xml・READMEを収録しています。JDK 17とMaven 3.9系で mvn test を実行してください。

package info.syachiku.constructor;

import lombok.AllArgsConstructor;

@AllArgsConstructor
public abstract class AbstractLogic {
    private final SampleUtil sampleUtil;
    private final SampleClass sampleClass;

    protected String common() {
        return sampleClass.prefix() + sampleUtil.message();
    }
}

子クラスのコンストラクタは、受け取った SampleUtil と SampleClass を super(...) で親クラスに渡します。

package info.syachiku.constructor;

import org.springframework.stereotype.Component;

@Component
public class AmbiguousLogic extends AbstractLogic implements SampleLogic {

    public AmbiguousLogic(SampleUtil sampleUtil, SampleClass sampleClass) {
        super(sampleUtil, sampleClass);
    }

    @Override
    public String run() {
        return common();
    }
}

SampleUtil 型の Bean を sampleUtilImpl と sampleUtilBackup の二つ登録して起動すると、AmbiguousLogic の第1引数が解決できず、NoUniqueBeanDefinitionException が原因となって失敗します。

原因

前回の記事と同様です。

Spring が選べないのは、生成された親コンストラクタの中の値ではなく、Spring が呼び出す子クラスの第1引数です。SampleClass の候補は一つなのでそのまま注入できます。

対策

子クラスの SampleUtil 引数に @Qualifier("sampleUtilImpl") を置きます。テストでは二つの候補を登録したうえで、run() が message:A を返すことを確認します。

package info.syachiku.constructor;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

@Component
public class SampleLogicImpl2 extends AbstractLogic implements SampleLogic {

    public SampleLogicImpl2(
            @Qualifier("sampleUtilImpl") SampleUtil sampleUtil,
            SampleClass sampleClass) {
        super(sampleUtil, sampleClass);
    }

    @Override
    public String run() {
        return common();
    }
}

Spring がコンストラクタを呼ぶ時点で候補を一つに絞り、子クラスは選ばれた Bean を親クラスへ渡します。フィールド注入に切り替える必要はありません。

この情報を残そうと思った経緯

後輩がこのエラーに遭遇し、コンストラクタ注入からフィールド注入へ切り替えようとしていました。変更すべきなのは、候補が複数あるコンストラクタ引数の指定です。

意外と知らない人いそうだったので、啓蒙のために。

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