为什么Spring的ApplicationContext.getBean被认为是不好的?


270

我问了一个一般性的Spring问题:自动播发Spring Bean,并让多个人回答说ApplicationContext.getBean()应尽可能避免调用Spring 。这是为什么?

我还应该如何访问配置了Spring创建的Bean?

我在非Web应用程序中使用Spring,并计划按LiorH的描述访问共享ApplicationContext对象。

修正案

我接受以下答案,但这是Martin Fowler的另一种选择,他讨论了依赖注入与使用Service Locator(本质上与调用wraped相同ApplicationContext.getBean()的优点

Fowler在某种程度上说:“ 使用服务定位器,应用程序类通过向定位器的消息显式地请求[服务]。使用注入时,没有显式请求,服务将出现在应用程序类中-因此控制权反转。控制反转是框架的一个共同特征,但这是有代价的。当您尝试进行调试时,它往往难以理解并导致问题。因此,总的来说,我希望避免这种情况。除非我需要它。这并不是说这是一件坏事,只是我认为它需要证明自己比更直接的选择更合理。

Answers:


202

我在另一个问题的评论中提到了这一点,但是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会自动注入myClassmyOtherClass

以这种方式声明所有内容,并且从根本上讲,它们类似于:

<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()..
蒂姆(Tim)

70
ApplicationContext.getBean()不是IoC并不是真的。Niether必须在Spring之前实例化所有类。那是不合适的教条。如果ApplicationContext本身是注入的,那么要求它以这种方式实例化一个bean很好-它创建的bean可以是基于最初注入的ApplicationContext的不同实现。例如,我有一个场景,其中我基于一个在编译时未知但与spring.xml文件中定义的实现之一匹配的bean名称动态创建新的bean实例。
Alex Worden

3
与亚历克斯同意,我有同样的问题,其中一个工厂类只知道通过用户交互在运行时使用的豆或实施,我认为这是在上下文感知接口进来

3
@elbek:applicationContext.getBean不是依赖项注入:它直接访问框架,并将其用作服务定位器
ColinD

6
@herman:我不了解Spring,因为我已经很长时间没有使用它了,但是在JSR-330 / Guice / Dagger中,您可以通过注入a Provider<Foo>而不是a Foo并在provider.get()每次需要的时候调用a 来做到这一点。新实例。没有引用容器本身,您可以轻松创建一个Provider用于测试的容器。
ColinD

64

选择服务定位器而不是控制反转(IoC)的原因是:

  1. 服务定位器对于其他人来说,遵循您的代码要容易得多。IoC是“魔术”,但是维护程序员必须了解复杂的Spring配置以及无数的位置,才能弄清楚如何连接对象。

  2. IoC对于调试配置问题非常糟糕。在某些类别的应用程序中,如果配置错误,则该应用程序将无法启动,并且您可能没有机会逐步调试程序。

  3. IoC主要基于XML(注解可以改善功能,但仍然有很多XML)。这意味着开发人员除非知道Spring定义的所有魔术标签,否则无法使用您的程序。仅仅了解Java还不够。这阻碍了经验较少的程序员(即,当较简单的解决方案(例如Service Locator)可以满足相同要求时,实际上使用较复杂的解决方案的设计实际上很差)。另外,对XML问题的诊断支持比对Java问题的支持要弱得多。

  4. 依赖注入更适合大型程序。大多数情况下,额外的复杂性是不值得的。

  5. 如果您“以后可能想更改实现”,通常会使用Spring。还有其他方法可以实现这一点,而无需Spring IoC的复杂性。

  6. 对于Web应用程序(Java EE WAR),Spring上下文在编译时有效地绑定了(除非您希望操作员在爆炸战争中陷入困境)。您可以使Spring使用属性文件,但是使用servlet,则属性文件将需要位于预定位置,这意味着您不能在同一盒子上同时部署多个servlet。您可以将Spring与JNDI一起使用,以在servlet启动时更改属性,但是如果您将JNDI用于管理员可修改的参数,则对Spring本身的需求会减少(因为JNDI实际上是服务定位器)。

  7. 使用Spring,如果Spring正在分派到您的方法,则可能会失去程序控制权。这很方便,可用于许多类型的应用程序,但不是全部。当您需要在初始化期间创建任务(线程等)或需要Spring不知道何时将内容绑定到WAR时需要的可修改资源时,您可能需要控制程序流程。

Spring对于事务管理非常有用,并且具有一些优势。只是IoC在许多情况下可能会过度设计,并给维护人员带来不必要的复杂性。不要在不首先考虑不使用IoC的情况下自动使用IoC。


7
另外-您的ServiceLocator始终可以使用Spring的IoC,使您的代码脱离依赖Spring的抽象,充斥着Spring注释和难以理解的魔术。我最近将一堆代码移植到了不支持Spring的GoogleAppEngine上。我希望我首先将所有IoC隐藏在ServiceFactory后面!
Alex Worden

IoC鼓励我鄙视的贫血领域模型。实体bean需要一种查找其服务的方法,以便它们可以实现自己的行为。在那一层,您真的无法解决需要服务定位器的问题。
乔尔(Joel)

4
比扎尔 我一直在使用带有注解的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;    
    }
}

