Warum wird Spring's ApplicationContext.getBean als schlecht angesehen?

270

Ich stellte eine allgemeine Spring-Frage: Spring Beans automatisch gegossen und mehrere Personen antworteten, dass das Anrufen von Spring's ApplicationContext.getBean()so weit wie möglich vermieden werden sollte. Warum ist das so?

Wie kann ich sonst auf die Beans zugreifen, für deren Erstellung ich Spring konfiguriert habe?

Ich verwende Spring in einer Nicht-Webanwendung und hatte geplant, auf ein freigegebenes ApplicationContextObjekt zuzugreifen, wie von LiorH beschrieben .

Änderung

Ich akzeptiere die Antwort unten, aber hier ist eine alternative Einstellung von Martin Fowler, der die Vorzüge der Abhängigkeitsinjektion im Vergleich zur Verwendung eines Service Locator (der im Wesentlichen dem Aufrufen eines Wraps entspricht ApplicationContext.getBean()) erläutert .

Zum Teil stellt Fowler fest: "Beim Service Locator fordert die Anwendungsklasse [den Service] explizit durch eine Nachricht an den Locator an. Bei der Injektion erfolgt keine explizite Anforderung, der Service wird in der Anwendungsklasse angezeigt - daher die Umkehrung der Steuerung." Die Inversion der Steuerung ist ein häufiges Merkmal von Frameworks, hat jedoch ihren Preis. Sie ist in der Regel schwer zu verstehen und führt zu Problemen beim Debuggen. Im Großen und Ganzen vermeide ich sie daher [Inversion der Steuerung ] es sei denn, ich brauche es. Das soll nicht heißen, dass es eine schlechte Sache ist, nur dass ich denke, es muss sich über die einfachere Alternative rechtfertigen. "

Vinnie
quelle

Antworten:

202

Ich habe dies in einem Kommentar zu der anderen Frage erwähnt, aber die ganze Idee von Inversion of Control besteht darin, dass keine Ihrer Klassen weiß oder sich darum kümmert, wie sie die Objekte erhalten, von denen sie abhängen . Auf diese Weise können Sie jederzeit leicht ändern, welche Art der Implementierung einer bestimmten Abhängigkeit Sie verwenden. Außerdem können die Klassen einfach getestet werden, da Sie Scheinimplementierungen von Abhängigkeiten bereitstellen können. Schließlich werden die Klassen einfacher und konzentrieren sich mehr auf ihre Kernverantwortung.

Anrufen ApplicationContext.getBean()ist keine Umkehrung der Kontrolle! Während es immer noch einfach ist zu ändern, welche Implementierung für den angegebenen Bean-Namen konfiguriert ist, verlässt sich die Klasse jetzt direkt auf Spring, um diese Abhängigkeit bereitzustellen, und kann sie nicht anders abrufen. Sie können nicht einfach Ihre eigene Scheinimplementierung in einer Testklasse erstellen und diese selbst weitergeben. Dies widerspricht im Grunde dem Zweck von Spring als Abhängigkeitsinjektionsbehälter.

Überall wollen Sie sagen:

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

Sie sollten stattdessen beispielsweise eine Methode deklarieren:

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

Und dann in Ihrer Konfiguration:

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

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

Frühling wird dann automatisch injiziert myClassin myOtherClass.

Deklarieren Sie alles auf diese Weise und haben Sie an der Wurzel so etwas wie:

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

MyApplicationist die zentralste Klasse und hängt zumindest indirekt von jedem anderen Dienst in Ihrem Programm ab. Beim Bootstrapping können Sie in Ihrer mainMethode aufrufen, applicationContext.getBean("myApplication")aber Sie sollten getBean()nirgendwo anders aufrufen müssen !

