春の循環依存

春の循環依存

1. 循環依存とは

Bean Aが別のBean Bに依存しており、Bean BもBean Aに依存している場合に発生します。

Bean A→Bean B→Bean A

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

Bean A→Bean B→Bean C→Bean D→Bean E→Bean A

2. 春に何が起こるか

SpringコンテキストがすべてのBeanをロードすると、Beanが完全に機能するために必要な順序でBeanを作成しようとします。 たとえば、次のような循環依存関係がなかった場合:

Bean A→Bean B→Bean C

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

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

constructor injectionを使用すると、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;
    }
}

これで、テスト用のConfigurationクラスを記述できます。これをTestConfigと呼びます。これは、コンポーネントをスキャンする基本パッケージを指定します。 Beanがパッケージ「com.example.circulardependency」で定義されていると仮定しましょう。

@Configuration
@ComponentScan(basePackages = { "com.example.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は、最初に必要になったときにのみ完全に作成されます。

コードでこれを試すために、循環依存を次のように変更できます。

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

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

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

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

最も一般的な回避策の1つであり、Spring documentation proposesは、セッターインジェクションを使用しています。

コンストラクター注入の代わりにセッター注入(またはフィールド注入)を使用するように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で注釈が付けられたメソッドを使用して他の依存関係を設定することです。

Beanには次のコードを含めることができます。

@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. ApplicationContextAwareInitializingBeanを実装します

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 projectにあります。