Warum wird Spring ApplicationContext.getBean als schlecht angesehen?

Ich stellte eine allgemeine Spring-Frage: Auto-Cast Spring Beans und hatten mehrere Leute antworten, dass Aufruf von Spring ApplicationContext.getBean() sollte so viel wie möglich vermieden werden. Warum das?

Wie sonst sollte ich Zugriff auf die Beans erhalten, die ich Spring zum Erstellen konfiguriert habe?

Ich verwende Spring in einer Nicht-Webanwendung und hatte geplant, auf ein gemeinsam genutztes ApplicationContext Objekt zuzugreifen, wie von LiorH beschrieben .

Änderung

Ich akzeptiere die Antwort unten, aber hier ist ein alternativer Ansatz von Martin Fowler, der die Vorzüge von Dependency Injection gegenüber der Verwendung eines Service Locator (was im Wesentlichen dasselbe ist wie das Aufrufen eines umhüllten ApplicationContext.getBean() ) erläutert .

Fowler stellt zum Teil fest: ” Mit Service Locator fragt die Anwendungsklasse explizit nach [dem Service] durch eine Nachricht an den Locator. Bei der Injektion gibt es keine explizite Anfrage, der Service erscheint in der Anwendungsklasse – daher die Inversion der Kontrolle. Inversion of Control ist ein häufiges Merkmal von Frameworks, aber es hat seinen Preis, es ist schwer zu verstehen und führt zu Problemen beim Debuggen, also im Allgemeinen bevorzuge ich es zu vermeiden [Inversion of Control ] Es sei denn, dass es eine schlechte Sache ist, nur dass ich denke, dass es sich über die einfachere Alternative rechtfertigen muss.

Ich erwähnte dies in einem Kommentar zu der anderen Frage, aber die ganze Idee von Inversion of Control besteht darin, dass keiner Ihrer classn weiß oder sich darum kümmert, wie sie die Objekte bekommen, von denen sie abhängen . Auf diese Weise können Sie die Art der Implementierung einer bestimmten Abhängigkeit, die Sie verwenden, jederzeit ändern. Außerdem können die classn leicht getestet werden, da Sie Mock-Implementierungen von Abhängigkeiten bereitstellen können. Schließlich macht es die classn einfacher und konzentrierter auf ihre coreverantwortung.

Das Aufrufen von ApplicationContext.getBean() ist keine Inversion der Kontrolle! Es ist zwar immer noch leicht zu ändern, welche Implementierung für den angegebenen Bean-Namen konfiguriert wurde, aber die class stützt sich jetzt direkt auf Spring, um diese Abhängigkeit bereitzustellen, und kann sie nicht anders erhalten. Sie können nicht einfach eine eigene Implementation in einer Testklasse erstellen und diese selbst an sie übergeben. Dies vereitelt grundsätzlich den Zweck von Spring als Dependency-Injection-Container.

Überall, wo du sagen willst:

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

Sie sollten stattdessen zum Beispiel eine Methode deklarieren:

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

Und dann in deiner Konfiguration:

 ...    

Spring wird myClass automatisch in myOtherClass .

Erklären Sie alles auf diese Art und an der Wurzel von allem:

     

MyApplication ist die zentralste class und hängt zumindest indirekt von jedem anderen Dienst in Ihrem Programm ab. Beim Bootstrapping können Sie in Ihrer main applicationContext.getBean("myApplication") aber Sie sollten getBean() nirgendwo anders aufrufen getBean() !

