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

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

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

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

Поправка

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

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

Ответ 1

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

Вызов ApplicationContext.getBean() не является инверсией управления! Хотя по-прежнему легко изменить, какая настройка была настроена для данного имени bean, теперь класс полагается непосредственно на 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() где-либо еще!

Ответ 2

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

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

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

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

  • Инъекция зависимостей больше подходит для более крупных программ. В большинстве случаев дополнительная сложность не стоит.

  • Часто Spring используется, если вы "захотите изменить реализацию позже". Существуют и другие способы достижения этого без сложности Spring IoC.

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

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

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

Ответ 3

Верно, что включение класса в 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;    
    }
}

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

Ответ 4

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

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

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

Ответ 5

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

Подумайте о контейнере, поскольку что-то невидимо для вашего кода, магически обеспечивая его потребности, не спрашивая.

Инъекция зависимостей - это контрапункт шаблона "locator". Если вы собираетесь искать зависимости по имени, вы можете также избавиться от контейнера DI и использовать что-то вроде JNDI.

Ответ 6

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

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

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

Ответ 7

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

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

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

Ответ 8

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

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

Ответ 9

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

Ответ 10

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

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

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

Ответ 11

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

Ответ 12

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

Ответ 13

Одна из причин - тестируемость. Скажем, у вас есть этот класс:

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

Как вы можете протестировать этот bean? Например. например:

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 работает (возможно, ему все равно, но он позволяет просматривать и тестировать код отдельно от того, что делает 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"));
    }
}

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

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