Dlaczego Spring ApplicationContext.getBean jest uważany za zły?


270

Zadałem ogólne pytanie wiosenne: Automatyczne rzucanie wiosennych ziaren i wiele osób odpowiedziało, że wzywając wiosennych ApplicationContext.getBean()należy unikać w jak największym stopniu. Dlaczego?

Jak inaczej mam uzyskać dostęp do ziaren, które skonfigurowałem do tworzenia przez Spring?

Używam Springa w aplikacji innej niż web i planowałem dostęp do ApplicationContextobiektu udostępnionego zgodnie z opisem LiorH .

Poprawka

Akceptuję odpowiedź poniżej, ale oto alternatywne ujęcie Martina Fowlera, który omawia zalety wstrzykiwania zależności w zależności od użycia lokalizatora usług (co w zasadzie jest takie samo jak wywołanie opakowania ApplicationContext.getBean()).

Po części Fowler stwierdza: „ W przypadku lokalizatora usług klasa aplikacji prosi o to [usługa] jawnie w komunikacie do lokalizatora. Po wstrzyknięciu nie ma wyraźnego żądania, usługa pojawia się w klasie aplikacji - stąd odwrócenie kontroli. Inwersja kontroli jest powszechną cechą frameworków, ale jest to coś, co ma swoją cenę. Jest to trudne do zrozumienia i prowadzi do problemów podczas próby debugowania. Ogólnie rzecz biorąc wolę tego unikać [Inwersja kontroli ] chyba, że ​​go potrzebuję. Nie oznacza to, że jest to zła rzecz, po prostu myślę, że musi uzasadnić się bardziej prostą alternatywą ”.

Odpowiedzi:


202

Wspomniałem o tym w komentarzu do drugiego pytania, ale cała idea Inwersji Kontroli polega na tym, aby żadna z twoich klas nie wiedziała ani nie przejmowała się, w jaki sposób zdobywają przedmioty, na których polegają . Ułatwia to zmianę, jakiego rodzaju implementacji danej zależności używasz w dowolnym momencie. Ułatwia także testowanie klas, ponieważ można zapewnić fałszywe implementacje zależności. Wreszcie, sprawia, że ​​zajęcia są prostsze i bardziej skoncentrowane na ich podstawowej odpowiedzialności.

Wywołanie ApplicationContext.getBean()nie jest odwróceniem kontroli! Chociaż nadal łatwo jest zmienić konfigurację implementacji dla podanej nazwy fasoli, klasa polega teraz bezpośrednio na Spring w celu zapewnienia tej zależności i nie może uzyskać jej w żaden inny sposób. Nie możesz po prostu stworzyć własnej próbnej implementacji w klasie testowej i samemu ją przekazać. Zasadniczo pokonuje to cel Springa jako pojemnik do wstrzykiwania zależności.

Gdziekolwiek chcesz powiedzieć:

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

powinieneś zamiast tego zadeklarować metodę:

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

A następnie w twojej konfiguracji:

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

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

Wiosna wtedy automatycznie wstrzykuje myClasssię myOtherClass.

Zadeklaruj wszystko w ten sposób, a u podstaw tego wszystkiego jest coś takiego:

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

MyApplicationjest najbardziej centralną klasą i zależy przynajmniej pośrednio od każdej innej usługi w twoim programie. Podczas ładowania w mainmetodzie możesz dzwonić, applicationContext.getBean("myApplication")ale nie musisz dzwonić getBean()nigdzie indziej!


3
Czy jest coś z tym związanego, co działa tylko z adnotacjami podczas tworzenia new MyOtherClass()obiektu? Wiem o @Autowired, ale kiedykolwiek używałem go tylko na polach, a on się psuje new MyOtherClass()...
Tim

70
To nieprawda, że ​​ApplicationContext.getBean () nie jest IoC. Niether jest obowiązkowe, aby wszystkie twoje zajęcia były tworzone przez Springa. To niewłaściwy dogmat. Jeśli wstrzykiwany jest sam ApplicationContext, to jest całkowicie w porządku, aby poprosić go o utworzenie instancji komponentu bean w ten sposób - a tworzone przez niego komponenty bean mogą być różnymi implementacjami opartymi na wstrzykiwanym ApplicationContext. Mam na przykład scenariusz, w którym dynamicznie tworzę nowe instancje komponentu bean na podstawie nazwy komponentu bean, który jest nieznany w czasie kompilacji, ale pasuje do jednej z implementacji zdefiniowanych w moim pliku spring.xml.
Alex Worden,