当我有某种需要使用应用程序某些方面的独立类(例如,用于测试)时,我需要做几次,但是我不想将其包含在应用程序上下文中,因为它不是实际上是应用程序的一部分。还要注意,这避免了使用String名称查找bean的需求,我一直认为String名称很丑陋。


我也能够通过@Autowired注释成功使用此方法。
2013年

21

使用诸如Spring之类的东西的最酷的好处之一就是您不必将对象连接在一起。宙斯的头裂开了,您的班级出现了,完全形成了他们的所有依赖关系,并根据需要进行了连接。神奇而神奇。

您说的越多ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed");,获得的魔力就越少。更少的代码几乎总是更好。如果您的班级确实需要ClassINeed bean,为什么不直接将其连接?

也就是说,显然需要创建第一个对象。通过getBean()获取一两个bean的主要方法没有什么问题,但是您应该避免使用它,因为每当使用它时,您实际上并没有真正使用Spring的所有功能。


1
但是OP并没有说“ ClassINeed”,而是说了“ BeanNameINeed”-允许IoC容器在以任何方式配置的任何类上创建实例。也许它比IoC更像是“服务定位器”模式,但是它仍然导致松耦合。
HDave

16

这样做的动机是编写不显式依赖Spring的代码。这样,如果您选择切换容器,则无需重写任何代码。

可以将容器视为您的代码看不到的东西,神奇地满足了它的需求,而无需询问。

依赖注入是“服务定位器”模式的对立点。如果要按名称查找依赖项,则最好摆脱DI容器并使用类似JNDI的东西。


11

使用@AutowiredApplicationContext.getBean()确实是同一回事。通过两种方式,您都可以获取在上下文中配置的bean,并且两种方式的代码都取决于spring。您唯一应避免的是实例化ApplicationContext。只做一次!换句话说,像

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

在您的应用程序中只能使用一次。


不。有时@Autowired或ApplicationContext.getBean()可能会产生完全不同的bean。我不确定这是怎么发生的,但是我现在正在为这个问题而苦苦挣扎。
Oleksandr_DJ

4

这个想法是您依赖于依赖项注入(控制反转或IoC)。也就是说,您的组件已配置了所需的组件。这些依赖项被注入(通过构造器或setter)-您不会自己搞定。

ApplicationContext.getBean()要求您在组件内明确命名一个bean。相反,通过使用IoC,您的配置可以确定将使用哪个组件。

这使您可以轻松地将应用程序与不同的组件实现重新连接,或者通过提供模拟的变体(例如模拟的DAO,从而在测试过程中不会访问数据库)以简单的方式配置对象以进行测试。


4

其他人已经指出了普遍的问题(并且是有效的答案),但是我仅提供另一条评论:不是您从不应该这样做,而是应该尽可能少地这样做。

通常,这意味着只完成一次:在引导过程中。然后只是访问“根” bean,通过它可以解决其他依赖关系。这可以是可重用的代码,例如基础servlet(如果正在开发Web应用程序)。


4

Spring前提之一是避免耦合。定义和使用接口,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的依赖,而无需更改任何代码(仅注释定义),并且测试开发人员无需了解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直接使用直接是错误的,应该不惜一切代价避免使用。但是,如果有更好的选择(大多数情况下都有),请使用更好的选择。


3

我只发现需要getBean()的两种情况:

其他人提到在main()中使用getBean()来获取独立程序的“ main” bean。

我对getBean()的另一种用法是在交互式用户配置确定特定情况下的bean组成的情况下。因此,例如,部分引导系统使用带有scope ='prototype'bean定义的getBean()遍历数据库表,然后设置其他属性。大概有一个UI可以调整数据库表,它比尝试(重新)编写应用程序上下文XML更友好。


3

还有一次使用getBean有意义。如果要重新配置已经存在的系统,则在Spring上下文文件中不会显式调出依赖项。您可以通过调用getBean来启动该过程,这样就不必一次将其连接起来。这样,您就可以缓慢地建立弹簧构型,将每个零件随时间推移放置在适当的位置,并使钻头正确排列。最终将替换对getBean的调用,但是,由于您了解或缺乏代码的结构,因此可以开始连接越来越多的bean和使用越来越少的对getBean的调用的过程。


2

但是,在某些情况下,您仍然需要服务定位器模式。例如,我有一个控制器bean,此控制器可能有一些默认服务bean,可以通过配置注入依赖项。尽管还可能有许多其他服务或新服务,但该控制器现在或以后可以调用,然后需要服务定位器来检索服务bean。


0

您应该使用:ConfigurableApplicationContext而不是用于ApplicationContext

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.