Gründe, Service Locator gegenüber Inversion of Control (IoC) vorzuziehen, sind:

  1. Service Locator ist viel, viel einfacher für andere Menschen in Ihrem Code zu folgen. IoC ist “magisch”, aber Wartungsprogrammierer müssen Ihre gewundenen Spring-Konfigurationen und all die unzähligen Orte verstehen, um herauszufinden, wie Sie Ihre Objekte verdrahtet haben.

  2. IoC ist für das Debuggen von Konfigurationsproblemen schrecklich. In bestimmten classn von Anwendungen wird die Anwendung nicht gestartet, wenn sie falsch konfiguriert ist und Sie möglicherweise nicht die Möglichkeit haben, die Schritte mit einem Debugger zu durchlaufen.

  3. IoC ist in erster Linie XML-basiert (Anmerkungen verbessern Dinge, aber es gibt immer noch viel XML draußen). Das bedeutet, dass Entwickler an Ihrem Programm nicht arbeiten können, wenn sie nicht alle von Spring definierten magischen Tags kennen. Es ist nicht gut genug, Java mehr zu kennen. Dies behindert weniger erfahrene Programmierer (dh es ist tatsächlich ein schlechtes Design, eine kompliziertere Lösung zu verwenden, wenn eine einfachere Lösung, wie zum Beispiel Service Locator, die gleichen Anforderungen erfüllt). Außerdem ist die Unterstützung für die Diagnose von XML-Problemen viel schwächer als die Unterstützung für Java-Probleme.

  4. Abhängigkeitsinjektion ist für größere Programme besser geeignet. Meistens lohnt sich die zusätzliche Komplexität nicht.

  5. Häufig wird Spring verwendet, falls Sie die Implementierung später ändern möchten. Es gibt andere Wege, dies ohne die Komplexität von Spring IoC zu erreichen.

  6. Bei Webanwendungen (Java EE WARs) wird der Spring-Kontext zur Kompilierungszeit effektiv gebunden (es sei denn, Sie möchten, dass Operatoren den Kontext im explodierten Krieg gruppieren). Sie können Spring veranlassen, Property-Dateien zu verwenden, aber die Property-Dateien von Servlets müssen sich an einem vorher festgelegten Speicherort befinden, was bedeutet, dass Sie nicht mehrere Servlets derselben Zeit auf derselben Box bereitstellen können. Sie können Spring mit JNDI verwenden, um Eigenschaften zum Zeitpunkt des Servlet-Starts zu ändern. Wenn Sie jedoch JNDI für vom Administrator änderbare Parameter verwenden, verringert sich die Notwendigkeit für Spring selbst (da JNDI effektiv ein Service Locator ist).

  7. Mit Spring können Sie das Programm Control verlieren, wenn Spring zu Ihren Methoden versendet. Dies ist praktisch und funktioniert für viele Arten von Anwendungen, aber nicht für alle. Möglicherweise müssen Sie den Programmablauf steuern, wenn Sie während der Initialisierung Aufgaben (Threads usw.) erstellen müssen, oder wenn Sie modifizierbare Ressourcen benötigen, von denen Spring nicht wusste, wann der Inhalt an Ihre WAR gebunden war.

Der Frühling ist sehr gut für das Transaktionsmanagement und hat einige Vorteile. Es ist nur so, dass IoC in vielen Situationen übertrieben sein kann und den Betreuern ungerechtfertigte Komplexität einbringen kann. Verwenden Sie IoC nicht automatisch, ohne darüber nachzudenken, wie Sie es nicht zuerst verwenden können.

Es stimmt, dass die Verwendung der class in application-context.xml die Verwendung von getBean überflüssig macht. Aber auch das ist eigentlich unnötig. Wenn Sie eine eigenständige Anwendung schreiben und Ihre Treiberklasse NICHT in application-context.xml einschließen möchten, können Sie den folgenden Code verwenden, damit Spring die Abhängigkeiten des Treibers automatisch aufruft:

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

Ich musste dies einige Male tun, wenn ich eine eigenständige class habe, die einen Aspekt meiner App verwenden muss (zB zum Testen), aber ich möchte sie nicht in den Anwendungskontext einbeziehen, weil dies nicht der Fall ist eigentlich Teil der App. Beachten Sie auch, dass dies die Notwendigkeit vermeidet, die Bean mithilfe eines String-Namens nachzuschlagen, was ich immer für hässlich gehalten habe.

