Чому SpringCoogle ApplicationContext.getBean вважається поганим?


270

Я задав загальне питання весни: Автомобільні весняні боби і кілька людей відповіли, що ApplicationContext.getBean()слід уникати викликів Spring якнайбільше. Чому так?

Як ще я можу отримати доступ до квасолі, яку я створив Spring для створення?

Я використовую Spring в не веб-додатку і планував доступ до спільного ApplicationContextоб'єкта, як описано LiorH .

Поправка

Я приймаю відповідь нижче, але ось черговий прийом Мартіна Фаулера, який обговорює достоїнства ін'єкції залежності залежно від використання сервера-локатора (що по суті те саме, що викликати завершений ApplicationContext.getBean()).

Частково Фоулер заявляє: " За допомогою сервісного локатора клас програми запитує про це [послугу] явно за допомогою повідомлення в локатор. При введенні явного запиту немає, сервіс з'являється в класі додатків - отже, інверсія управління. Інверсія керування є загальною рисою фреймворків, але це щось, що виходить ціною. Це, як правило, важко зрозуміти і призводить до проблем, коли ви намагаєтесь налагоджувати. Тому в цілому я вважаю за краще уникати цього [Інверсія управління ], якщо мені це не потрібно. Це не означає, що це погана річ, просто я думаю, що вона повинна виправдовуватися над більш простою альтернативою ".

Відповіді:


202

Я згадував про це в коментарі до іншого питання, але вся ідея Інверсії управління полягає в тому, щоб ніхто з ваших класів не знав і не цікавив, як вони отримують об'єкти, від яких вони залежать . Це дозволяє легко змінити тип реалізації тієї чи іншої залежності, яку ви використовуєте в будь-який час. Це також полегшує тестування класів, оскільки ви можете надати макетні реалізації залежностей. Нарешті, це робить заняття більш простими та зосередженими на їх основній відповідальності.

Виклик ApplicationContext.getBean()- це не інверсія управління! Незважаючи на те, що все ще легко змінити те, що реалізація налаштована для даного імені квасолі, тепер клас покладається безпосередньо на Spring, щоб забезпечити цю залежність і не може отримати її іншим способом. Ви не можете просто зробити власну макетну реалізацію в тестовому класі та передати це їй самостійно. Це в основному перемагає призначення Spring як контейнера для ін'єкцій залежностей.

Куди б ви хотіли сказати:

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

замість цього слід, наприклад, оголосити метод:

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

А потім у вашій конфігурації:

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

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

Потім пружина автоматично вводиться myClassв myOtherClass.

Декларуйте все таким чином, і в основі цього все є щось на кшталт:

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

MyApplicationє найбільш центральним класом, і принаймні опосередковано залежить від будь-якого іншого сервісу у вашій програмі. Під час завантаження, у вашомуmain методі ви можете зателефонувати, applicationContext.getBean("myApplication")але не потрібно більше дзвонити getBean()нікуди!


3
Чи є щось пов’язане з цим, що працює лише з анотаціями при створенні new MyOtherClass()об’єкта? Я знаю про @Autowired, але я лише коли-небудь використовував його на полях, і він ламається new MyOtherClass()..
Тім

70
Неправда, що ApplicationContext.getBean () не є IoC. Нітер не є обов'язковим для того, щоб усі ваші заняття були створені весною. Це недоречна догма. Якщо ApplicationContext сам вводиться, то цілком чудово попросити його інстанціювати боб таким чином - і боби, які він створює, можуть бути різними реалізаціями на основі початково введеного ApplicationContext. Наприклад, у мене є сценарій, коли я динамічно створюю нові екземпляри біна на основі імені біна, яке невідоме під час компіляції, але відповідає одній із реалізацій, визначених у моєму файлі spring.xml.
Алекс Worden

3
Погодьтесь з Алексом, у мене є той самий випуск, де заводський клас дізнається лише, який бін або реалізацію використовувати під час виконання за допомогою взаємодії з користувачем, я думаю, саме тут приходить інтерфейс ContextAware
Бен

