1. 循環依存とは何ですか?

Bean Aが別のBeanBに依存し、BeanBもBeanAに依存している場合、循環依存が発生します。

BeanA→BeanB→BeanA

もちろん、より多くのBeanを暗示することができます。

BeanA→BeanB→BeanC→BeanD→BeanE→BeanA

2. 春に何が起こるか

SpringコンテキストがすべてのBeanをロードすると、完全に機能するために必要な順序でBeanを作成しようとします。

循環依存関係がないとしましょう。 代わりに、次のようなものがあります。

BeanA→BeanB→BeanC

SpringはBeanCを作成し、次にBean Bを作成し(そしてBean Cをそれに注入し)、次にBean Aを作成します(そしてBean Bをそれに注入します)。

ただし、循環依存の場合、Springは、相互に依存しているため、最初に作成するBeanを決定できません。 このような場合、Springはコンテキストのロード中にBeanCurrentlyInCreationExceptionを発生させます。

コンストラクタインジェクションを使用するとSpringで発生する可能性があります。他のタイプのインジェクションを使用する場合、依存関係はコンテキストの読み込みではなく必要なときに注入されるため、この問題は発生しません。

3. 簡単な例

(コンストラクターインジェクションを介して)相互に依存する2つのBeanを定義しましょう:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(CircularDependencyB circB) {
        this.circB = circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    @Autowired
    public CircularDependencyB(CircularDependencyA circA) {
        this.circA = circA;
    }
}

これで、コンポーネントをスキャンする基本パッケージを指定するテスト用の構成クラス( TestConfig と呼びます)を作成できます。

Beanがパッケージ「com.baeldung.circulardependency」で定義されていると仮定します。

@Configuration
@ComponentScan(basePackages = { "com.baeldung.circulardependency" })
public class TestConfig {
}

最後に、循環依存関係をチェックするJUnitテストを作成できます。

コンテキストの読み込み中に循環依存が検出されるため、テストは空にすることができます。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {

    @Test
    public void givenCircularDependency_whenConstructorInjection_thenItFails() {
        // Empty test; we just want the context to load
    }
}

このテストを実行しようとすると、次の例外が発生します。

BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA':
Requested bean is currently in creation: Is there an unresolvable circular reference?

4. 回避策

ここで、この問題に対処するための最も一般的な方法のいくつかを示します。

4.1. 再設計

循環依存がある場合、設計上の問題があり、責任が十分に分離されていない可能性があります。 コンポーネントを適切に再設計して、階層が適切に設計され、循環依存が不要になるようにする必要があります。

ただし、レガシーコード、テスト済みで変更できないコード、完全な再設計に十分な時間やリソースがないなど、再設計を実行できない理由は多数考えられます。 コンポーネントを再設計できない場合は、いくつかの回避策を試すことができます。

4.2. @Lazyを使用

サイクルを中断する簡単な方法は、SpringにBeanの1つを遅延的に初期化するように指示することです。 したがって、Beanを完全に初期化する代わりに、他のBeanに注入するためのプロキシを作成します。 注入されたBeanは、最初に必要になったときにのみ完全に作成されます。

コードでこれを試すために、CircularDependencyAを変更できます。

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public CircularDependencyA(@Lazy CircularDependencyB circB) {
        this.circB = circB;
    }
}

ここでテストを実行すると、今回はエラーが発生していないことがわかります。

4.3. セッター/フィールドインジェクションを使用する

最も一般的な回避策の1つであり、 Springのドキュメントでが示唆していることは、セッターインジェクションを使用することです。

簡単に言えば、Beanの配線方法を変更することで問題に対処できます。つまり、コンストラクターインジェクションの代わりにセッターインジェクション(またはフィールドインジェクション)を使用します。 このようにして、SpringはBeanを作成しますが、依存関係は必要になるまで注入されません。

それでは、セッターインジェクションを使用するようにクラスを変更し、別のフィールド( message )を CircularDependencyB に追加して、適切な単体テストを実行できるようにします。

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

    @Autowired
    public void setCircB(CircularDependencyB circB) {
        this.circB = circB;
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

次に、単体テストにいくつかの変更を加える必要があります。

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = { TestConfig.class })
public class CircularDependencyTest {

    @Autowired
    ApplicationContext context;

    @Bean
    public CircularDependencyA getCircularDependencyA() {
        return new CircularDependencyA();
    }

    @Bean
    public CircularDependencyB getCircularDependencyB() {
        return new CircularDependencyB();
    }

    @Test
    public void givenCircularDependency_whenSetterInjection_thenItWorks() {
        CircularDependencyA circA = context.getBean(CircularDependencyA.class);

        Assert.assertEquals("Hi!", circA.getCircB().getMessage());
    }
}

これらの注釈を詳しく見てみましょう。

@Bean は、注入するBeanの実装を取得するためにこれらのメソッドを使用する必要があることをSpringフレームワークに通知します。

また、 @Test アノテーションを使用すると、テストはコンテキストから CircularDependencyA Beanを取得し、 CircularDependencyB が適切に挿入されたことを表明し、の値を確認します。 ]messageプロパティ。

4.4. @PostConstructを使用します

サイクルを中断する別の方法は、Beanの1つに @Autowired を使用して依存関係を注入し、次に@PostConstructで注釈が付けられたメソッドを使用して他の依存関係を設定することです。

私たちの豆はこのコードを持つことができます:

@Component
public class CircularDependencyA {

    @Autowired
    private CircularDependencyB circB;

    @PostConstruct
    public void init() {
        circB.setCircA(this);
    }

    public CircularDependencyB getCircB() {
        return circB;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;
	
    private String message = "Hi!";

    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }
	
    public String getMessage() {
        return message;
    }
}

また、以前と同じテストを実行できるため、循環依存の例外がまだスローされていないこと、および依存が適切に挿入されていることを確認します。

4.5. ApplicationContextAwareおよびInitializingBeanを実装します

Beanの1つがApplicationContextAwareを実装している場合、そのBeanはSpringコンテキストにアクセスでき、そこから他のBeanを抽出できます。

InitializingBean を実装することにより、すべてのプロパティが設定された後、このBeanがいくつかのアクションを実行する必要があることを示します。 この場合、依存関係を手動で設定します。

Beanのコードは次のとおりです。

@Component
public class CircularDependencyA implements ApplicationContextAware, InitializingBean {

    private CircularDependencyB circB;

    private ApplicationContext context;

    public CircularDependencyB getCircB() {
        return circB;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        circB = context.getBean(CircularDependencyB.class);
    }

    @Override
    public void setApplicationContext(final ApplicationContext ctx) throws BeansException {
        context = ctx;
    }
}
@Component
public class CircularDependencyB {

    private CircularDependencyA circA;

    private String message = "Hi!";

    @Autowired
    public void setCircA(CircularDependencyA circA) {
        this.circA = circA;
    }

    public String getMessage() {
        return message;
    }
}

ここでも、前のテストを実行して、例外がスローされておらず、テストが期待どおりに機能していることを確認できます。

5. 結論

Springで循環依存に対処する方法はたくさんあります。

循環依存の必要がないように、最初にBeanの再設計を検討する必要があります。 これは、循環依存関係は通常、改善可能な設計の症状であるためです。

ただし、プロジェクトで循環依存がどうしても必要な場合は、ここで提案されている回避策のいくつかに従うことができます。

推奨される方法は、セッターインジェクションを使用することです。 しかし、他の選択肢もあります。一般的に、SpringがBeanの初期化とインジェクションを管理するのを停止し、さまざまな戦略を使用してこれを自分で達成することに基づいています。

この記事の例は、GitHubプロジェクトにあります。