Pourquoi ApplicationContext.getBean de Spring est-il considéré comme mauvais?

270

J'ai posé une question générale sur Spring: les beans Spring auto-castés et plusieurs personnes ont répondu qu'il ApplicationContext.getBean()fallait éviter autant que possible d' appeler Spring's . Pourquoi donc?

Sinon, comment puis-je accéder aux beans que j'ai configurés pour créer Spring?

J'utilise Spring dans une application non Web et j'avais prévu d'accéder à un ApplicationContextobjet partagé comme décrit par LiorH .

Amendement

J'accepte la réponse ci-dessous, mais voici une autre prise de Martin Fowler qui discute des mérites de l'injection de dépendance par rapport à l'utilisation d'un localisateur de service (qui est essentiellement la même chose que d'appeler un wrappé ApplicationContext.getBean()).

En partie, Fowler déclare: " Avec le localisateur de service, la classe d'application le demande [le service] explicitement par un message au localisateur. Avec l'injection, il n'y a pas de demande explicite, le service apparaît dans la classe d'application - d'où l'inversion du contrôle. L'inversion de contrôle est une caractéristique courante des frameworks, mais c'est quelque chose qui a un prix. Elle a tendance à être difficile à comprendre et à poser des problèmes lorsque vous essayez de déboguer. Donc, dans l'ensemble, je préfère l'éviter [Inversion de contrôle ] sauf si j'en ai besoin. Cela ne veut pas dire que c'est une mauvaise chose, mais je pense que cela doit se justifier par rapport à l'alternative la plus simple. "

Vinnie
la source

Réponses:

202

Je l'ai mentionné dans un commentaire sur l'autre question, mais l'idée de l'inversion de contrôle est de faire en sorte qu'aucune de vos classes ne sache ou ne se soucie de la façon dont elle obtient les objets dont elle dépend . Cela permet de changer facilement le type d'implémentation d'une dépendance donnée que vous utilisez à tout moment. Cela rend également les classes faciles à tester, car vous pouvez fournir des implémentations simulées de dépendances. Enfin, cela rend les classes plus simples et plus axées sur leur responsabilité principale.

Appeler ApplicationContext.getBean()n'est pas une inversion de contrôle! Bien qu'il soit toujours facile de changer l'implémentation configurée pour le nom de bean donné, la classe s'appuie désormais directement sur Spring pour fournir cette dépendance et ne peut pas l'obtenir autrement. Vous ne pouvez pas simplement créer votre propre implémentation fictive dans une classe de test et la transmettre vous-même. Cela va à l'encontre du but de Spring en tant que conteneur d'injection de dépendance.

Partout où vous voulez dire:

MyClass myClass = applicationContext.getBean("myClass");

à la place, vous devez par exemple déclarer une méthode:

public void setMyClass(MyClass myClass) {
   this.myClass = myClass;
}

Et puis dans votre configuration:

<bean id="myClass" class="MyClass">...</bean>

<bean id="myOtherClass" class="MyOtherClass">
   <property name="myClass" ref="myClass"/>
</bean>

Le printemps s'injectera alors automatiquement myClassdans myOtherClass.

Déclarez tout de cette façon, et à la racine de tout cela a quelque chose comme:

<bean id="myApplication" class="MyApplication">
   <property name="myCentralClass" ref="myCentralClass"/>
   <property name="myOtherCentralClass" ref="myOtherCentralClass"/>
</bean>

MyApplicationest la classe la plus centrale et dépend au moins indirectement de tous les autres services de votre programme. Lors du démarrage, dans votre mainméthode, vous pouvez appeler applicationContext.getBean("myApplication")mais vous ne devriez pas avoir besoin d'appeler getBean()ailleurs!

ColinD
la source
3
Y a-t-il quelque chose en rapport avec cela qui fonctionne avec seulement des annotations lors de la création d'un new MyOtherClass()objet? Je connais @Autowired, mais je ne l'ai utilisé que sur des champs et il se décompose new MyOtherClass()..
Tim
71
Ce n'est pas vrai que ApplicationContext.getBean () n'est pas IoC. Niether est obligatoire d'avoir toutes vos classes instanciées au printemps. C'est un dogme inapproprié. Si l'ApplicationContext est lui-même injecté, il est tout à fait correct de lui demander d'instancier un bean de cette manière - et les beans qu'il crée peuvent être des implémentations différentes basées sur l'ApplicationContext initialement injecté. Par exemple, j'ai un scénario dans lequel je crée dynamiquement de nouvelles instances de bean sur la base d'un nom de bean qui est inconnu au moment de la compilation mais correspond à l'une des implémentations définies dans mon fichier spring.xml.
Alex Worden,
3
D'accord avec Alex, j'ai le même problème, où une classe d'usine ne saura que le bean ou l'implémentation à utiliser au moment de l'exécution via l'interaction avec l'utilisateur, je pense que c'est là que l'interface ContextAware entre en jeu
Ben
3
@elbek: applicationContext.getBeann'est pas une injection de dépendance: il accède directement au framework, l'utilise comme localisateur de service .
ColinD
6
@herman: Je ne connais pas Spring parce que je ne l'ai pas utilisé depuis longtemps, mais dans JSR-330 / Guice / Dagger, vous feriez cela en injectant un Provider<Foo>au lieu d'un Fooet en appelant provider.get()chaque fois que vous avez besoin d'un nouvelle instance. Aucune référence au conteneur lui-même, et vous pouvez facilement en créer un Providerpour le test.
ColinD
65

Les raisons de préférer Service Locator à Inversion of Control (IoC) sont:

  1. Service Locator est beaucoup, beaucoup plus facile pour les autres personnes à suivre votre code. L'IoC est «magique», mais les programmeurs de maintenance doivent comprendre vos configurations de Spring alambiquées et toute la myriade d'emplacements pour comprendre comment vous avez câblé vos objets.

  2. L'IoC est terrible pour le débogage des problèmes de configuration. Dans certaines classes d'applications, l'application ne démarre pas lorsqu'elle est mal configurée et vous risquez de ne pas pouvoir passer en revue ce qui se passe avec un débogueur.

  3. L'IoC est principalement basé sur XML (les annotations améliorent les choses mais il y a encore beaucoup de XML). Cela signifie que les développeurs ne peuvent pas travailler sur votre programme à moins qu'ils ne connaissent toutes les balises magiques définies par Spring. Il ne suffit plus de connaître Java. Cela gêne les programmeurs moins expérimentés (c'est-à-dire qu'il est en fait de mauvaise conception d'utiliser une solution plus compliquée lorsqu'une solution plus simple, comme Service Locator, répondra aux mêmes exigences). De plus, la prise en charge du diagnostic des problèmes XML est beaucoup plus faible que la prise en charge des problèmes Java.

  4. L'injection de dépendance est plus adaptée aux programmes plus importants. La plupart du temps, la complexité supplémentaire n'en vaut pas la peine.

  5. Souvent, Spring est utilisé au cas où vous "voudriez changer la mise en œuvre plus tard". Il existe d'autres moyens d'y parvenir sans la complexité de Spring IoC.

  6. Pour les applications Web (Java EE WARs), le contexte Spring est effectivement lié au moment de la compilation (sauf si vous voulez que les opérateurs explorent le contexte dans la guerre éclatée). Vous pouvez faire en sorte que Spring utilise les fichiers de propriétés, mais avec les servlets, les fichiers de propriétés devront se trouver à un emplacement prédéterminé, ce qui signifie que vous ne pouvez pas déployer plusieurs servlets en même temps sur la même boîte. Vous pouvez utiliser Spring avec JNDI pour modifier les propriétés au démarrage de la servlet, mais si vous utilisez JNDI pour des paramètres modifiables par l'administrateur, le besoin de Spring lui-même diminue (puisque JNDI est en fait un localisateur de service).

  7. Avec Spring, vous pouvez perdre le contrôle du programme si Spring se répartit dans vos méthodes. C'est pratique et fonctionne pour de nombreux types d'applications, mais pas pour toutes. Vous devrez peut-être contrôler le flux du programme lorsque vous devez créer des tâches (threads, etc.) pendant l'initialisation ou avoir besoin de ressources modifiables que Spring ignorait quand le contenu était lié à votre WAR.

Le printemps est très bon pour la gestion des transactions et présente certains avantages. C'est juste que l'IoC peut être une ingénierie excessive dans de nombreuses situations et introduire une complexité injustifiée pour les mainteneurs. N'utilisez pas automatiquement l'IoC sans penser à des moyens de ne pas l'utiliser en premier.

Moa
la source
7
De plus, votre ServiceLocator peut toujours utiliser l'IoC de Spring, évitant à votre code d'être dépendant de Spring, jonché d'annotations Spring et de magik indéchiffrable. J'ai récemment porté un tas de code sur GoogleAppEngine où Spring n'est pas pris en charge. J'aimerais avoir caché tout l'IoC derrière une ServiceFactory en premier lieu!
Alex Worden,
L'IoC encourage un modèle de domaine anémique, que je méprise. Les beans entité ont besoin d'un moyen de rechercher leurs services afin de pouvoir implémenter leur propre comportement. À cette couche, vous ne pouvez pas vraiment contourner le besoin d'un localisateur de services.
Joel
4
Bizar. J'utilise Spring TOUT le temps avec des annotations. Bien qu'en effet une certaine courbe d'apprentissage soit impliquée, maintenant, je n'ai aucun problème de maintenance, de débogage, de clarté, de lisibilité ... Je suppose que la façon dont vous structurez les choses est l'astuce.
Lawrence
25

Il est vrai que l'inclusion de la classe dans application-context.xml évite d'avoir à utiliser getBean. Cependant, même cela n'est en fait pas nécessaire. Si vous écrivez une application autonome et que vous NE VOULEZ PAS inclure votre classe de pilote dans application-context.xml, vous pouvez utiliser le code suivant pour que Spring transfère automatiquement les dépendances du pilote:

public class AutowireThisDriver {

    private MySpringBean mySpringBean;    

    public static void main(String[] args) {
       AutowireThisDriver atd = new AutowireThisDriver(); //get instance

       ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext(
                  "/WEB-INF/applicationContext.xml"); //get Spring context 

       //the magic: auto-wire the instance with all its dependencies:
       ctx.getAutowireCapableBeanFactory().autowireBeanProperties(atd,
                  AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE, true);        

       // code that uses mySpringBean ...
       mySpringBean.doStuff() // no need to instantiate - thanks to Spring
    }

    public void setMySpringBean(MySpringBean bean) {
       this.mySpringBean = bean;    
    }
}

J'ai dû le faire plusieurs fois lorsque j'ai une sorte de classe autonome qui doit utiliser un aspect de mon application (par exemple pour les tests) mais je ne veux pas l'inclure dans le contexte de l'application car ce n'est pas fait partie de l'application. Notez également que cela évite d'avoir à rechercher le bean en utilisant un nom de chaîne, ce que j'ai toujours trouvé laid.

iftheshoefritz
la source
J'ai également réussi à utiliser cette méthode avec l' @Autowiredannotation.
blong
21

L'un des avantages les plus intéressants de l'utilisation de quelque chose comme Spring est que vous n'avez pas à câbler vos objets ensemble. La tête de Zeus s'ouvre et vos classes apparaissent, entièrement formées avec toutes leurs dépendances créées et câblées, selon les besoins. C'est magique et fantastique.

Plus vous en dites ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, moins vous obtenez de magie. Moins de code est presque toujours meilleur. Si votre classe avait vraiment besoin d'un bean ClassINeed, pourquoi ne l'avez-vous pas simplement connecté?

Cela dit, quelque chose doit évidemment créer le premier objet. Il n'y a rien de mal à ce que votre méthode principale acquière un bean ou deux via getBean (), mais vous devriez l'éviter car chaque fois que vous l'utilisez, vous n'utilisez pas vraiment toute la magie de Spring.

Brandon Yarbrough
la source
1
Mais l'OP ne dit pas "ClassINeed", il dit "BeanNameINeed" - ce qui permet au conteneur IoC de créer une instance sur n'importe quelle classe configurée de quelque manière que ce soit. Peut-être ressemble-t-il davantage au modèle «localisateur de services» qu'à l'IoC, mais il en résulte toujours un couplage lâche.
HDave
16

La motivation est d'écrire du code qui ne dépend pas explicitement de Spring. De cette façon, si vous choisissez de changer de conteneur, vous n'avez pas à réécrire de code.

Considérez le conteneur comme quelque chose d'invisible pour votre code, répondant par magie à ses besoins, sans qu'on le lui demande.

L'injection de dépendances est un contrepoint au modèle "localisateur de services". Si vous allez rechercher des dépendances par nom, vous pourriez aussi bien vous débarrasser du conteneur DI et utiliser quelque chose comme JNDI.

erickson
la source
11

Utiliser @Autowiredou ApplicationContext.getBean()c'est vraiment la même chose. Dans les deux cas, vous obtenez le bean configuré dans votre contexte et dans les deux cas, votre code dépend du printemps. La seule chose à éviter est d'instancier votre ApplicationContext. Faites cela une seule fois! En d'autres termes, une ligne comme

ApplicationContext context = new ClassPathXmlApplicationContext("AppContext.xml");

ne doit être utilisé qu'une seule fois dans votre application.

easyplanner.cu.cc
la source
Nan. Parfois, @Autowired ou ApplicationContext.getBean () peuvent produire des beans totalement différents. Je ne sais pas comment cela s'est produit, mais je lutte actuellement avec ce problème.
Oleksandr_DJ
4

L'idée est que vous comptez sur l'injection de dépendances ( inversion de contrôle , ou IoC). Autrement dit, vos composants sont configurés avec les composants dont ils ont besoin. Ces dépendances sont injectées (via le constructeur ou les setters) - vous n'obtenez pas alors vous-même.

ApplicationContext.getBean()vous oblige à nommer un bean explicitement dans votre composant. Au lieu de cela, en utilisant IoC, votre configuration peut déterminer quel composant sera utilisé.

Cela vous permet de recâbler facilement votre application avec différentes implémentations de composants, ou de configurer des objets pour les tests de manière simple en fournissant des variantes simulées (par exemple, un DAO simulé afin de ne pas toucher une base de données pendant les tests)

Brian Agnew
la source
4

D'autres ont souligné le problème général (et sont des réponses valables), mais je vais juste faire un commentaire supplémentaire: ce n'est pas que vous ne devriez JAMAIS le faire, mais plutôt que vous le fassiez le moins possible.

Cela signifie généralement que cela se fait exactement une fois: lors du démarrage. Et puis c'est juste pour accéder au bean "root", à travers lequel d'autres dépendances peuvent être résolues. Il peut s'agir de code réutilisable, comme le servlet de base (si vous développez des applications Web).

StaxMan
la source
4

L'une des prémisses de Spring est d'éviter le couplage . Définissez et utilisez des interfaces, DI, AOP et évitez d'utiliser ApplicationContext.getBean () :-)

sourcerebels
la source
4

L'une des raisons est la testabilité. Disons que vous avez cette classe:

interface HttpLoader {
    String load(String url);
}
interface StringOutput {
    void print(String txt);
}
@Component
class MyBean {
    @Autowired
    MyBean(HttpLoader loader, StringOutput out) {
        out.print(loader.load("http://stackoverflow.com"));
    }
}

Comment pouvez-vous tester ce bean? Par exemple, comme ceci:

class MyBeanTest {
    public void creatingMyBean_writesStackoverflowPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();

        // execution
        new MyBean(Collections.singletonMap("https://stackoverflow.com", stackOverflowHtml)::get, result::append);

        // evaluation
        assertEquals(result.toString(), stackOverflowHtml);
    }
}