3
@elbek: applicationContext.getBeanне є ін'єкцією залежності: це доступ до фреймворку безпосередньо, використовуючи його як сервіс-локатор .
ColinD

6
@herman: Я не знаю про Spring, тому що я давно не використовував її, але в JSR-330 / Guice / Dagger, ви б це зробили, вводячи Provider<Foo>замість Fooі, і дзвонили provider.get()кожен раз, коли вам потрібно новий екземпляр. Немає посилання на сам контейнер, і ви можете легко створити Providerтест для тестування.
ColinD

64

Причини віддавати перевагу локатору послуг над інверсією управління (IoC):

  1. Локатор послуг набагато, набагато простіше дотримуватися інших у вашому коді. IoC - це "магія", але програмісти з технічного обслуговування повинні розуміти ваші конфігурації весни та всі безліч локацій, щоб зрозуміти, як ви з'єднали об'єкти.

  2. IoC страшний для налагодження проблем з конфігурацією. У деяких класах додатків програма не запуститься при неправильній конфігурації, і ви, можливо, не отримаєте шанс переглядати те, що відбувається з налагоджувачем.

  3. IoC в основному базується на XML (Анотації покращують речі, але там ще багато XML). Це означає, що розробники не можуть працювати над вашою програмою, якщо вони не знають усіх магічних тегів, визначених Spring. Недостатньо добре знати Java вже. Це заважає програмістам менше досвіду (тобто фактично погана конструкція використовувати більш складне рішення, коли більш просте рішення, наприклад, сервіс-локатор, буде виконувати ті самі вимоги). Крім того, підтримка діагностики проблем XML набагато слабша, ніж підтримка проблем Java.

  4. Ін'єкційна залежність більше підходить для великих програм. Більшу частину часу додаткової складності не варто.

  5. Часто Spring використовується в тому випадку, якщо ви "хочете змінити реалізацію пізніше". Є й інші способи досягти цього без складності Spring IoC.

  6. Для веб-додатків (Java EE WARs) контекст Весна фактично пов'язаний під час компіляції (якщо тільки ви не хочете, щоб оператори перекочували контекст у вибуховій війні). Ви можете змусити Spring використовувати файли властивостей, але файли властивостей сервлетів повинні знаходитись заздалегідь визначеним місцем, а це означає, що ви не можете розгортати кілька сервлетів за один і той же час в одному полі. Ви можете використовувати Spring з JNDI для зміни властивостей під час запуску сервлетів, але якщо ви використовуєте JNDI для параметрів, що змінюються адміністратором, потреба в самій Spring зменшується (оскільки JNDI фактично є Локатором послуг).

  7. Завдяки Spring ви можете втратити програму Control, якщо Spring надсилає ваші методи. Це зручно і працює для багатьох типів додатків, але не для всіх. Можливо, вам потрібно буде контролювати потік програми, коли вам потрібно створити завдання (потоки тощо) під час ініціалізації або потрібні модифіковані ресурси, про які Spring не знав, коли вміст був прив’язаний до вашої WAR.

Весна дуже хороша для управління транзакціями і має деякі переваги. Просто IoC може бути надмірно інженерним у багатьох ситуаціях та створити необґрунтовану складність для обслуговуючого персоналу. Не використовуйте автоматично IoC, не замислюючись про способи не використовувати його спочатку.


7
Плюс - ваш ServiceLocator завжди може використовувати IoC від Spring, абстрагуючи ваш код від того, щоб залежати від Spring, завалених анотаціями Spring та нерозбірливим магіком. Нещодавно я переніс купу коду до GoogleAppEngine, де Spring не підтримується. Я б хотів, щоб я сховав увесь IoC за ServiceFactory в першу чергу!
Алекс Worden

IoC заохочує анемічну модель домену, яку я зневажаю. Організаціям потрібні способи пошуку своїх послуг, щоб вони могли реалізувати власну поведінку. На цьому шарі ви дійсно не можете обійти потрібний сервіс-локатор.
Джоель

