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

我问了一个普通的Spring问题: 自动生成Spring Bean ,并且有多个人回应说Spring应该尽量避免使用ApplicationContext.getBean() 。 这是为什么?

我还应该如何获得我configurationSpring创build的bean的访问权限?

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

修订

我接受下面的答案,但是这里是Martin Fowler的一个替代scheme,他讨论了dependency injection与使用服务定位器 (实质上与调用一个包装的ApplicationContext.getBean() )相同的优点 。

部分地,Fowler指出:“ 使用服务定位器,应用程序类通过消息显式地向定位器请求(服务)。注入没有明确的请求,服务出现在应用程序类中 – 因此是控制的反转。控制反转是框架的一个共同特征,但它是有代价的,当你试图debugging的时候,它往往很难理解,并导致问题,所以总的来说,我宁愿避免它[控制反转],除非我需要它,这并不是说这是一件坏事,只是我认为它需要在更简单的select上certificate自己。

我在对另一个问题的评论中提到了这一点,但是控制反转的整个想法是让你的class级不知道或不在乎他们如何获得他们所依赖的对象 。 这使得可以随时更改您使用的给定依赖项的实现types。 它也使得类很容易testing,因为你可以提供依赖的模拟实现。 最后,它使课程变得更简单 ,更专注于自己的核心责任。

调用ApplicationContext.getBean()不是控制反转! 尽pipe改变为给定的bean名configuration了什么实现仍然很容易,但是这个类现在直接依赖于Spring来提供这种依赖关系,并且无法以其他方式获取它。 你不能仅仅在testing类中做自己的模拟实现,而是自己把它传递给它。 这基本上破坏了Spring作为dependency injection容器的目的。

无论你想说什么:

 MyClass myClass = applicationContext.getBean("myClass"); 

你应该改为例如声明一个方法:

 public void setMyClass(MyClass myClass) { this.myClass = myClass; } 

然后在你的configuration中:

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

服务定位器优于控制反转(IoC)的原因是:

  1. 服务定位器对于其他人来说更容易遵循你的代码。 IoC是“魔术”,但维护程序员必须了解你复杂的Springconfiguration和所有无数的位置,以弄清楚如何连接你的对象。

  2. IoC对debuggingconfiguration问题非常糟糕。 在某些types的应用程序中,如果configuration错误,应用程序将无法启动,而且您可能没有机会逐步debuggingdebugging器。

  3. IoC主要是基于XML的(注解改进了一些东西,但是仍然有很多XML)。 这意味着开发人员不能在你的程序上工作,除非他们知道Spring定义的所有魔术标签。 了解Java已经不够了。 这妨碍了程序员的经验减less(即,当更简单的解决scheme(例如服务定位器)将满足相同的要求时,使用更复杂的解决scheme实际上是糟糕的devise)。 另外,支持诊断XML问题比支持Java问题弱得多。

  4. dependency injection更适合更大的程序。 大多数情况下,额外的复杂性是不值得的。

  5. 通常情况下,Spring会被用于“以后可能需要更改实现”的情况。 除此之外,还有其他方法可以实现Spring IoC的复杂性。

  6. 对于Web应用程序(Java EE WARs),Spring上下文在编译时被有效绑定(除非您希望运算符在爆炸战争中绕过上下文)。 您可以让Spring使用属性文件,但是servlet属性文件需要位于预定位置,这意味着您不能在同一个盒子上部署同一时间的多个servlet。 您可以使用Spring和JNDI在servlet启动时更改属性,但是如果您使用JNDI作为pipe理员可修改的参数,则对Spring本身的需求减less(因为JNDI实际上是服务定位器)。

  7. 使用Spring,如果Spring正在调度您的方法,您可能会失去程序控制权。 这很方便,适用于很多types的应用程序,但不是全部。 当需要在初始化过程中创build任务(线程等)时,可能需要控制程序stream程,或者需要Spring修改内容绑定到WAR时不知道的可修改资源。

Spring对事务pipe理非常有用,并且有一些优点。 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; } } 

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

使用类似Spring的最酷的好处之一就是你不必把你的对象连接在一起。 宙斯的脑袋分裂开来,你的class级出现,完全形成了所有依赖,并根据需要进行连接。 这是神奇的和美妙的。

你说的越多ClassINeed classINeed = (ClassINeed)ApplicationContext.getBean("classINeed"); ,你得到的魔法越less。 更less的代码几乎总是更好。 如果你的类真的需要一个ClassINeed bean,为什么不把它连接起来呢?

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

动机是编写不依赖于Spring的代码。 这样,如果您select切换容器,则不必重写任何代码。

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

dependency injection是“服务定位器”模式的一个对应点。 如果你打算通过名字来查找依赖关系,那么你也可以摆脱DI容器,并使用类似于JNDI的东西。

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

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

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

这个想法是,你依赖dependency injection( 控制反转 ,或IoC)。 也就是说,您的组件configuration了他们需要的组件。 这些依赖关系被注入 (通过构造函数或者setter) – 你自己不会得到。

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

这使您可以轻松地使用不同的组件实现重新连线您的应用程序,或通过提供嘲讽的变体(例如模拟的DAO,以便在testing过程中不会碰到数据库)以简单的方式configuration对象进行testing。

其他人指出了一般问题(也是有效的答案),但我只是提出一个额外的评论:并不是你永远不应该这样做,而是尽可能less做。

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

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

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

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

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

还有一次使用getBean是有道理的。 如果您正在重新configuration已经存在的系统,那么在spring的上下文文件中没有显式地调用依赖关系。 你可以通过调用getBean来启动这个过程,所以你不必一次把它连接起来。 这样,你可以慢慢地build立你的弹簧configuration,把每一块地方随着时间的推移,并正确排列位。 对getBean的调用最终将被replace,但是当您了解代码的结构或缺less代码的结构时,可以开始连接越来越多的bean以及使用越来越less的getBean调用。

但是,仍然存在您需要服务定位器模式的情况。 例如,我有一个控制器bean,这个控制器可能有一些默认的服务bean,可以通过configurationdependency injection。 同时也可以有许多额外的或新的服务,这个控制器现在或以后可以调用,然后需要服务定位器检索服务bean。

其中一个原因是可testing性。 说你有这个class级:

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

你怎么testing这个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的依赖(只有注解定义),而且testing开发人员不需要知道spring是如何工作的它允许单独检查和testing代码,弹簧是什么)。

使用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); } } 

这是相当可能的,但我想大多数人会同意,第一个选项更优雅,使testing更简单。

唯一的select是真正的问题是这样的:

 @Component class MyBean { @Autowired MyBean(StringOutput out) { out.print(new HttpLoader().load("http://stackoverflow.com")); } } 

testing这需要付出巨大的努力,否则你的bean将试图连接到每个testing的stackoverflow。 只要你有一个networking故障(或者由于访问速度过高,pipe理员在stackoverflow阻止你),你将有随机失败的testing。

所以作为一个结论,我不会说直接使用ApplicationContext是自动错误的,应该不惜一切代价避免。 但是,如果有更好的select(并且在大多数情况下),那么使用更好的选项。