J'ai posé une question générale sur Spring: Beans Spring à diffusion automatique et plusieurs personnes ont répondu qu'il était préférable d'éviter d'appeler ApplicationContext.getBean()
de Spring. Pourquoi donc?
Sinon, comment devrais-je accéder aux beans que Spring a configurés pour créer?
J'utilise Spring dans une application non Web et j'avais prévu d'accéder à un objet ApplicationContext
partagé comme décrit par LiorH .
Amendement
J'accepte la réponse ci-dessous, mais voici une autre solution prise par Martin Fowler qui explique les avantages de l'injection de dépendance par rapport à l'utilisation d'un localisateur de service (ce qui revient essentiellement à appeler un ApplicationContext.getBean()
enveloppé) .
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 applicative. - d’où l’inversion du contrôle. L’inversion du contrôle est une caractéristique commune des frameworks, mais c’est quelque chose qui a un prix. Elle a tendance à être difficile à comprendre et à créer des problèmes lorsque vous essayez de déboguer. pour l'éviter [Inversion of Control] à moins que j'en ai besoin. Cela ne veut pas dire que c'est une mauvaise chose, mais que je pense qu'il doit se justifier par une alternative plus simple. "
Je l’ai mentionné dans un commentaire sur l’autre question, mais l’idée même de l’inversion du contrôle est d’avoir aucune de vos classes ne sait ni ne se soucie de savoir comment elles obtiennent les objets dont elles dépendent. Cela facilite le changement du type d'implémentation d'une dépendance donnée que vous utilisez à tout moment. Cela facilite également le test des classes, car vous pouvez fournir une implémentation factice de dépendances. Enfin, cela rend les classes plus simples et plus centrées sur leur responsabilité première.
L'appel de ApplicationContext.getBean()
n'est pas une inversion de contrôle! Bien qu'il soit encore facile de changer le type d'implémentation configuré pour le nom de bean donné, la classe s'appuie désormais directement sur Spring pour fournir cette dépendance et ne peut l'obtenir autrement. Vous ne pouvez pas simplement créer votre propre implémentation factice dans une classe de test et la lui transmettre vous-même. Cela détruit fondamentalement l'objectif de Spring en tant que conteneur d'injection de dépendance.
Partout où vous voulez dire:
MyClass myClass = applicationContext.getBean("myClass");
vous devriez plutôt, 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>
Spring injectera alors automatiquement myClass
dans myOtherClass
.
Déclarez tout de cette façon, et à la base de tout cela a quelque chose comme:
<bean id="myApplication" class="MyApplication">
<property name="myCentralClass" ref="myCentralClass"/>
<property name="myOtherCentralClass" ref="myOtherCentralClass"/>
</bean>
MyApplication
est 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 méthode main
, vous pouvez appeler applicationContext.getBean("myApplication")
mais vous ne devriez pas avoir à appeler getBean()
ailleurs!
Les raisons de préférer Service Locator à Inversion of Control (IoC) sont les suivantes:
Service Locator est beaucoup, beaucoup plus facile à suivre pour votre code. IoC est "magique", mais les programmeurs de maintenance doivent comprendre vos configurations compliquées de Spring et toute la myriade d'emplacements pour comprendre comment vous avez câblé vos objets.
IoC est terrible pour le débogage des problèmes de configuration. Dans certaines classes d'applications, l'application ne démarrera pas si elle est mal configurée et vous n'aurez peut-être pas la chance de voir ce qui se passe avec un débogueur.
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 s'ils ne connaissent pas toutes les balises magiques définies par Spring. Il ne suffit pas de connaître Java. Cela gêne les programmeurs moins expérimentés (par exemple, le fait d'utiliser une solution plus compliquée est en fait d'une conception médiocre lorsqu'une solution plus simple, telle que 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 celle des problèmes Java.
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.
Souvent, Spring est utilisé dans le cas où vous "voudriez changer d'implémentation plus tard". Il existe d'autres moyens d'y parvenir sans la complexité de Spring IoC.
Pour les applications Web (Java EE WARs), le contexte Spring est effectivement lié lors de la compilation (à moins que vous ne souhaitiez que les opérateurs analysent le contexte de la guerre éclatée). Vous pouvez créer des fichiers de propriétés Spring, mais avec les servlets, ils devront se trouver à un emplacement prédéterminé, ce qui signifie que vous ne pouvez pas déployer plusieurs servlets de la même heure sur la même boîte. Vous pouvez utiliser Spring avec JNDI pour modifier les propriétés au moment du démarrage du servlet. Toutefois, si vous utilisez JNDI pour des paramètres modifiables par l'administrateur, la nécessité de Spring lui-même diminue (JNDI étant en fait un localisateur de service).
Avec Spring, vous pouvez perdre le contrôle du programme si Spring envoie vos méthodes. C'est pratique et convient à de nombreux types d'applications, mais pas à toutes. Vous devrez peut-être contrôler le déroulement du programme lorsque vous devez créer des tâches (threads, etc.) lors de l'initialisation ou que vous avez besoin de ressources modifiables que Spring ne connaissait pas lorsque le contenu était lié à votre fichier WAR.
Le printemps est très bon pour la gestion des transactions et présente certains avantages. C’est juste que IoC peut être trop technique dans de nombreuses situations et créer une complexité injustifiée pour les responsables de la maintenance. N'utilisez pas automatiquement IoC sans penser à des moyens de ne pas l'utiliser au préalable.
Il est vrai qu'inclure la classe dans application-context.xml évite d'avoir à utiliser getBean. Cependant, même cela est en fait inutile. Si vous écrivez une application autonome et que vous NE souhaitez PAS inclure votre classe de pilote dans application-context.xml, vous pouvez utiliser le code suivant pour que Spring mette 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 eu besoin de le faire plusieurs fois lorsque j'ai une sorte de classe autonome qui doit utiliser certains aspects de mon application (par exemple, pour les tests), mais je ne souhaite pas l'inclure dans le contexte d'application, car ce n'est pas le cas. fait partie de l'application. Notez également que cela évite de rechercher le haricot en utilisant un nom de chaîne, ce que j'ai toujours pensé être moche.
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 connecté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 préférable. Si votre classe avait vraiment besoin d'un haricot ClassINeed, pourquoi ne l'avez-vous pas câblé?
Cela dit, il faut évidemment que le premier objet soit créé. Il n’ya rien de mal à ce que votre méthode principale obtienne un ou deux beans via getBean (), mais évitez-le car, chaque fois que vous l’utilisez, vous n’utilisez pas vraiment toute la magie du printemps.
La motivation est d'écrire du code qui ne dépend pas explicitement de Spring. Ainsi, si vous choisissez de changer de conteneur, vous n'avez pas à réécrire le code.
Pensez au conteneur comme étant quelque chose d’invisible pour votre code, répondant magiquement à ses besoins, sans que vous en demandiez.
L'injection de dépendance est un contrepoint au motif "localisateur de service". Si vous recherchez des dépendances par nom, vous pouvez également vous débarrasser du conteneur DI et utiliser quelque chose comme JNDI.
Utiliser @Autowired
ou ApplicationContext.getBean()
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 que vous devriez éviter, c'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.
Un des locaux de Spring est à éviter couplage . Définissez et utilisez les interfaces, DI, AOP et évitez d’utiliser ApplicationContext.getBean () :-)
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 haricot? Par exemple. comme ça:
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 (à cause des annotations), vous pouvez supprimer votre dépendance à Spring sans modifier le code (uniquement les définitions d'annotation) et le développeur de test n'a pas besoin de savoir comment Spring fonctionne (peut-être qu'il devrait le faire de toute façon). cela permet de réviser et de tester le code séparément de ce que fait Spring).
Il est toujours possible de faire la même chose avec ApplicationContext. Cependant, alors vous devez vous moquer de ApplicationContext
qui est une interface énorme. Vous avez besoin d'une implémentation factice ou vous pouvez utiliser un framework moqueur 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 une possibilité, mais je pense que la plupart des gens s'accorderont pour dire que la première option est plus élégante et simplifie le test.
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"));
}
}
Cela nécessite d'énormes efforts, sinon votre bean tentera de se connecter à stackoverflow à chaque test. Et dès que vous rencontrez une défaillance du réseau (ou que les administrateurs de stackoverflow vous bloquent en raison d'un taux d'accès excessif), vous aurez des tests échoués de manière aléatoire.
Donc, pour conclure, je ne dirais pas que l’utilisation directe de la ApplicationContext
est automatiquement fausse et doit être évitée à tout prix. Cependant, s'il existe de meilleures options (et dans la plupart des cas), utilisez les meilleures.
L'idée est que vous vous fiez à l'injection de dépendance ( inversion du 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 reconnecter facilement votre application avec différentes implémentations de composants ou de configurer des objets à tester de manière simple en fournissant des variantes simulées (par exemple, une DAO simulée de manière à ne pas frapper une base de données pendant les tests).
D'autres ont souligné le problème général (et constituent des réponses valables), mais je ferai simplement un commentaire supplémentaire: ce n'est pas que vous ne devriez JAMAIS le faire, mais plutôt le faire le moins possible.
Cela signifie généralement que cela se fait exactement une fois: pendant l'amorçage. Et ensuite, il suffit d'accéder au bean "racine", à travers lequel d'autres dépendances peuvent être résolues. Cela peut être un code réutilisable, comme une servlet de base (si vous développez des applications Web).
J'ai seulement trouvé deux situations où getBean () était requis:
D'autres ont mentionné l'utilisation de getBean () dans main () pour extraire le bean "principal" d'un programme autonome.
Une autre utilisation de getBean () que je fais est dans les situations où une configuration utilisateur interactive détermine la constitution du bean pour une situation particulière. Ainsi, par exemple, une partie du système d’amorçage parcourt 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 d'essayer de (ré) écrire le XML de contexte d'application.
Il y a un autre moment où utiliser getBean est logique. Si vous reconfigurez un système qui existe déjà, les dépendances ne sont pas explicitement appelées dans les fichiers de contexte Spring. Vous pouvez démarrer le processus en plaçant des appels à getBean, de sorte que vous n'ayez pas à tout connecter en même temps. De cette façon, vous pouvez construire lentement votre configuration de ressort en mettant chaque pièce en place au fil du temps et en alignant correctement les embouts. Les appels à getBean seront éventuellement remplacés, mais si vous comprenez la structure du code ou si vous en manquez, vous pouvez lancer le processus de câblage de plus en plus de beans et utiliser de moins en moins d'appels pour getBean.
cependant, il existe encore des cas où vous avez besoin du modèle de localisateur de service. Par exemple, j'ai un bean controller, ce contrôleur peut avoir des beans de service par défaut, qui peuvent être une dépendance injectée par la configuration. Bien qu'il puisse également y avoir de nombreux services supplémentaires ou nouveaux, ce contrôleur peut invoquer maintenant ou ultérieurement, ce qui nécessite le localisateur de service pour récupérer les beans de service.