4
Смутний. Я використовую Spring ALL час із анотаціями. Хоча насправді пов'язана певна крива навчання, тепер у мене немає проблем із технічним обслуговуванням, налагодженням, чіткістю, читабельністю .... Я думаю, як ви структуруєте речі - це трюк.
Лоуренс

25

Це правда, що включення класу в application-context.xml уникає необхідності використання getBean. Однак навіть це насправді непотрібне. Якщо ви пишете автономну програму, і не хочете включати свій клас драйверів у application-context.xml, ви можете використовувати наступний код, щоб Spring автоматично перейшов у залежність від драйвера:

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

Мені потрібно було це зробити кілька разів, коли у мене є якийсь окремий клас, який повинен використовувати якийсь аспект мого додатка (наприклад, для тестування), але я не хочу його включати в контекст програми, оскільки це не фактично частина програми. Зауважте також, що це дозволяє уникнути необхідності шукати квасоля за допомогою імені String, яке я завжди вважав некрасивим.


Мені вдалося також успішно використовувати цей метод разом із @Autowiredанотацією.
вибухнув

21

Однією з найкрутіших переваг використання чогось подібного Spring є те, що вам не доведеться з'єднувати об’єкти разом. Голова Зевса розщеплюється, і ваші заняття з'являються, повністю сформовані з усіма своїми залежностями, створеними та підключеними до необхідності. Це магічно і фантастично.

Чим більше ви кажете ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");, тим менше магії ви отримуєте. Менше коду майже завжди краще. Якщо вашому класу дійсно потрібен квасоля ClassINeed, чому ви просто не ввімкнули його?

Однак, щось очевидно потребує створення першого об'єкта. Немає нічого поганого в тому, щоб ваш основний метод придбав квасоля або два за допомогою getBean (), але вам слід уникати цього, тому що коли ви його використовуєте, ви не використовуєте всю магію Весни.


1
Але ОП не каже "ClassINeed", він говорить "BeanNameINeed" - що дозволяє контейнеру IoC створювати примірник для будь-якого класу, налаштованого будь-яким чином. Можливо, він більше схожий на шаблон "локатора обслуговування", ніж на IoC, але він все одно призводить до втрати зв'язку.
HDave

16

Мотивація полягає в написанні коду, який явно не залежить від Spring. Таким чином, якщо ви вирішите переключити контейнери, вам не доведеться переписувати жоден код.

Подумайте про контейнер як про те, що ваш код непомітний, магічно забезпечивши його потреби, не запитуючи його.

Ін'єкція залежності залежить від схеми "локатор обслуговування". Якщо ви збираєтеся шукати залежності за назвою, ви також можете позбутися контейнера DI і використовувати щось на зразок JNDI.


11

Використання @Autowiredабо ApplicationContext.getBean()насправді те саме. В обох способах ви отримуєте боб, налаштований у вашому контексті, і в обох напрямках ваш код залежить від весни. Єдине, чого вам слід уникати - це екземпляр вашого ApplicationContext. Зробіть це лише один раз! Іншими словами, такий рядок

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

слід використовувати лише один раз у вашій програмі.


Ні. Іноді @Autowired або ApplicationContext.getBean () можуть давати зовсім різні боби. Я не впевнений, як це сталося, але я зараз борюся з цим питанням.
Олександр_DJ

4

Ідея полягає в тому, що ви покладаєтесь на ін'єкцію залежності ( інверсія управління або IoC). Тобто ваші компоненти налаштовані на потрібні їм компоненти. Ці залежності вводяться (через конструктор або сетери) - ви не отримуєте цього самостійно.

ApplicationContext.getBean()вимагає, щоб ви чітко назвали квасоля в межах свого компонента. Натомість, використовуючи IoC, ваша конфігурація може визначити, який компонент буде використовуватися.

Це дозволяє легко перепрофілювати додаток з різними компонентними реалізаціями або налаштувати об'єкти для тестування прямолінійно, надаючи глузливі варіанти (наприклад, глузливий DAO, щоб ви не потрапляли в базу даних під час тестування)