Einer der coolsten Vorteile von etwas wie Spring ist, dass Sie Ihre Objekte nicht miteinander verkabeln müssen. Zeus ‘Kopf spaltet sich auf und deine classn erscheinen, vollständig gebildet mit allen Abhängigkeiten, erstellt und verdrahtet, je nach Bedarf. Es ist magisch und fantastisch.

Je mehr Sie sagen, ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed"); Je weniger Magie du bekommst. Weniger Code ist fast immer besser. Wenn Ihre class wirklich eine ClassINeed-Bean benötigt, warum haben Sie sie nicht einfach verdrahtet?

Das heißt, etwas muss offensichtlich das erste Objekt erstellen. Es ist nichts falsch daran, dass Ihre Hauptmethode über beanBean () eine oder zwei Bohnen kauft, aber Sie sollten es vermeiden, denn wenn Sie es verwenden, nutzen Sie nicht wirklich die gesamte Magie von Spring.

Die Motivation ist, Code zu schreiben, der nicht explizit von Spring abhängt. Auf diese Weise müssen Sie, wenn Sie Container wechseln, keinen Code neu schreiben.

Stellen Sie sich den Container so vor, als ob etwas für Ihren Code unsichtbar wäre, und sorgt auf magische Weise für seine Bedürfnisse, ohne gefragt zu werden.

Abhängigkeitsinjektion ist ein Kontrapunkt zum Muster “Service Locator”. Wenn Sie Abhängigkeiten nach Namen suchen, können Sie auch den DI-Container loswerden und so etwas wie JNDI verwenden.

Die Verwendung von @Autowired oder ApplicationContext.getBean() ist wirklich das Gleiche. In beiden Fällen erhalten Sie die Bean, die in Ihrem Kontext konfiguriert ist, und in beiden Fällen hängt Ihr Code vom Frühling ab. Das einzige, was Sie vermeiden sollten, ist die Instantiierung Ihres ApplicationContext. Mach das nur einmal! Mit anderen Worten, eine Zeile wie

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

sollte nur einmal in Ihrer Anwendung verwendet werden.

Die Idee ist, dass Sie auf Abhängigkeitsinjektion angewiesen sind ( Inversion of Control oder IoC). Das heißt, Ihre Komponenten sind mit den Komponenten konfiguriert, die sie benötigen. Diese Abhängigkeiten werden injiziert (über den Konstruktor oder die Setter) – Sie bekommen es dann nicht selbst.

ApplicationContext.getBean() erfordert, dass Sie eine Bean explizit in Ihrer Komponente benennen. Stattdessen kann Ihre Konfiguration mithilfe von IoC bestimmen, welche Komponente verwendet wird.

Dies ermöglicht es Ihnen, Ihre Anwendung einfach mit verschiedenen Komponentenimplementierungen neu zu verkabeln oder Objekte zum Testen auf unkomplizierte Weise zu konfigurieren, indem Sie mockige Varianten bereitstellen (z. B. ein verspottetes DAO, so dass Sie während des Tests keine database treffen).

Andere haben auf das allgemeine Problem hingewiesen (und sind gültige Antworten), aber ich werde nur einen zusätzlichen Kommentar geben: Es ist nicht so, dass du es NIEMALS tun solltest, sondern dass du es so wenig wie möglich machst.

Normalerweise bedeutet dies, dass es genau einmal gemacht wird: während des Bootstrappings. Und dann ist es nur, um auf die “Root” -Bohne zuzugreifen, durch die andere Abhängigkeiten aufgetriggers werden können. Dies kann wiederverwendbarer Code sein, wie das Basis-Servlet (wenn Web-Apps entwickelt werden).

Ich habe nur zwei Situationen gefunden, in denen getBean () benötigt wurde:

Andere haben erwähnt, dass getBean () in main () verwendet wird, um die Bean “main” für ein eigenständiges Programm zu holen.

