我问了一个常见的Spring问题:自动转换Spring bean,很多人回答说应该尽可能避免调用Spring的ApplicationContext.getBean()。为什么呢?

我还应该如何访问我配置Spring创建的bean呢?

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

修正案

我接受下面的答案,但这里有Martin Fowler的另一种观点,他讨论了依赖注入与使用服务定位器(本质上与调用包装的ApplicationContext.getBean()相同)的优点。

In part, Fowler states, "With service locator the application class asks for it [the service] explicitly by a message to the locator. With injection there is no explicit request, the service appears in the application class - hence the inversion of control. Inversion of control is a common feature of frameworks, but it's something that comes at a price. It tends to be hard to understand and leads to problems when you are trying to debug. So on the whole I prefer to avoid it [Inversion of Control] unless I need it. This isn't to say it's a bad thing, just that I think it needs to justify itself over the more straightforward alternative."


当前回答

我在另一个问题的评论中提到了这一点,但控制反转的整个思想是让你的任何类都不知道或关心它们如何获得它们所依赖的对象。这使得您可以随时更改所使用的给定依赖项的实现类型。它还使类易于测试,因为您可以提供依赖关系的模拟实现。最后,它使类更简单,更专注于它们的核心职责。

调用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是最核心的类,它至少间接地依赖于程序中的所有其他服务。当引导时,在你的主方法中,你可以调用applicationContext.getBean("myApplication"),但你不需要在其他任何地方调用getBean() !

其他回答

但是,仍然有一些情况需要使用服务定位器模式。 例如,我有一个控制器bean,这个控制器可能有一些默认的服务bean,这些服务bean可以通过配置注入依赖项。 虽然这个控制器现在或以后还可以调用许多附加的或新的服务,但这些服务需要服务定位器来检索服务bean。

原因之一是可测试性。假设你有这样一个类:

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,这是一个巨大的接口。你要么需要一个虚拟的实现,要么你可以使用一个mock框架,比如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是自动错误的,应该不惜一切代价避免。然而,如果有更好的选择(大多数情况下都有),那么就使用更好的选择。

其他人指出了普遍的问题(并且是有效的答案),但我只想提供一个额外的评论:并不是说你永远不应该这样做,而是尽可能少地做。

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

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

把容器想象成代码看不见的东西,神奇地提供它的需要,而不需要被要求。

依赖注入是“服务定位器”模式的对应。如果您打算按名称查找依赖项,那么您也可以摆脱DI容器,使用JNDI之类的东西。

使用Spring之类的东西的一个最酷的好处是,您不必将对象连接在一起。Zeus的头部打开,您的类就出现了,并且根据需要创建并连接了它们的所有依赖项。这是神奇的和奇妙的。

你越是说ClassINeed ClassINeed = (ClassINeed)ApplicationContext.getBean(" ClassINeed ");,你得到的魔法就越少。代码越少越好。如果您的类确实需要ClassINeed bean,为什么不直接将它连接进来呢?

也就是说,显然需要创建第一个对象。主方法通过getBean()获取一两个bean并没有什么问题,但是应该避免使用它,因为无论何时使用它,都没有真正使用Spring的所有魔力。