Perché ApplicationContext.getBean di Spring è considerato negativo?


270

Ho fatto una domanda generale su Spring: Spring Bean Auto-cast e ho avuto più persone che rispondono che chiamare Spring ApplicationContext.getBean()dovrebbe essere evitato il più possibile. Perché?

In quale altro modo dovrei accedere ai bean che ho configurato Spring per creare?

Sto usando Spring in un'applicazione non web e avevo pianificato di accedere a un ApplicationContextoggetto condiviso come descritto da LiorH .

Emendamento

Accetto la risposta di seguito, ma ecco una versione alternativa di Martin Fowler che discute i meriti dell'iniezione di dipendenza rispetto all'uso di un Service Locator (che è essenzialmente lo stesso di chiamare un wrapping ApplicationContext.getBean()).

In parte, Fowler afferma: " Con il localizzatore di servizi la classe applicativa lo richiede [il servizio] esplicitamente tramite un messaggio al localizzatore. Con l'iniezione non c'è una richiesta esplicita, il servizio appare nella classe dell'applicazione - da qui l'inversione del controllo. L'inversione del controllo è una caratteristica comune dei framework, ma è qualcosa che ha un prezzo. Tende ad essere difficile da capire e porta a problemi quando si tenta di eseguire il debug. Quindi nel complesso preferisco evitarlo [Inversion of Control ] a meno che non ne abbia bisogno. Questo non vuol dire che è una brutta cosa, solo che penso che debba giustificarsi sull'alternativa più semplice. "

Risposte:


202

L'ho menzionato in un commento sull'altra domanda, ma l'idea generale di Inversion of Control è che nessuna delle tue classi sappia o si preoccupi di come ottengono gli oggetti da cui dipendono . Ciò semplifica la modifica del tipo di implementazione di una determinata dipendenza che si utilizza in qualsiasi momento. Inoltre semplifica il test delle classi, in quanto è possibile fornire implementazioni simulate delle dipendenze. Infine, rende le classi più semplici e più focalizzate sulla loro responsabilità principale.

La chiamata ApplicationContext.getBean()non è inversione di controllo! Mentre è ancora facile cambiare l'implementazione è configurata per il nome del bean dato, la classe ora si affida direttamente a Spring per fornire quella dipendenza e non può ottenerla in nessun altro modo. Non puoi semplicemente realizzare la tua finta implementazione in una classe di test e passarla a te stesso. Ciò sostanzialmente vanifica lo scopo di Spring come contenitore di iniezione di dipendenza.

Ovunque tu voglia dire:

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

dovresti invece, ad esempio, dichiarare un metodo:

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

E poi nella tua configurazione:

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

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

Molla viene poi iniettare automaticamente myClassin myOtherClass.

Dichiarare tutto in questo modo, e alla radice di tutto hanno qualcosa del tipo:

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

MyApplicationè la classe più centrale e dipende almeno indirettamente da ogni altro servizio nel programma. Durante l'avvio, nel tuo mainmetodo, puoi chiamare applicationContext.getBean("myApplication")ma non dovresti aver bisogno di chiamare getBean()altrove!


3
C'è qualcosa di correlato a questo che funziona solo con le annotazioni durante la creazione di un new MyOtherClass()oggetto? So di @Autowired, ma l'ho mai usato solo sui campi e si rompe su new MyOtherClass()..
Tim

71
Non è vero che ApplicationContext.getBean () non è IoC. Niether è obbligatorio avere tutte le tue lezioni istanziate da Spring. Questo è dogma inappropriato. Se viene iniettato ApplicationContext stesso, è perfettamente corretto chiedergli di creare un'istanza di un bean in questo modo - e i bean che crea possono essere implementazioni diverse in base a ApplicationContext inizialmente iniettato. Ad esempio, ho uno scenario in cui creo dinamicamente nuove istanze di bean in base a un nome di bean sconosciuto al momento della compilazione ma che corrisponde a una delle implementazioni definite nel mio file spring.xml.
Alex Worden,

3
Concordo con Alex, ho lo stesso problema, in cui una classe di fabbrica saprà solo quale bean o implementazione utilizzare in fase di esecuzione tramite l'interazione dell'utente, penso che sia qui che entra in gioco l'interfaccia ContextAware
Ben

