Почему SpringCon ApplicationContext.getBean считается плохим?

270

Я задал общий вопрос Spring: Auto-cast Spring Beans, и несколько человек ответили, что он вызывает Spring.ApplicationContext.getBean() следует избегать как можно больше. Это почему?

Как еще мне получить доступ к bean-компонентам, которые я настроил для создания Spring?

Я использую Spring в не-веб-приложении и планировал доступ к общему ApplicationContextобъекту, как описано LiorH .

поправка

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

В частности, Фаулер заявляет: « С помощью локатора сервиса класс приложения запрашивает его [сервис] явно посредством сообщения локатору. При внедрении нет явного запроса, сервис появляется в классе приложения - отсюда и инверсия управления. Инверсия контроля - это общая черта фреймворков, но это то, что имеет свою цену. Это трудно понять и приводит к проблемам при попытке отладки. Поэтому в целом я предпочитаю избегать этого [Инверсия контроля ] если только мне это не нужно. Это не значит, что это плохо, просто я думаю, что нужно оправдываться над более простой альтернативой ».

Vinnie
источник

Ответы:

202

Я упомянул об этом в комментарии к другому вопросу, но вся идея Inversion of Control заключается в том, чтобы никто из ваших классов не знал и не заботился о том, как они получают объекты, от которых зависят . Это позволяет легко изменить тип реализации данной зависимости, которую вы используете в любое время. Это также облегчает тестирование классов, поскольку вы можете предоставить имитационные реализации зависимостей. Наконец, это делает классы более простыми и более сосредоточенными на их основной ответственности.

призвание 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>

Spring будет автоматически myClassвведен в myOtherClass.

Объявите все таким образом, и в корне всего этого есть что-то вроде:

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

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

ColinD
источник
3
Есть ли что-нибудь связанное с этим, что работает только с аннотациями при создании new MyOtherClass()объекта? Я знаю о @Autowired, но я когда-либо использовал его только на полях, и он разбивается на new MyOtherClass()...
Тим
71
Это неправда, что ApplicationContext.getBean () не является IoC. И не обязательно, чтобы все ваши классы были созданы в Spring. Это неуместная догма. Если ApplicationContext сам внедряется, то вполне нормально попросить его создать экземпляр компонента таким способом - и создаваемые им компоненты могут быть различными реализациями, основанными на изначально введенном ApplicationContext. Например, у меня есть сценарий, в котором я динамически создаю новые экземпляры bean-компонентов, основанные на имени bean-компонента, которое неизвестно во время компиляции, но соответствует одной из реализаций, определенных в моем файле spring.xml.
Алекс Уорден
3
Согласитесь с Алексом, у меня та же проблема, когда фабричный класс будет знать только то, какой bean-компонент или реализацию использовать во время выполнения через взаимодействие с пользователем, я думаю, что именно здесь появляется интерфейс ContextAware
Бен,
3
@elbek: applicationContext.getBeanэто не внедрение зависимостей: это прямой доступ к фреймворку, использующий его как локатор службы .
ColinD
6
@herman: Я не знаю о Spring, потому что я не использовал его долгое время, но в JSR-330 / Guice / Dagger вы бы делали это, вставляя Provider<Foo>вместо a Fooи вызывая provider.get()каждый раз, когда вам нужно новый экземпляр. Нет ссылки на сам контейнер, и вы можете легко создать Providerдля тестирования.
ColinD
65