3
Zgadzam się z Alexem, mam ten sam problem, w którym klasa fabryczna będzie wiedziała tylko, którą fasolę lub implementację użyć w czasie wykonywania poprzez interakcję użytkownika, myślę, że właśnie tutaj pojawia się interfejs ContextAware
Ben

3
@elbek: applicationContext.getBeannie jest wstrzykiwaniem zależności: uzyskuje bezpośredni dostęp do frameworka, używając go jako lokalizatora usług .
ColinD

6
@herman: Nie wiem o Spring, ponieważ nie używałem jej od dawna, ale w JSR-330 / Guice / Dagger robiłbyś to, wstrzykując Provider<Foo>zamiast a Fooi dzwoniąc za provider.get()każdym razem, gdy potrzebujesz nowa instancja. Brak odniesienia do samego kontenera i można łatwo utworzyć Providertest.
ColinD

64

Powody, dla których preferuje się lokalizator usług zamiast inwersji kontroli (IoC) to:

  1. Lokalizator usług jest znacznie łatwiejszy dla osób śledzących Twój kod. IoC to „magia”, ale programiści zajmujący się konserwacją muszą zrozumieć twoje skomplikowane konfiguracje wiosenne i wszystkie niezliczone lokalizacje, aby dowiedzieć się, w jaki sposób podłączyłeś swoje obiekty.

  2. IoC jest straszny w przypadku problemów z konfiguracją. W niektórych klasach aplikacji aplikacja nie uruchomi się, gdy zostanie źle skonfigurowana, i możesz nie mieć szansy przejść przez to, co dzieje się z debuggerem.

  3. IoC opiera się głównie na XML (Adnotacje poprawiają rzeczy, ale wciąż jest wiele XML-a). Oznacza to, że programiści nie mogą pracować z twoim programem, dopóki nie znają wszystkich magicznych tagów zdefiniowanych przez Springa. Nie wystarczy już znać Javę. Utrudnia to mniej doświadczonym programistom (tj. Właściwie kiepskim pomysłem jest stosowanie bardziej skomplikowanego rozwiązania, gdy prostsze rozwiązanie, takie jak Service Locator, spełni te same wymagania). Ponadto obsługa diagnozowania problemów XML jest znacznie słabsza niż obsługa problemów Java.

  4. Wstrzykiwanie zależności jest bardziej odpowiednie dla większych programów. W większości przypadków dodatkowa złożoność nie jest tego warta.

  5. Często wiosna jest używana w przypadku, gdy „możesz chcieć zmienić implementację później”. Istnieją inne sposoby osiągnięcia tego bez złożoności Spring IoC.

  6. W przypadku aplikacji internetowych (Java EE WARs) kontekst wiosenny jest skutecznie wiązany w czasie kompilacji (chyba że chcesz, aby operatorzy poruszali się po tym kontekście podczas wybuchu wojny). Możesz sprawić, by Spring używał plików właściwości, ale w przypadku serwletów pliki właściwości będą musiały znajdować się we wcześniej określonej lokalizacji, co oznacza, że ​​nie możesz wdrożyć wielu serwletów w tym samym czasie na tym samym urządzeniu. Możesz użyć Spring z JNDI do zmiany właściwości podczas uruchamiania serwletu, ale jeśli używasz JNDI do parametrów modyfikowanych przez administratora, potrzeba samej Spring zmniejsza się (ponieważ JNDI jest efektywnie lokalizatorem usług).

  7. Dzięki Spring możesz stracić kontrolę nad programem, jeśli Spring wysyła do twoich metod. Jest to wygodne i działa dla wielu rodzajów aplikacji, ale nie dla wszystkich. Może być konieczne kontrolowanie przepływu programu, gdy trzeba tworzyć zadania (wątki itp.) Podczas inicjalizacji lub potrzebne są modyfikowalne zasoby, o których Spring nie wiedział, kiedy treść była związana z twoją WAR.

Wiosna jest bardzo dobra do zarządzania transakcjami i ma pewne zalety. Tyle tylko, że IoC może być nadmiar inżynierii w wielu sytuacjach i wprowadzać nieuzasadnioną złożoność dla opiekunów. Nie używaj IoC automatycznie, nie zastanawiając się, jak go najpierw nie używać.


7
Plus - Twój ServiceLocator zawsze może korzystać z IoC od Springa, odciągając Twój kod od zależności od Springa, zaśmieconej adnotacjami Springa i nierozszyfrowalnym magikiem. Niedawno przesłałem sporo kodu do GoogleAppEngine, gdzie Spring nie jest obsługiwany. Chciałbym przede wszystkim ukryć cały IoC za ServiceFactory!
Alex Worden,