ColinD
quelle
3
Gibt es etwas in diesem Zusammenhang, das beim Erstellen eines Objekts nur mit Anmerkungen funktioniert new MyOtherClass()? Ich weiß über @Autowired Bescheid, aber ich habe es immer nur auf Feldern verwendet und es bricht weiter new MyOtherClass().
Tim
70
Es ist nicht wahr, dass ApplicationContext.getBean () kein IoC ist. Niether ist es obligatorisch, dass alle Ihre Klassen bis zum Frühjahr instanziiert werden. Das ist ein unangemessenes Dogma. Wenn der ApplicationContext selbst injiziert wird, ist es vollkommen in Ordnung, ihn zu bitten, eine Bean auf diese Weise zu instanziieren - und die von ihm erstellten Beans können unterschiedliche Implementierungen sein, die auf dem ursprünglich injizierten ApplicationContext basieren. Zum Beispiel habe ich ein Szenario, in dem ich dynamisch neue Bean-Instanzen basierend auf einem Bean-Namen erstelle, der zum Zeitpunkt der Kompilierung unbekannt ist, aber mit einer der in meiner Datei spring.xml definierten Implementierungen übereinstimmt.
Alex Worden
3
Ich stimme Alex zu, ich habe das gleiche Problem, bei dem eine Factory-Klasse nur über Benutzerinteraktion weiß, welche Bean oder Implementierung zur Laufzeit verwendet werden soll. Ich denke, hier kommt die ContextAware-Oberfläche ins Spiel
Ben
3
@elbek: applicationContext.getBeanist keine Abhängigkeitsinjektion: Es greift direkt auf das Framework zu und verwendet es als Service Locator .
ColinD
6
@herman: Ich weiß nichts über Spring, weil ich es schon lange nicht mehr benutzt habe, aber in JSR-330 / Guice / Dagger würden Sie dies tun, indem Sie ein Provider<Foo>anstelle von a injizieren Foound provider.get()jedes Mal aufrufen, wenn Sie ein benötigen neue Instanz. Kein Verweis auf den Container selbst, und Sie können problemlos einen Providerzum Testen erstellen .
ColinD
64

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

  1. Service Locator ist für andere Personen viel einfacher zu befolgen. IoC ist 'magisch', aber Wartungsprogrammierer müssen Ihre verschlungenen Federkonfigurationen und die Vielzahl von Orten verstehen, um herauszufinden, wie Sie Ihre Objekte verkabelt haben.

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

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

  4. Die Abhängigkeitsinjektion eignet sich eher für größere Programme. Meistens lohnt sich die zusätzliche Komplexität nicht.

  5. Oft wird Spring verwendet, wenn Sie "die Implementierung später ändern möchten". Es gibt andere Möglichkeiten, 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 Explosionskrieg durchsuchen). Sie können Spring dazu bringen, Eigenschaftendateien zu verwenden. Bei Servlets müssen sich die Eigenschaftendateien jedoch an einem festgelegten Speicherort befinden. Dies bedeutet, dass Sie nicht mehrere Servlets gleichzeitig auf derselben Box bereitstellen können. Sie können Spring mit JNDI verwenden, um die Eigenschaften beim Servlet-Start zu ändern. Wenn Sie jedoch JNDI für vom Administrator veränderbare Parameter verwenden, verringert sich der Bedarf an Spring selbst (da JNDI effektiv ein Service Locator ist).

  7. Mit Spring können Sie die Programmsteuerung verlieren, wenn Spring an Ihre Methoden sendet. Dies ist praktisch und funktioniert für viele Arten von Anwendungen, jedoch nicht für alle. Möglicherweise müssen Sie den Programmfluss steuern, wenn Sie während der Initialisierung Aufgaben (Threads usw.) erstellen müssen oder 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 überentwickelt sein und ungerechtfertigte Komplexität für die Betreuer mit sich bringen kann. Verwenden Sie IoC nicht automatisch, ohne vorher darüber nachzudenken, wie Sie es nicht verwenden können.

Moa
quelle
7
Außerdem kann Ihr ServiceLocator immer IoC von Spring verwenden, um Ihren Code von der Abhängigkeit von Spring zu abstrahieren, mit Spring-Anmerkungen übersät zu sein und nicht entzifferbare Magie. Ich habe kürzlich eine Reihe von Codes auf GoogleAppEngine portiert, wo Spring nicht unterstützt wird. Ich wünschte, ich hätte überhaupt alle IoC hinter einer ServiceFactory versteckt!
Alex Worden
IoC fördert ein anämisches Domänenmodell, das ich verachte. Entity Beans benötigen eine Möglichkeit, ihre Dienste nachzuschlagen, damit sie ihr eigenes Verhalten implementieren können. Auf dieser Ebene kommt man nicht wirklich darum herum, einen Service Locator zu benötigen.
Joel
4
Bizar. Ich benutze Spring die ganze Zeit mit Anmerkungen. Obwohl in der Tat eine bestimmte Lernkurve involviert ist, habe ich jetzt keinerlei Probleme mit Wartung, Debugging, Klarheit, Lesbarkeit ... Ich denke, wie Sie die Dinge strukturieren, ist der Trick.
Lawrence
25