4

Інші вказали на загальну проблему (і є дійсними відповідями), але я просто запропоную ще один додатковий коментар: справа не в тому, що НІКОЛИ не слід це робити, а скоріше, щоб це робити якомога менше.

Зазвичай це означає, що це робиться точно один раз: під час завантаження. І тоді просто отримати доступ до «кореневого» бобу, за допомогою якого можна вирішити інші залежності. Це може бути багаторазовий код, як базовий сервлет (якщо розробляються веб-додатки).


4

Одне з приміщень весни - це уникнення з'єднання . Визначте та використовуйте інтерфейси, DI, AOP та уникайте використання ApplicationContext.getBean () :-)


4

Однією з причин є заповітність. Скажіть, у вас цей клас:

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

Як ви можете протестувати цю квасолю? Наприклад:

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

Легко, правда?

Незважаючи на те, що ви все ще залежите від Spring (завдяки анотаціям), ви можете зняти залежність від Spring, не змінюючи жодного коду (лише визначення анотацій), а розробнику тесту не потрібно нічого знати про те, як працює весна (можливо, він повинен у будь-якому випадку, але це дозволяє переглянути та перевірити код окремо від того, що робить весна).

Це все ще можливо зробити те ж саме при використанні ApplicationContext. Однак тоді вам потрібно знущатися над ApplicationContextвеличезним інтерфейсом. Вам або потрібна фіктивна реалізація, або ви можете використовувати глузливі рамки, такі як 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);
    }
}

Це цілком можлива можливість, але я думаю, що більшість людей погодиться, що перший варіант є більш елегантним і робить тест простішим.

Єдиний варіант, який насправді є проблемою, це такий:

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

Для тестування цього потрібні величезні зусилля, або ваш боб намагатиметься підключитися до потокового потоку на кожному тесті. І як тільки у вас з’явиться збій у мережі (або адміністратори в stackoverflow блокують вас через надмірну швидкість доступу), у вас будуть випадкові збої в тестах.

Тож як висновок я б не сказав, що використовувати ApplicationContextбезпосередньо автоматично неправильно, і його слід уникати будь-якою ціною. Однак якщо є кращі варіанти (а є в більшості випадків), тоді використовуйте кращі варіанти.


3

Я знайшов лише дві ситуації, коли потрібно getBean ():

Інші згадали, що використовують getBean () в main () для отримання "головного" квасолі для окремої програми.

Ще одне використання я getBean () в ситуаціях, коли інтерактивна конфігурація користувача визначає макіяж квасолі для конкретної ситуації. Так що, наприклад, частина завантажувальної системи проходить цикл через таблицю бази даних, використовуючи getBean () з визначенням bean names = 'prototype', а потім встановлюючи додаткові властивості. Імовірно, є інтерфейс користувача, який коригує таблицю бази даних, яка була б приємнішою, ніж спроба (повторно) записати контекст програми XML.


3

Існує інший час, коли використання getBean має сенс. Якщо ви переконфігуруєте систему, яка вже існує, де залежності не явно викликаються у весняних файлах контексту. Ви можете розпочати процес, надіславши дзвінки до getBean, так що вам не доведеться одразу все передавати. Таким чином, ви можете повільно нарощувати свою весняну конфігурацію, поставляючи кожен шматок на своє місце і отримуючи шматочки належним чином. Виклики до getBean з часом будуть замінені, але, як ви розумієте структуру коду, або його немає, ви можете запустити процес підключення все більшої кількості бобів і використовувати все менше і менше дзвінків до getBean.


2

однак все ж є випадки, коли вам потрібна схема пошуку локатора послуг. наприклад, у мене є боб контролера, цей контролер може мати деякі сервісні боби за замовчуванням, які можуть бути залежними від конфігурації. хоча може бути багато додаткових або нових послуг, цей контролер може звертатися зараз або пізніше, для чого потрібен локатор сервісів для отримання службових бобів.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.