Причины, по которым сервисный локатор предпочтительнее инверсионного контроля (IoC):

  1. Сервис Локатор намного, намного проще для других людей следовать в вашем коде. IoC - «волшебство», но программисты по техническому обслуживанию должны понимать ваши замысловатые конфигурации Spring и все множество мест, чтобы выяснить, как вы подключили свои объекты.

  2. IoC ужасен для отладки проблем конфигурации. В некоторых классах приложений приложение не будет запускаться при неправильной настройке, и у вас может не быть возможности прокрутить то, что происходит с отладчиком.

  3. IoC в основном основан на XML (аннотации улучшают ситуацию, но XML по-прежнему существует). Это означает, что разработчики не могут работать с вашей программой, если они не знают всех магических тегов, определенных Spring. Это недостаточно хорошо, чтобы знать Java больше. Это мешает программистам с меньшим опытом (то есть, на самом деле плохой дизайн - использовать более сложное решение, когда более простое решение, такое как Service Locator, будет соответствовать тем же требованиям). Кроме того, поддержка диагностики проблем XML намного слабее, чем поддержка проблем Java.

  4. Внедрение зависимостей больше подходит для больших программ. Большую часть времени дополнительная сложность не стоит.

  5. Часто Spring используется в случае, если вы «захотите изменить реализацию позже». Есть и другие способы достижения этого без сложности Spring IoC.

  6. Для веб-приложений (Java EE WARs) контекст Spring эффективно связывается во время компиляции (если вы не хотите, чтобы операторы разбирались с контекстом в разнесенной войне). Вы можете сделать так, чтобы Spring использовал файлы свойств, но файлы сервлетов должны быть в заранее определенном месте, что означает, что вы не можете развернуть несколько сервлетов одновременно в одном окне. Вы можете использовать Spring с JNDI для изменения свойств во время запуска сервлета, но если вы используете JNDI для изменяемых администратором параметров, необходимость в самом Spring уменьшается (поскольку JNDI фактически является локатором служб).

  7. С Spring вы можете потерять управление программой, если Spring отправляет ваши методы. Это удобно и работает для многих типов приложений, но не для всех. Вам может понадобиться контролировать поток программы, когда вам нужно создавать задачи (потоки и т. Д.) Во время инициализации или вам нужны изменяемые ресурсы, о которых Spring не знал, когда контент был связан с вашей WAR.

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

Моа
источник
7
Плюс - ваш ServiceLocator всегда может использовать IoC из Spring, абстрагируя ваш код от зависимости от Spring, изобилующего аннотациями Spring и неразборчивой магией. Я недавно портировал кучу кода в GoogleAppEngine, где Spring не поддерживается. Я бы хотел спрятать все IoC за ServiceFactory!
Алекс Уорден
IoC поддерживает модель анемичной области, которую я презираю. Бинам сущностей нужен способ поиска своих сервисов, чтобы они могли реализовать свое собственное поведение. На этом уровне вы не можете обойтись без необходимости поиска сервисов.
Джоэл
4
Bizar. Я использую Spring все время с аннотациями. Хотя действительно существует определенная кривая обучения, сейчас у меня нет никаких проблем в обслуживании, отладке, ясности, читабельности…. Я думаю, как вы структурируете вещи, это хитрость.
Лоуренс
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;    
    }
}

Я должен был сделать это пару раз, когда у меня есть какой-то отдельный класс, который должен использовать какой-то аспект моего приложения (например, для тестирования), но я не хочу включать его в контекст приложения, потому что это не так. на самом деле часть приложения. Также обратите внимание, что это избавляет от необходимости искать бин, используя строковое имя, которое я всегда считал уродливым.

iftheshoefritz
источник
Я смог успешно использовать этот метод с @Autowiredаннотацией также.
Блонг
21

Одним из самых классных преимуществ использования чего-то вроде Spring является то, что вам не нужно соединять свои объекты вместе. Голова Зевса распадается, и ваши классы появляются полностью сформированными со всеми зависимостями, созданными и подключенными по мере необходимости. Это волшебно и фантастично.

Чем больше вы говорите ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed"); , тем меньше магии вы получаете. Меньше кода почти всегда лучше. Если вашему классу действительно нужен компонент ClassINeed, почему вы просто не подключили его?

Тем не менее, очевидно, что что-то должно создать первый объект. В вашем основном методе нет ничего плохого в том, чтобы получить один или два компонента с помощью getBean (), но вам следует избегать этого, потому что всякий раз, когда вы его используете, вы на самом деле не используете всю магию Spring.

Брэндон Ярбро
источник
1
Но OP не говорит «ClassINeed», он говорит «BeanNameINeed» - что позволяет контейнеру IoC создавать экземпляр любого класса, настроенного любым способом. Возможно, он больше похож на шаблон «поиск сервисов», чем на IoC, но все равно приводит к слабой связи.
HDave
16

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

Думайте о контейнере как о чем-то невидимом для вашего кода, волшебно обеспечивающем его потребности без всяких вопросов.

Внедрение зависимостей является контрапунктом к шаблону «поиск сервисов». Если вы собираетесь искать зависимости по имени, вы можете также избавиться от контейнера DI и использовать что-то вроде JNDI.

