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

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

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

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

поправка

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

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

13 ответов

Решение

Я упомянул об этом в комментарии к другому вопросу, но вся идея 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() где-нибудь еще!

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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://stackru.com"));
    }
}

Как вы можете проверить этот боб? Например, вот так:

class MyBeanTest {
    public void creatingMyBean_writesStackruPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();

        // execution
        new MyBean(Collections.singletonMap("https://stackru.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://stackru.com"));
    }
}
class MyBeanTest {
    public void creatingMyBean_writesStackruPageToOutput() {
        // setup
        String stackOverflowHtml = "dummy";
        StringBuilder result = new StringBuilder();
        ApplicationContext context = Mockito.mock(ApplicationContext.class);
        Mockito.when(context.getBean(HttpLoader.class))
            .thenReturn(Collections.singletonMap("https://stackru.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://stackru.com"));
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

Другие вопросы по тегам