Es ist wahr, dass durch die Aufnahme der Klasse in application-context.xml die Verwendung von getBean vermieden wird. Aber auch das ist eigentlich unnötig. Wenn Sie eine eigenständige Anwendung schreiben und Ihre Treiberklasse NICHT in application-context.xml aufnehmen möchten, können Sie den folgenden Code verwenden, damit Spring die Abhängigkeiten des Treibers automatisch verdrahtet:

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 Klasse habe, die einen Aspekt meiner App verwenden muss (z. B. zum Testen), aber ich möchte sie nicht in den Anwendungskontext aufnehmen, da dies nicht der Fall ist eigentlich Teil der App. Beachten Sie auch, dass dadurch vermieden wird, dass die Bean mit einem String-Namen nachgeschlagen werden muss, was ich immer für hässlich gehalten habe.

iftheshoefritz
quelle
Ich konnte diese Methode auch mit der @AutowiredAnnotation erfolgreich anwenden.
Blong
21

Einer der coolsten Vorteile von Spring ist, dass Sie Ihre Objekte nicht miteinander verbinden müssen. Zeus 'Kopf spaltet sich auf und Ihre Klassen erscheinen vollständig geformt mit all ihren Abhängigkeiten, die nach Bedarf erstellt und verkabelt wurden. Es ist magisch und fantastisch.

Je mehr du sagst ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, desto weniger Magie bekommst du. Weniger Code ist fast immer besser. Wenn Ihre Klasse wirklich eine ClassINeed-Bohne brauchte, warum haben Sie sie nicht einfach angeschlossen?

Das heißt, etwas muss offensichtlich das erste Objekt erstellen. Es ist nichts Falsches daran, dass Ihre Hauptmethode ein oder zwei Bohnen über getBean () erwirbt, aber Sie sollten dies vermeiden, da Sie bei jeder Verwendung nicht wirklich die gesamte Magie des Frühlings nutzen.

Brandon Yarbrough
quelle
1
Das OP sagt jedoch nicht "ClassINeed", sondern "BeanNameINeed". Dadurch kann der IoC-Container eine Instanz für jede Klasse erstellen, die auf irgendeine Weise konfiguriert wurde. Vielleicht ähnelt es eher dem "Service Locator" -Muster als IoC, aber es führt immer noch zu einer losen Kopplung.
HDave
16

Die Motivation besteht darin, Code zu schreiben, der nicht explizit von Spring abhängt. Auf diese Weise müssen Sie beim Wechseln von Containern keinen Code neu schreiben.

Stellen Sie sich den Container als etwas vor, das für Ihren Code unsichtbar ist und auf magische Weise für seine Anforderungen sorgt, ohne gefragt zu werden.

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

erickson
quelle
11

Verwenden @Autowiredoder ApplicationContext.getBean()ist wirklich das gleiche. Auf beide Arten erhalten Sie die Bean, die in Ihrem Kontext konfiguriert ist, und auf beide Arten hängt Ihr Code von spring ab. Das einzige, was Sie vermeiden sollten, ist das Instanziieren Ihres ApplicationContext. Mach das nur einmal! Mit anderen Worten, eine Linie wie

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

sollte nur einmal in Ihrer Anwendung verwendet werden.

easyplanner.cu.cc
quelle
Nee. Manchmal können @Autowired oder ApplicationContext.getBean () völlig unterschiedliche Beans erzeugen. Ich bin mir nicht sicher, wie es passiert ist, aber ich habe gerade mit diesem Problem zu kämpfen.
Oleksandr_DJ
4

Die Idee ist, dass Sie sich auf die Abhängigkeitsinjektion ( Inversion of Control oder IoC) verlassen. Das heißt, Ihre Komponenten werden mit den Komponenten konfiguriert, die sie benötigen. Diese Abhängigkeiten werden injiziert (über den Konstruktor oder die Setter) - Sie erhalten sie 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.

Auf diese Weise können Sie Ihre Anwendung mit verschiedenen Komponentenimplementierungen einfach neu verkabeln oder Objekte zum Testen auf einfache Weise konfigurieren, indem Sie verspottete Varianten bereitstellen (z. B. ein verspottetes DAO, damit Sie beim Testen nicht auf eine Datenbank stoßen).

Brian Agnew
quelle
4

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