3
@elbek: applicationContext.getBeannon è l'iniezione di dipendenza: sta accedendo direttamente al framework, usandolo come localizzatore di servizi .
ColinD

6
@herman: Non conosco Spring perché non lo uso da molto tempo, ma in JSR-330 / Guice / Dagger, lo faresti iniettando un Provider<Foo>invece di un Fooe chiamando provider.get()ogni volta che hai bisogno di un nuova istanza. Nessun riferimento al contenitore stesso e puoi facilmente creare un oggetto Providerdi prova.
ColinD,

65

I motivi per preferire Service Locator a Inversion of Control (IoC) sono:

  1. Service Locator è molto, molto più facile per gli altri seguire il tuo codice. L'IoC è "magico", ma i programmatori di manutenzione devono comprendere le configurazioni contorte della Primavera e tutte le innumerevoli posizioni per capire come cablare i tuoi oggetti.

  2. IoC è terribile per i problemi di configurazione del debug. In alcune classi di applicazioni l'applicazione non si avvierà se configurata in modo errato e potresti non avere la possibilità di passare attraverso ciò che sta accadendo con un debugger.

  3. IoC è principalmente basato su XML (le annotazioni migliorano le cose ma c'è ancora molto XML là fuori). Ciò significa che gli sviluppatori non possono lavorare sul tuo programma se non conoscono tutti i tag magici definiti da Spring. Non è abbastanza buono per conoscere più Java. Ciò ostacola i programmatori meno esperti (vale a dire che in realtà è una progettazione scadente utilizzare una soluzione più complicata quando una soluzione più semplice, come Service Locator, soddisferà gli stessi requisiti). Inoltre, il supporto per la diagnosi dei problemi XML è molto più debole del supporto per i problemi Java.

  4. L'iniezione di dipendenza è più adatta a programmi più grandi. Il più delle volte la complessità aggiuntiva non ne vale la pena.

  5. Spesso viene utilizzata Spring nel caso in cui "si desideri modificare l'implementazione in un secondo momento". Esistono altri modi per raggiungere questo obiettivo senza la complessità di Spring IoC.

  6. Per le applicazioni Web (Java EE WARs) il contesto Spring è effettivamente associato al momento della compilazione (a meno che non si desideri che gli operatori si occupino del contesto durante la guerra esplosa). È possibile fare in modo che Spring utilizzi i file delle proprietà, ma con i file delle proprietà dei servlet sarà necessario trovarsi in una posizione predeterminata, il che significa che non è possibile distribuire più servlet contemporaneamente nella stessa casella. È possibile utilizzare Spring con JNDI per modificare le proprietà all'avvio del servlet, ma se si utilizza JNDI per parametri modificabili dall'amministratore, la necessità di Spring stessa diminuisce (poiché JNDI è effettivamente un Service Locator).

  7. Con Spring puoi perdere il controllo del programma se Spring sta inviando i tuoi metodi. Questo è conveniente e funziona per molti tipi di applicazioni, ma non per tutti. Potrebbe essere necessario controllare il flusso del programma quando è necessario creare attività (thread, ecc.) Durante l'inizializzazione o se sono necessarie risorse modificabili di cui Spring non era a conoscenza quando il contenuto era associato a WAR.

La primavera è ottima per la gestione delle transazioni e presenta alcuni vantaggi. È solo che l'IoC può essere eccessivamente ingegneristico in molte situazioni e introdurre complessità ingiustificata per i manutentori. Non utilizzare automaticamente l'IoC senza pensare a come non utilizzarlo prima.


7
Inoltre, ServiceLocator può sempre utilizzare l'IoC di Spring, sottraendo il codice dalla dipendenza da Spring, disseminato di annotazioni Spring e magik indecifrabile. Recentemente ho portato un sacco di codice su GoogleAppEngine dove Spring non è supportato. Vorrei aver nascosto tutto l'IoC dietro una ServiceFactory in primo luogo!
Alex Worden,

IoC incoraggia un modello di dominio anemico, che disprezzo. I bean di entità hanno bisogno di un modo per cercare i propri servizi in modo da poter implementare il proprio comportamento. A quel livello, non puoi davvero andare in giro avendo bisogno di un localizzatore di servizi.
Gioele

4
Bizar. Uso sempre la primavera con le annotazioni. Anche se in effetti è coinvolta una certa curva di apprendimento, ora non ho alcun problema in termini di manutenzione, debug, chiarezza, leggibilità ... Immagino che il modo in cui strutturi le cose sia il trucco.
Lawrence,

25

È vero che includere la classe in application-context.xml evita la necessità di usare getBean. Tuttavia, anche questo non è effettivamente necessario. Se stai scrivendo un'applicazione autonoma e NON vuoi includere la tua classe di driver in application-context.xml, puoi usare il codice seguente per fare in modo che Spring autowire le dipendenze del driver:

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

Ho dovuto farlo un paio di volte quando ho una sorta di classe autonoma che deve usare alcuni aspetti della mia app (ad esempio per i test) ma non voglio includerla nel contesto dell'applicazione perché non è attualmente parte dell'app. Nota anche che questo evita la necessità di cercare il bean usando un nome String, che ho sempre pensato fosse brutto.


Sono stato in grado di utilizzare questo metodo con successo anche con l' @Autowiredannotazione.
blong,

21

Uno dei vantaggi più interessanti dell'utilizzo di qualcosa come Spring è che non devi collegare i tuoi oggetti insieme. La testa di Zeus si spalanca e le tue classi appaiono, completamente formate con tutte le loro dipendenze create e cablate, se necessario. È magico e fantastico.

Più dici ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, meno magia stai ricevendo. Meno codice è quasi sempre migliore. Se la tua classe aveva davvero bisogno di un bean ClassINeed, perché non l'hai semplicemente collegato?

Detto questo, qualcosa deve ovviamente creare il primo oggetto. Non c'è niente di sbagliato nel tuo metodo principale di acquisire un bean o due tramite getBean (), ma dovresti evitarlo perché ogni volta che lo usi, non stai davvero usando tutta la magia di Spring.


1
Ma l'OP non sta dicendo "ClassINeed", sta dicendo "BeanNameINeed", che consente al contenitore IoC di creare un'istanza su qualsiasi classe configurata in alcun modo. Forse è più simile al modello di "localizzatore di servizio" rispetto all'IoC, ma risulta comunque in accoppiamento libero.
HDave

16

La motivazione è scrivere codice che non dipenda esplicitamente da Spring. In questo modo, se si sceglie di cambiare contenitore, non è necessario riscrivere alcun codice.

Pensa al contenitore come qualcosa di invisibile al tuo codice, che provvede magicamente alle sue esigenze, senza che ti venga chiesto.

L'iniezione di dipendenza è un contrappunto al modello "localizzatore di servizio". Se stai cercando le dipendenze per nome, potresti anche sbarazzarti del contenitore DI e usare qualcosa come JNDI.


11

Usare @Autowiredo ApplicationContext.getBean()è davvero la stessa cosa. In entrambi i modi si ottiene il bean configurato nel proprio contesto e in entrambi i modi il codice dipende dalla primavera. L'unica cosa che dovresti evitare è creare un'istanza di ApplicationContext. Fallo solo una volta! In altre parole, una linea come

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

dovrebbe essere usato solo una volta nella tua applicazione.


No. A volte @Autowired o ApplicationContext.getBean () possono produrre bean totalmente diversi. Non sono sicuro di come sia successo, ma sto lottando con questo problema in questo momento.
Oleksandr_DJ,

4

L'idea è di fare affidamento sull'iniezione di dipendenza ( inversione di controllo o IoC). Cioè, i componenti sono configurati con i componenti di cui hanno bisogno. Queste dipendenze vengono iniettate (tramite il costruttore o i setter) - non si ottiene quindi te stesso.

ApplicationContext.getBean()richiede di nominare un bean in modo esplicito all'interno del componente. Invece, utilizzando IoC, la configurazione può determinare quale componente verrà utilizzato.

Ciò consente di ricollegare facilmente l'applicazione con diverse implementazioni di componenti o configurare oggetti per il test in modo semplice fornendo varianti derise (ad esempio un DAO deriso in modo da non colpire un database durante il test)


4

Altri hanno indicato il problema generale (e sono risposte valide), ma offrirò solo un commento aggiuntivo: non è che non dovresti MAI farlo, ma piuttosto che lo faccia il meno possibile.

Di solito questo significa che viene fatto esattamente una volta: durante l'avvio. E poi è solo per accedere al bean "root", attraverso il quale è possibile risolvere altre dipendenze. Questo può essere un codice riutilizzabile, come il servlet di base (se si sviluppano app Web).


4

Una delle premesse di Spring è evitare l' accoppiamento . Definisci e usa Interfaces, DI, AOP ed evita di usare ApplicationContext.getBean () :-)


4

Uno dei motivi è la testabilità. Dì che hai questa lezione:

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

Come puoi testare questo fagiolo? Ad esempio in questo modo:

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 vero?

Mentre continui a dipendere da Spring (a causa delle annotazioni) puoi rimuovere la tua dipendenza da spring senza cambiare alcun codice (solo le definizioni delle annotazioni) e lo sviluppatore del test non ha bisogno di sapere come funziona Spring (forse dovrebbe comunque, ma permette di rivedere e testare il codice separatamente da quello che fa Spring).

È ancora possibile fare lo stesso quando si utilizza ApplicationContext. Tuttavia, è necessario prendere in giro ApplicationContextche è un'interfaccia enorme. O hai bisogno di un'implementazione fittizia o puoi usare un framework beffardo come 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);
    }
}