Эриксон
источник
11

Использование @Autowiredили ApplicationContext.getBean()действительно то же самое. В обоих случаях вы получаете компонент, настроенный в вашем контексте, и в обоих случаях ваш код зависит от Spring. Единственное, чего вам следует избегать - это создать экземпляр ApplicationContext. Делай это только один раз! Другими словами, строка как

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

следует использовать только один раз в вашем приложении.

easyplanner.cu.cc
источник
Нет. Иногда @Autowired или ApplicationContext.getBean () могут создавать абсолютно разные bean-компоненты. Я не уверен, как это произошло, но сейчас я борюсь с этим вопросом.
Oleksandr_DJ
4

Идея заключается в том, что вы полагаетесь на внедрение зависимостей ( инверсия управления или IoC). То есть ваши компоненты настроены так, как им нужно. Эти зависимости вводятся (через конструктор или сеттеры) - тогда вы сами не получите.

ApplicationContext.getBean()требует, чтобы вы назвали бин явно внутри вашего компонента. Вместо этого, используя IoC, ваша конфигурация может определить, какой компонент будет использоваться.

Это позволяет вам легко перемонтировать ваше приложение с различными реализациями компонентов или настроить объекты для тестирования простым способом, предоставляя имитированные варианты (например, поддельный DAO, чтобы вы не попали в базу данных во время тестирования)

Брайан Агнью
источник
4

Другие указали на общую проблему (и являются правильными ответами), но я просто предложу еще один комментарий: это не значит, что вы НИКОГДА не должны делать этого, а скорее делать это как можно меньше.

Обычно это означает, что это делается ровно один раз: во время начальной загрузки. И тогда это просто доступ к «корневому» компоненту, с помощью которого можно разрешить другие зависимости. Это может быть повторно используемый код, например базовый сервлет (при разработке веб-приложений).

StaxMan
источник
4

Одно из помещений Spring - избегать сцепления . Определите и используйте интерфейсы, DI, AOP и избегайте использования ApplicationContext.getBean () :-)

sourcerebels
источник
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 без изменения какого-либо кода (только определения аннотаций), и разработчику теста не нужно ничего знать о том, как работает 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 в каждом тесте. И как только у вас произойдет сбой в сети (или администраторы в stackoverflow блокируют вас из-за чрезмерной скорости доступа), у вас будут случайно провальные тесты.

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

янки
источник
3

Я обнаружил только две ситуации, когда требуется getBean ():

Другие упоминали об использовании getBean () в main () для извлечения «основного» bean-компонента для отдельной программы.

Другое использование getBean (), которое я использовал, - это ситуации, когда интерактивная конфигурация пользователя определяет структуру bean-компонента для конкретной ситуации. Так, например, часть загрузочной системы проходит по таблице базы данных, используя getBean () с определением компонента scope = 'prototype', а затем устанавливает дополнительные свойства. Предположительно, существует пользовательский интерфейс, который настраивает таблицу базы данных, что было бы более удобным, чем попытка (пере) переписать контекст приложения XML.

nsayer
источник
3

Есть другое время, когда использование getBean имеет смысл. Если вы реконфигурируете систему, которая уже существует, где зависимости явно не вызываются в контекстных файлах Spring. Вы можете начать процесс, введя вызовы getBean, чтобы вам не приходилось подключать все сразу. Таким образом, вы можете постепенно наращивать конфигурацию пружины, со временем размещая каждый элемент на месте и правильно расставляя биты. Вызовы getBean в конечном итоге будут заменены, но, как только вы поймете структуру кода или его отсутствие, вы можете начать процесс подключения все большего числа компонентов и использовать все меньше и меньше вызовов getBean.

Тони Джакконе
источник
2

однако, все еще есть случаи, когда вам нужен шаблон поиска сервисов. например, у меня есть bean-компонент контроллера, у этого контроллера могут быть некоторые служебные bean-компоненты по умолчанию, которые могут быть зависимы от конфигурации. в то время как также может быть много дополнительных или новых сервисов, этот контроллер может вызывать сейчас или позже, которые затем нуждаются в локаторе сервисов для получения сервисных бинов.

lwpro2
источник
0

Вы должны использовать: ConfigurableApplicationContext вместо ApplicationContext

Хай Нгуен Ле
источник