Dépendances circulaires au printemps

Dépendances circulaires au printemps

1. Qu'est-ce qu'une dépendance circulaire?

Cela se produit lorsqu'un haricot A dépend d'un autre haricot B et que le haricot B dépend également du haricot A:

Bean A → Bean B → Bean A

Bien sûr, nous pourrions avoir plus de haricots impliqués:

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

2. Ce qui se passe au printemps

Lorsque le contexte Spring charge tous les haricots, il essaie de créer les haricots dans l'ordre nécessaire à leur fonctionnement complet. Par exemple, si nous n’avions pas de dépendance circulaire, comme dans le cas suivant:

Bean A → Bean B → Bean C

Spring créera le haricot C, puis créera le haricot B (et y injectera le haricot C), puis créera le haricot A (et y injectera le haricot B).

Cependant, lorsqu'il a une dépendance circulaire, Spring ne peut pas décider lequel des haricots doit être créé en premier, car ils dépendent les uns des autres. Dans ces cas, Spring lèvera unBeanCurrentlyInCreationException lors du chargement du contexte.

Cela peut arriver au printemps lors de l'utilisation deconstructor injection; si vous utilisez d'autres types d'injections, vous ne devriez pas trouver ce problème car les dépendances seront injectées quand elles sont nécessaires et non sur le chargement du contexte.

3. Un exemple rapide

Définissons deux beans qui dépendent l’un de l’autre (via l’injection du constructeur):

@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;
    }
}

Nous pouvons maintenant écrire une classe de configuration pour les tests, appelons-laTestConfig, qui spécifie le package de base à rechercher des composants. Supposons que nos beans soient définis dans le package «com.example.circulardependency»:

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

Et enfin, nous pouvons écrire un test JUnit pour vérifier la dépendance circulaire. Le test peut être vide, car la dépendance circulaire sera détectée lors du chargement du contexte.

@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
    }
}

Si vous essayez d'exécuter ce test, vous obtiendrez l'exception suivante:

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

4. Les solutions de contournement

Nous montrerons quelques-uns des moyens les plus populaires de résoudre ce problème.

4.1. Refonte

Lorsque vous avez une dépendance circulaire, vous avez probablement un problème de conception et les responsabilités ne sont pas bien séparées. Vous devez essayer de redéfinir les composants correctement afin que leur hiérarchie soit bien conçue et qu'il ne soit pas nécessaire de recourir à des dépendances circulaires.

Si vous ne pouvez pas redéfinir les composants (il peut exister de nombreuses raisons possibles: code hérité, code qui a déjà été testé et ne peut pas être modifié, pas assez de temps ou de ressources pour une refonte complète…), il existe des solutions de contournement à essayer.

4.2. Utilisez@Lazy

Un moyen simple de rompre le cycle consiste à dire au printemps d’initialiser l’un des haricots paresseusement. C'est-à-dire qu'au lieu d'initialiser complètement le haricot, il créera un proxy pour l'injecter dans l'autre haricot. Le haricot injecté ne sera entièrement créé qu’à la première utilisation.

Pour essayer ceci avec votre code, vous pouvez changer la Dépendance Circulaire comme suit:

@Component
public class CircularDependencyA {

    private CircularDependencyB circB;

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

Si vous exécutez le test maintenant, vous verrez que l'erreur ne se produit pas cette fois-ci.

4.3. Utiliser l'injection Setter / Field

L'une des solutions de contournement les plus populaires, et aussi ce queSpring documentation proposes, utilise l'injection de setter.

En termes simples, si vous modifiez la façon dont vos haricots sont câblés pour utiliser l’injection de setter (ou l’injection sur le terrain) au lieu de l’injection de constructeur, cela résout le problème. De cette façon, Spring crée les haricots, mais les dépendances ne sont pas injectées tant qu'elles ne sont pas nécessaires.

Faisons cela - changeons nos classes pour utiliser des injections de setter et ajouterons un autre champ (message) àCircularDependencyB afin que nous puissions faire un test unitaire approprié:

@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;
    }
}

Nous devons maintenant apporter quelques modifications à notre test unitaire:

@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());
    }
}

Ce qui suit explique les annotations vues ci-dessus:

@Bean: pour indiquer au framework Spring que ces méthodes doivent être utilisées pour récupérer une implémentation des beans à injecter.

@Test: Le test obtiendra le bean CircularDependencyA du contexte et affirmera que son CircularDependencyB a été injecté correctement, vérifiant la valeur de sa propriétémessage.

4.4. Utilisez@PostConstruct

Une autre façon de rompre le cycle consiste à injecter une dépendance en utilisant@Autowired sur l'un des beans, puis à utiliser une méthode annotée avec@PostConstruct pour définir l'autre dépendance.

Nos haricots pourraient avoir le code suivant:

@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;
    }
}

Et nous pouvons exécuter le même test que précédemment, nous vérifions donc que l'exception de dépendance circulaire n'est toujours pas générée et que les dépendances sont correctement injectées.

4.5. ImplémenterApplicationContextAware etInitializingBean

Si l'un des beans implémenteApplicationContextAware, le bean a accès au contexte Spring et peut extraire l'autre bean à partir de là. En implémentantInitializingBean, nous indiquons que ce bean doit faire quelques actions après que toutes ses propriétés ont été définies; dans ce cas, nous voulons définir manuellement notre dépendance.

Le code de nos haricots serait:

@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;
    }
}

De nouveau, nous pouvons exécuter le test précédent et voir que l'exception n'est pas levée et que le test fonctionne comme prévu.

5. En conclusion

Il existe de nombreuses façons de traiter les dépendances circulaires au printemps. La première chose à considérer est de repenser vos beans de manière à éviter les dépendances circulaires: ils sont généralement le symptôme d’une conception qui peut être améliorée.

Mais si vous avez absolument besoin de dépendances circulaires dans votre projet, vous pouvez suivre certaines des solutions de contournement suggérées ici.

La méthode préférée consiste à utiliser des injections de setter. Mais il existe d'autres alternatives, généralement basées sur le fait d'empêcher Spring de gérer l'initialisation et l'injection des haricots, et de le faire vous-même en utilisant une stratégie ou une autre.

Les exemples peuvent être trouvés dans lesGitHub project.