IoC zachęca do anemicznego modelu domeny, który gardzę. Fasola encji potrzebuje sposobu, aby sprawdzić swoje usługi, aby mogli wprowadzić własne zachowanie. Na tej warstwie naprawdę nie można się poruszać, potrzebując lokalizatora usług.
Joel

4
Bizar. Cały czas używam Springa z adnotacjami. Mimo że w grę wchodzi pewna krzywa uczenia się, teraz nie mam żadnych problemów z konserwacją, debugowaniem, jasnością, czytelnością ... Myślę, że to, w jaki sposób budujesz różne rzeczy, jest podstępem.
Lawrence

25

Prawdą jest, że włączenie klasy do application-context.xml pozwala uniknąć potrzeby użycia getBean. Jednak nawet to jest w rzeczywistości niepotrzebne. Jeśli piszesz samodzielną aplikację i NIE chcesz dołączać swojej klasy sterownika do pliku application-context.xml, możesz użyć następującego kodu, aby Spring automatycznie zarządzał zależnościami sterownika:

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

Musiałem to zrobić kilka razy, gdy mam jakąś niezależną klasę, która musi korzystać z niektórych aspektów mojej aplikacji (np. Do testowania), ale nie chcę uwzględniać jej w kontekście aplikacji, ponieważ nie jest właściwie część aplikacji. Zauważ też, że pozwala to uniknąć konieczności wyszukiwania fasoli za pomocą nazwy String, co zawsze uważałem za brzydkie.


Udało mi się @Autowiredrównież z powodzeniem zastosować tę metodę z adnotacją.
blong

21

Jedną z najfajniejszych zalet używania czegoś takiego jak Wiosna jest to, że nie musisz łączyć ze sobą swoich obiektów. Głowa Zeusa otwiera się i pojawiają się twoje klasy, w pełni uformowane ze wszystkimi ich zależnościami utworzonymi i podłączonymi, w razie potrzeby. To magiczne i fantastyczne.

Im więcej mówisz ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, tym mniej magii dostajesz. Mniej kodu jest prawie zawsze lepsze. Jeśli twoja klasa naprawdę potrzebowała fasoli ClassINeed, dlaczego po prostu jej nie podłączyłeś?

To powiedziawszy, coś oczywiście musi stworzyć pierwszy obiekt. Nie ma nic złego w twojej głównej metodzie pozyskiwania fasoli lub dwóch za pomocą getBean (), ale powinieneś tego unikać, ponieważ za każdym razem, gdy go używasz, tak naprawdę nie używasz całej magii Wiosny.


1
Ale OP nie mówi „ClassINeed”, mówi „BeanNameINeed” - co pozwala kontenerowi IoC utworzyć instancję na dowolnej klasie skonfigurowanej w dowolny sposób. Być może jest bardziej podobny do wzorca „lokalizatora usług” niż IoC, ale nadal powoduje luźne sprzężenie.
HDave

16

Motywacją jest napisanie kodu, który nie zależy bezpośrednio od Springa. W ten sposób, jeśli zdecydujesz się na zmianę kontenerów, nie musisz przepisywać żadnego kodu.

Pomyśl o pojemniku jako o czymś niewidocznym dla twojego kodu, magicznie zaspokajającym jego potrzeby, bez pytania.

Wstrzykiwanie zależności jest kontrapunktem dla wzorca „lokalizatora usług”. Jeśli zamierzasz wyszukiwać zależności według nazwy, równie dobrze możesz pozbyć się kontenera DI i użyć czegoś takiego jak JNDI.


11

Używanie @Autowiredlub ApplicationContext.getBean()to tak naprawdę to samo. W obu przypadkach otrzymujesz komponent bean, który jest skonfigurowany w twoim kontekście i w obu przypadkach twój kod zależy od wiosny. Jedyną rzeczą, której powinieneś unikać, jest utworzenie wystąpienia ApplicationContext. Zrób to tylko raz! Innymi słowy, linia jak

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

należy użyć tylko raz w aplikacji.


Nie. Czasami @Autowired lub ApplicationContext.getBean () może produkować zupełnie inne ziarna. Nie jestem pewien, jak to się stało, ale obecnie mam problem z tym problemem.
Oleksandr_DJ

4

Chodzi o to, że polegasz na wstrzykiwaniu zależności ( inwersja kontroli lub IoC). Oznacza to, że komponenty są skonfigurowane z komponentami, których potrzebują. Te zależności są wstrzykiwane (przez konstruktora lub seterów) - wtedy nie dostaniesz tego.

ApplicationContext.getBean()wymaga wyraźnego nazwania fasoli w komponencie. Zamiast tego, używając IoC, twoja konfiguracja może określić, który składnik będzie używany.