Questa è una possibilità, ma penso che la maggior parte delle persone concorderebbe sul fatto che la prima opzione è più elegante e semplifica il test.

L'unica opzione che è davvero un problema è questa:

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

Il test richiede enormi sforzi o il bean tenterà di connettersi allo stackoverflow su ogni test. E non appena si verifica un errore di rete (o gli amministratori di StackOverflow ti bloccano a causa di un tasso di accesso eccessivo), i test falliranno casualmente.

Quindi, in conclusione, non direi che l'uso ApplicationContextdiretto è automaticamente sbagliato e dovrebbe essere evitato a tutti i costi. Tuttavia, se esistono opzioni migliori (e nella maggior parte dei casi), utilizzare le opzioni migliori.


3

Ho trovato solo due situazioni in cui era richiesto getBean ():

Altri hanno menzionato l'uso di getBean () in main () per recuperare il bean "main" per un programma autonomo.

Un altro uso che ho fatto di getBean () è in situazioni in cui una configurazione utente interattiva determina la composizione del bean per una situazione particolare. In modo che, ad esempio, parte del sistema di avvio esegua il ciclo attraverso una tabella di database utilizzando getBean () con una definizione del bean scope = 'prototype' e quindi impostando proprietà aggiuntive. Presumibilmente, esiste un'interfaccia utente che regola la tabella del database che sarebbe più amichevole del tentativo di (ri) scrivere l'XML del contesto dell'applicazione.


3

C'è un'altra volta che ha senso usare getBean. Se stai riconfigurando un sistema già esistente, in cui le dipendenze non sono esplicitamente richiamate nei file di contesto di primavera. Puoi avviare il processo effettuando chiamate a getBean, in modo da non doverlo collegare tutti in una volta. In questo modo puoi costruire lentamente la tua configurazione a molla mettendo ogni pezzo in posizione nel tempo e facendo allineare correttamente i pezzi. Le chiamate a getBean verranno eventualmente sostituite, ma quando comprendi la struttura del codice o ne manchi, puoi iniziare il processo di cablaggio di sempre più bean e utilizzando sempre meno chiamate per getBean.


2

tuttavia, ci sono ancora casi in cui è necessario il modello di localizzazione del servizio. ad esempio, ho un bean controller, questo controller potrebbe avere alcuni bean di servizio predefiniti, che possono essere iniettati in dipendenza dalla configurazione. mentre potrebbero esserci anche molti servizi aggiuntivi o nuovi che questo controller può invocare ora o in seguito, il che richiede il localizzatore di servizi per recuperare i bean di servizio.


0

È necessario utilizzare: ConfigurableApplicationContext anziché per ApplicationContext

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.