为什么Spring's ApplicationContext。getBean被认为不好?

我问了一个一般的Spring问题:自动铸造春豆,有很多人回答说,应该尽可能避免调用Spring的ApplicationContext.getBean()。为什么呢?

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

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

修正案

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

Fowler在部分内容中说,“使用服务定位器,应用程序类通过向定位器发送消息显式地请求它[服务]。对于注入,没有显式的请求,服务出现在应用程序类中——因此是控制反转。 控制反转是框架的一个常见特性,但它是有代价的。当您尝试调试时,它往往难以理解并导致问题。所以总的来说,除非我需要,否则我宁愿避免它(控制反转)。这并不是说这是一件坏事,只是我认为它需要证明自己比更直接的选择
391546 次浏览

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

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

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

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

ApplicationContext.getBean()要求你在组件中显式地命名一个bean。相反,通过使用IoC,您的配置可以确定将使用什么组件。

这让你可以轻松地用不同的组件实现重新连接应用程序,或者通过提供模拟变量(例如,模拟DAO,这样你就不会在测试期间碰到数据库)以一种直接的方式配置测试对象。

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

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

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

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

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

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

调用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() !

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

其他人已经提到在main()中使用getBean()为独立程序获取“主”bean。

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

Spring的前提之一是避免耦合。定义和使用接口,DI, AOP,避免使用ApplicationContext.getBean():-)

的确,在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的需要,我一直认为这是丑陋的。

选择服务定位器而不是控制反转(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 WARs), Spring上下文在编译时被有效地绑定(除非你想让操作符在已爆炸的war中四处搜寻上下文)。您可以让Spring使用属性文件,但是使用servlet时,属性文件需要位于预先确定的位置,这意味着您不能在同一个机器上同时部署多个servlet。您可以在servlet启动时使用带有JNDI的Spring来更改属性,但是如果您使用JNDI作为管理员可修改的参数,那么对Spring本身的需求就会减少(因为JNDI实际上是一个服务定位器)。

  7. 使用Spring,如果Spring调度到你的方法,你可能会失去程序控制。这很方便,适用于许多类型的应用程序,但不是所有类型的应用程序。在初始化过程中,当您需要创建任务(线程等)或需要可修改的资源时,您可能需要控制程序流,而当内容绑定到WAR时,Spring并不知道这些资源。

Spring非常适合事务管理,并且有一些优点。只是IoC在许多情况下会过度设计,给维护者带来不必要的复杂性。不要在没有考虑不使用IoC的情况下自动使用它。

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

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

这样的行
ApplicationContext context = new ClassPathXmlApplicationContext("AppContext.xml");

在应用程序中只应使用一次。

在另一种情况下,使用getBean是有意义的。如果您正在重新配置一个已经存在的系统,其中依赖项在spring上下文文件中没有显式地调用。您可以通过对getBean进行调用来开始这个过程,这样您就不必一次性将其全部连接起来。通过这种方式,您可以慢慢地建立您的弹簧配置,随着时间的推移,将每个部件放在适当的位置,并使比特正确排列。对getBean的调用最终将被替换,但是当您理解了代码的结构(或者缺乏这种结构)之后,您可以开始连接越来越多的bean,并使用越来越少的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://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自动是错误的,应该不惜一切代价避免。然而,如果有更好的选择(大多数情况下都有),那么就使用更好的选择。

你应该使用:ConfigurableApplicationContext而不是for ApplicationContext