Eine weitere Verwendung, die ich von getBean () gemacht habe, sind Situationen, in denen eine interaktive Benutzerkonfiguration das Bean-Makeup für eine bestimmte Situation bestimmt. So durchläuft zum Beispiel ein Teil des Boot-Systems eine databasetabelle unter Verwendung von getBean () mit einer Bean-Definition scope = ‘Prototyp’ und setzt dann zusätzliche Eigenschaften. Vermutlich gibt es eine Benutzeroberfläche, die die databasetabelle anpasst, die freundlicher wäre als der Versuch, die Anwendungskontext-XML (neu) zu schreiben.

Eine der Voraussetzungen von Spring ist die Vermeidung von Kopplungen . Definieren und verwenden Sie Interfaces, DI, AOP und vermeiden Sie ApplicationContext.getBean () 🙂

Es gibt jedoch noch Fälle, in denen Sie das Service-Locator-Muster benötigen. Zum Beispiel, ich habe eine Controller-Bean, dieser Controller möglicherweise einige Standard-Service-Beans, die Abhängigkeit von der Konfiguration injiziert werden kann. Es kann auch viele zusätzliche oder neue Dienste geben, die dieser Controller jetzt oder später aufrufen kann und die dann den Service-Locator zum Abrufen der Service-Beans benötigen.

Es gibt eine andere Zeit, wenn getBean sinnvoll ist. Wenn Sie ein bereits vorhandenes System neu konfigurieren, werden die Abhängigkeiten nicht explizit in Spring-Kontextdateien aufgerufen. Sie können den process starten, indem Sie Aufrufe von getBean eingeben, sodass Sie nicht alle gleichzeitig verbinden müssen. Auf diese Weise können Sie langsam Ihre Federkonfiguration aufbauen, indem Sie jedes Teil im Laufe der Zeit an Ort und Stelle setzen und die Teile richtig aufgereiht bekommen. Die Aufrufe von getBean werden schließlich ersetzt, aber wenn Sie die Struktur des Codes verstehen oder nicht, können Sie den process der Verkabelung von immer mehr Beans starten und immer weniger Aufrufe von getBean verwenden.

Einer der Gründe ist die Testbarkeit. Angenommen, Sie haben diese class:

 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")); } } 

Wie können Sie diese Bohne testen? ZB so:

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

Einfach richtig?

Während Sie immer noch auf Spring angewiesen sind (aufgrund der Annotationen), können Sie Ihre Abhängigkeit vom Frühling entfernen, ohne irgendeinen Code zu ändern (nur die Annotationsdefinitionen) und der Testentwickler muss nichts über den Frühling wissen (vielleicht sollte er es trotzdem, aber es ermöglicht, den Code separat zu überprüfen und zu testen, was der Frühling tut).

Es ist immer noch möglich, dasselbe zu tun, wenn Sie den ApplicationContext verwenden. Aber dann müssen Sie ApplicationContext vortäuschen, die eine riesige Schnittstelle ist. Sie benötigen entweder eine Dummy-Implementierung oder Sie können ein spöttisches Framework wie Mockito verwenden:

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

Dies ist durchaus möglich, aber ich denke, die meisten Leute würden zustimmen, dass die erste Option eleganter ist und den Test einfacher macht.

Die einzige Option, die wirklich ein Problem ist, ist diese:

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

Dies zu testen erfordert große Anstrengungen oder Ihr Bean wird versuchen, bei jedem Test eine Verbindung zu stackoverflow herzustellen. Und sobald Sie einen Netzwerkausfall haben (oder die Admins bei stackoverflow blockieren Sie wegen zu hoher Zugriffsrate), werden Sie zufällig versagende Tests haben.

Als Schlussfolgerung würde ich nicht sagen, dass die direkte Verwendung des ApplicationContext automatisch falsch ist und um jeden Preis vermieden werden sollte. Wenn es jedoch bessere Optionen gibt (und in den meisten Fällen gibt es), dann verwenden Sie die besseren Optionen.