Normalerweise bedeutet dies, dass es genau einmal gemacht wird: während des Bootstrappens. Und dann müssen Sie nur noch auf die "Root" -Bohne zugreifen, über die andere Abhängigkeiten aufgelöst werden können. Dies kann wiederverwendbarer Code sein, wie z. B. ein Basisservlet (wenn Web-Apps entwickelt werden).

StaxMan
quelle
4

Eine der Prämissen von Spring ist die Vermeidung von Kopplungen . Definieren und verwenden Sie Interfaces, DI, AOP und vermeiden Sie die Verwendung von ApplicationContext.getBean () :-)

Sourcerebels
quelle
4

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

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 (aufgrund der Anmerkungen) immer noch von Spring abhängig sind, können Sie Ihre Abhängigkeit von Spring entfernen, ohne Code zu ändern (nur die Anmerkungsdefinitionen), und der Testentwickler muss nichts darüber wissen, wie Spring funktioniert (vielleicht sollte er es trotzdem, aber Es ermöglicht die Überprüfung und Prüfung des Codes getrennt von den Funktionen der Feder.

Bei Verwendung des ApplicationContext ist dies weiterhin möglich. Dann müssen Sie sich jedoch lustig machen, ApplicationContextwas eine riesige Schnittstelle ist. Sie benötigen entweder eine Dummy-Implementierung oder Sie können ein Verspottungsframework 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 eine ziemliche Möglichkeit, 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 darstellt, ist diese:

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

Das Testen erfordert große Anstrengungen, oder Ihre Bean wird bei jedem Test versuchen, eine Verbindung zum Stackoverflow herzustellen. Und sobald Sie einen Netzwerkfehler haben (oder die Administratoren bei Stackoverflow Sie aufgrund einer übermäßigen Zugriffsrate blockieren), werden Sie zufällig fehlgeschlagene Tests haben.

Abschließend würde ich nicht sagen, dass die ApplicationContextdirekte Verwendung automatisch falsch ist und unter allen Umständen vermieden werden sollte. Wenn es jedoch bessere Optionen gibt (und in den meisten Fällen auch), verwenden Sie die besseren Optionen.

Yankee
quelle
3

Ich habe nur zwei Situationen gefunden, in denen getBean () erforderlich war:

Andere haben erwähnt, dass getBean () in main () verwendet wird, um die "main" -Bohne für ein eigenständiges Programm abzurufen.

Eine andere Verwendung, die ich mit getBean () gemacht habe, sind Situationen, in denen eine interaktive Benutzerkonfiguration das Bean-Make-up für eine bestimmte Situation bestimmt. So durchläuft beispielsweise ein Teil des Boot-Systems eine Datenbanktabelle mit getBean () mit einer Bean-Definition von scope = 'prototype' und legt dann zusätzliche Eigenschaften fest. Vermutlich gibt es eine Benutzeroberfläche, die die Datenbanktabelle anpasst, die freundlicher wäre als der Versuch, das XML des Anwendungskontexts (neu) zu schreiben.

nsayer
quelle
3

Es gibt eine andere Zeit, in der die Verwendung von getBean sinnvoll ist. Wenn Sie ein bereits vorhandenes System neu konfigurieren, bei dem die Abhängigkeiten in Spring-Kontextdateien nicht explizit aufgerufen werden. Sie können den Prozess starten, indem Sie getBean aufrufen, damit Sie nicht alles auf einmal verkabeln müssen. Auf diese Weise können Sie langsam Ihre Federkonfiguration aufbauen, indem Sie jedes Teil im Laufe der Zeit in Position bringen und die Bits richtig ausrichten. Die Aufrufe von getBean werden irgendwann ersetzt, aber wenn Sie die Struktur des Codes verstehen oder nicht, können Sie damit beginnen, mehr und mehr Beans zu verkabeln und immer weniger Aufrufe für getBean zu verwenden.

Tony Giaccone
quelle
2

Es gibt jedoch immer noch Fälle, in denen Sie das Service Locator-Muster benötigen. Zum Beispiel habe ich eine Controller-Bean. Dieser Controller verfügt möglicherweise über einige Standard-Service-Beans, deren Abhängigkeit durch die Konfiguration injiziert werden kann. Es kann jedoch auch viele zusätzliche oder neue Dienste geben, die dieser Controller jetzt oder später aufrufen kann. Diese benötigen dann den Service Locator, um die Service Beans abzurufen.

lwpro2
quelle
0

Sie sollten Folgendes verwenden: ConfigurableApplicationContext anstelle von ApplicationContext

Hai Nguyen Le
quelle