Umożliwia to łatwe ponowne połączenie aplikacji z różnymi implementacjami komponentów lub skonfigurowanie obiektów do testowania w prosty sposób, poprzez udostępnienie fałszywych wariantów (np. Fałszywego DAO, aby nie trafić do bazy danych podczas testowania)


4

Inni wskazywali na ogólny problem (i są poprawnymi odpowiedziami), ale przedstawię tylko jeden dodatkowy komentarz: to nie tak, że NIGDY nie powinieneś tego robić, ale raczej robić to tak mało, jak to możliwe.

Zwykle oznacza to, że odbywa się to dokładnie raz: podczas ładowania systemu. A potem jest tylko dostęp do fasoli „root”, dzięki której można rozwiązać inne zależności. Może to być kod wielokrotnego użytku, taki jak serwlet podstawowy (w przypadku tworzenia aplikacji internetowych).


4

Jedną z przesłanek Spring jest unikanie łączenia . Zdefiniuj i używaj interfejsów, DI, AOP i unikaj używania ApplicationContext.getBean () :-)


4

Jednym z powodów jest testowalność. Powiedz, że masz tę klasę:

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

Jak możesz przetestować tę fasolę? Np. Tak:

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

Łatwe, prawda?

Chociaż nadal jesteś zależny od Springa (z powodu adnotacji), możesz usunąć swoją zależność od wiosny bez zmiany żadnego kodu (tylko definicje adnotacji), a programista testów nie musi wiedzieć nic o tym, jak działa wiosna (może i tak powinien, ale pozwala przejrzeć i przetestować kod niezależnie od tego, co robi wiosna).

Nadal można to zrobić podczas korzystania z ApplicationContext. Jednak musisz wyśmiewać ApplicationContextco jest ogromnym interfejsem. Potrzebujesz albo fałszywej implementacji, albo możesz użyć frameworku, takiego jak 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);
    }
}

Jest to całkiem możliwe, ale myślę, że większość ludzi zgodzi się, że pierwsza opcja jest bardziej elegancka i upraszcza test.

Jedyną opcją, która naprawdę stanowi problem, jest ta:

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

Testowanie tego wymaga ogromnego wysiłku lub twoja fasola będzie próbowała połączyć się z przepełnieniem stosu w każdym teście. A gdy tylko dojdzie do awarii sieci (lub administratorzy blokują przepływ stosu blokując Cię z powodu nadmiernej szybkości dostępu), będziesz miał losowo nieudane testy.

Podsumowując, nie powiedziałbym, że używanie ApplicationContextbezpośrednio jest automatycznie niewłaściwe i należy go za wszelką cenę unikać. Jeśli jednak istnieją lepsze opcje (i w większości przypadków), skorzystaj z lepszych opcji.


3

Znalazłem tylko dwie sytuacje, w których wymagana była metoda getBean ():

Inni wspominali o użyciu getBean () w main () w celu pobrania „głównego” komponentu bean dla samodzielnego programu.

Kolejne zastosowanie getBean (), które zrobiłem, to sytuacje, w których interaktywna konfiguracja użytkownika określa makijaż bean dla konkretnej sytuacji. Na przykład część systemu rozruchowego zapętla się przez tabelę bazy danych za pomocą getBean () z definicją komponentu bean scope = 'prototype', a następnie ustawiając dodatkowe właściwości. Prawdopodobnie istnieje interfejs użytkownika, który dostosowuje tabelę bazy danych, co byłoby bardziej przyjazne niż próba (ponownego) zapisu kontekstowego pliku XML.


3

Jest inny czas, kiedy użycie getBean ma sens. Jeśli zmieniasz konfigurację systemu, który już istnieje, gdzie zależności nie są jawnie wywoływane w plikach kontekstu wiosny. Możesz rozpocząć ten proces, wprowadzając wywołania getBean, abyś nie musiał odkładać wszystkiego na raz. W ten sposób możesz powoli budować konfigurację sprężynową, ustawiając każdy element na miejscu i odpowiednio ustawiając bity. Wywołania getBean zostaną ostatecznie zastąpione, ale gdy zrozumiesz strukturę kodu lub go tam nie masz, możesz rozpocząć proces okablowania coraz większej liczby komponentów bean i używając coraz mniej wywołań do getBean.


2

jednak nadal istnieją przypadki, w których potrzebujesz wzorca lokalizatora usług. na przykład mam komponent bean kontrolera, ten kontroler może mieć niektóre domyślne komponenty bean usługi, które mogą być wstrzykiwane w zależności przez konfigurację. chociaż może być także wiele dodatkowych lub nowych usług, które kontroler może wywoływać teraz lub później, które następnie potrzebują lokalizatora usług, aby pobrać komponenty bean usługi.


Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.