Facile, non?

Bien que vous dépendiez toujours de Spring (en raison des annotations), vous pouvez supprimer votre dépendance à Spring sans modifier aucun code (uniquement les définitions d'annotations) et le développeur de test n'a besoin de rien savoir sur le fonctionnement de Spring (peut-être qu'il devrait de toute façon, mais il permet de revoir et de tester le code séparément de ce que fait le ressort).

Il est toujours possible de faire de même lors de l'utilisation d'ApplicationContext. Cependant, vous devez vous moquer de ApplicationContextce qui est une énorme interface. Vous avez besoin d'une implémentation factice ou vous pouvez utiliser un cadre de simulation tel que Mockito:

@Component
class MyBean {
    @Autowired
    MyBean(ApplicationContext context) {
        HttpLoader loader = context.getBean(HttpLoader.class);
        StringOutput out = context.getBean(StringOutput.class);

        out.print(loader.load("http://stackoverflow.com"));
    }
}
class MyBeanTest {
    public void creatingMyBean_writesStackoverflowPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();
        ApplicationContext context = Mockito.mock(ApplicationContext.class);
        Mockito.when(context.getBean(HttpLoader.class))
            .thenReturn(Collections.singletonMap("https://stackoverflow.com", stackOverflowHtml)::get);
        Mockito.when(context.getBean(StringOutput.class)).thenReturn(result::append);

        // execution
        new MyBean(context);

        // evaluation
        assertEquals(result.toString(), stackOverflowHtml);
    }
}

C'est tout à fait une possibilité, mais je pense que la plupart des gens conviendraient que la première option est plus élégante et rend le test plus simple.

La seule option qui pose vraiment problème est celle-ci:

@Component
class MyBean {
    @Autowired
    MyBean(StringOutput out) {
        out.print(new HttpLoader().load("http://stackoverflow.com"));
    }
}

Tester cela nécessite des efforts énormes ou votre bean va tenter de se connecter à stackoverflow à chaque test. Et dès que vous avez une panne de réseau (ou que les administrateurs de stackoverflow vous bloquent en raison d'un taux d'accès excessif), vous aurez des tests qui échouent au hasard.

Donc, en guise de conclusion, je ne dirais pas que l'utilisation ApplicationContextdirecte est automatiquement erronée et doit être évitée à tout prix. Cependant, s'il existe de meilleures options (et il y en a dans la plupart des cas), utilisez les meilleures options.

Yankee
la source
3

Je n'ai trouvé que deux situations où getBean () était requis:

D'autres ont mentionné l'utilisation de getBean () dans main () pour récupérer le bean "principal" pour un programme autonome.

Une autre utilisation que j'ai faite de getBean () est dans des situations où une configuration utilisateur interactive détermine la composition du bean pour une situation particulière. Ainsi, par exemple, une partie du système de démarrage passe en boucle dans une table de base de données en utilisant getBean () avec une définition de bean scope = 'prototype', puis en définissant des propriétés supplémentaires. Vraisemblablement, il existe une interface utilisateur qui ajuste la table de base de données qui serait plus conviviale que de tenter de (ré) écrire le XML du contexte de l'application.

nsayer
la source
3

Il y a un autre moment où l'utilisation de getBean est logique. Si vous reconfigurez un système qui existe déjà, où les dépendances ne sont pas explicitement appelées dans les fichiers de contexte de Spring. Vous pouvez démarrer le processus en envoyant des appels à getBean, afin de ne pas avoir à tout câbler en même temps. De cette façon, vous pouvez lentement construire votre configuration de ressort en mettant chaque pièce en place au fil du temps et en alignant correctement les mèches. Les appels à getBean seront éventuellement remplacés, mais si vous comprenez la structure du code, ou si vous n'en avez pas, vous pouvez démarrer le processus de câblage de plus en plus de beans et d'utiliser de moins en moins d'appels pour getBean.

Tony Giaccone
la source
2

cependant, il existe encore des cas où vous avez besoin du modèle de localisateur de service. par exemple, j'ai un bean de contrôleur, ce contrôleur peut avoir des beans de service par défaut, qui peuvent être des dépendances injectées par la configuration. Bien qu'il puisse également y avoir de nombreux services supplémentaires ou nouveaux que ce contrôleur peut appeler maintenant ou plus tard, qui ont ensuite besoin du localisateur de services pour récupérer les beans de service.

lwpro2
la source
0

Vous devez utiliser: ConfigurableApplicationContext au lieu de pour ApplicationContext

Hai Nguyen Le
la source