Why is Spring's ApplicationContext.getBean considered bad?
我问了一个普通的弹簧问题:自动铸造的弹簧豆,有很多人回答说,应该尽量避免打电话给弹簧的
否则,我应该如何访问我配置的Spring要创建的bean?
我在非Web应用程序中使用Spring,并计划访问liorh所描述的共享
修正案
我接受下面的答案,但下面是Martin Fowler的另一个观点,他讨论了依赖注入与使用服务定位器(本质上与调用包装的
福勒表示,在某种程度上,"对于服务定位器,应用程序类通过向定位器发送消息明确地请求它[服务]。对于注入,没有显式的请求,服务出现在应用程序类中——因此控制反转。控制反转是框架的一个常见特性,但它是有代价的。当您尝试调试时,它往往很难理解并导致问题。所以总的来说,除非我需要它,否则我宁愿避免它。这并不是说这是一件坏事,只是我认为它需要为自己的选择寻找更直接的理由。"
我在对另一个问题的评论中提到了这一点,但是控制反转的整个思想是让您的类中的任何一个都不知道或不关心它们是如何得到它们所依赖的对象的。这使得您可以很容易地随时更改所使用的给定依赖项的实现类型。它还使类易于测试,因为您可以提供依赖项的模拟实现。最后,它使类变得简单,并且更加关注它们的核心职责。
调用
无论你想说什么:
1 | MyClass myClass = applicationContext.getBean("myClass"); |
例如,您应该声明一个方法:
1 2 3 | public void setMyClass(MyClass myClass) { this.myClass = myClass; } |
然后在您的配置中:
1 2 3 4 5 | <bean id="myClass" class="MyClass">...</bean> <bean id="myOtherClass" class="MyOtherClass"> <property name="myClass" ref="myClass"/> </bean> |
然后弹簧会自动将
以这种方式声明所有的东西,并且在它的根上都有类似的东西:
1 2 3 4 | <bean id="myApplication" class="MyApplication"> <property name="myCentralClass" ref="myCentralClass"/> <property name="myOtherCentralClass" ref="myOtherCentralClass"/> </bean> |
选择服务定位器而不是控制反转(IOC)的原因有:
对于其他人来说,服务定位器在您的代码中更容易跟踪。IOC是"魔法",但维护程序员必须了解您复杂的弹簧配置和所有无数的位置,以了解如何连接对象。
IOC对于调试配置问题很糟糕。在某些应用程序类中,应用程序在配置错误时不会启动,您可能无法单步执行调试器的操作。
IOC主要是基于XML的(注释改进了一些东西,但是仍然有很多XML存在)。这意味着开发人员不能在您的程序上工作,除非他们知道Spring定义的所有魔术标签。再也不知道Java了。这阻碍了经验较少的程序员(即,当一个更简单的解决方案(如服务定位器)能够满足相同的需求时,使用更复杂的解决方案实际上是糟糕的设计)。另外,支持XML问题的支持远比对Java问题的支持弱很多。
依赖注入更适合于大型程序。大多数时候,额外的复杂性并不值得。
通常使用Spring,以防"以后可能要更改实现"。在没有春季国际奥委会复杂性的情况下,还有其他方法可以实现这一目标。
对于Web应用程序(JavaEE战争),Spring上下文在编译时有效绑定(除非您希望操作员在爆炸战争中搜索上下文)。您可以使Spring使用属性文件,但使用servlet时,属性文件需要位于预先确定的位置,这意味着您不能在同一个框中同时部署多个servlet。您可以在servlet启动时使用spring和jndi来更改属性,但是如果您使用jndi作为管理员可修改的参数,那么对spring本身的需求就会减少(因为jndi实际上是一个服务定位器)。
使用Spring,如果Spring向您的方法发送数据,您可能会失去程序控制。这很方便,适用于许多类型的应用程序,但并非所有应用程序。当您需要在初始化期间创建任务(线程等)或需要Spring不知道内容何时绑定到您的战争中的可修改资源时,您可能需要控制程序流。
Spring非常适合事务管理,并且具有一些优势。这仅仅是因为IOC在许多情况下都会过度工程化,并且会给维护人员带来不必要的复杂性。在不考虑不首先使用IOC的方法的情况下,不要自动使用它。
在application-context.xml中包含类可以避免使用getbean。然而,即使是这样,实际上也是不必要的。如果您正在编写独立的应用程序,并且不希望在application-context.xml中包含驱动程序类,则可以使用以下代码让Spring自动连接驱动程序的依赖项:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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的需要,我一直认为这很难看。
使用Spring这样的工具最酷的好处之一是,您不必将对象连接在一起。宙斯的头被打开了,你的类出现了,完全形成了它们所有的依赖关系,并根据需要进行了连接。这是不可思议的。
你说的越多,你得到的魔法就越少。代码越少越好。如果你的班级真的需要一个一流的豆子,你为什么不把它连起来呢?
也就是说,显然需要创建第一个对象。通过getbean()获取一两个bean的主要方法没有什么问题,但是您应该避免使用它,因为无论何时使用它,您都不会真正使用Spring的所有魔力。
其动机是编写不明确依赖于Spring的代码。这样,如果您选择切换容器,就不必重写任何代码。
把容器想象成代码看不见的东西,神奇地提供它的需求,而不被要求。
依赖注入是与"服务定位器"模式相对应的。如果您打算按名称查找依赖项,那么您也可以去掉DI容器并使用类似于JNDI的东西。
使用
1 | ApplicationContext context = new ClassPathXmlApplicationContext("AppContext.xml"); |
应用程序中只能使用一次。
其中一个原因是可测试性。假设你有这门课:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
如何测试这个bean?像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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时仍然可以这样做。然而,您需要模拟
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | @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); } } |
这是很有可能的,但我认为大多数人都会同意第一个选项更优雅,使测试更简单。
唯一真正有问题的选择是:
1 2 3 4 5 6 7 | @Component class MyBean { @Autowired MyBean(StringOutput out) { out.print(new HttpLoader().load("http://stackoverflow.com")); } } |
测试这需要付出巨大的努力,否则您的bean将尝试在每个测试上连接到stackoverflow。一旦您出现网络故障(或者StackOverflow的管理员由于访问率过高而阻止您),您将有随机失败的测试。
因此,作为一个结论,我不会说直接使用
弹簧前提之一是避免耦合。定义和使用接口、DI、AOP并避免使用applicationContext.getBean():-)
其他人已经指出了这个普遍的问题(并且是正确的答案),但我将给出另外一条评论:这不是说你永远不应该这样做,而是说尽可能少地这样做。
通常这意味着它只完成一次:在引导过程中。然后只需访问"根"bean,通过它可以解决其他依赖关系。这可以是可重用的代码,比如基本servlet(如果开发Web应用程序的话)。
其思想是依赖依赖注入(控制反转,或IOC)。也就是说,您的组件配置了它们需要的组件。这些依赖项被注入(通过构造器或设置器)-然后你就不会得到。
这允许您使用不同的组件实现轻松地重新布线应用程序,或者通过提供模拟变量(例如模拟DAO,以便在测试期间不命中数据库)以直接的方式配置用于测试的对象。
还有一次使用getbean是有意义的。如果要重新配置已经存在的系统,那么在Spring上下文文件中不会显式调用依赖项。您可以通过对getbean进行调用来启动这个过程,这样就不必一次就把它连接起来。通过这种方式,您可以慢慢建立您的弹簧配置,随着时间的推移,将每个零件放置到位,并使位正确排列。对getbean的调用最终将被替换,但是随着您了解代码的结构,或者缺少代码结构,您可以开始连接越来越多的bean并使用越来越少的对getbean的调用的过程。
我只发现需要getbean()的两种情况:
其他人提到在main()中使用getbean()来获取独立程序的"main"bean。
我对getbean()的另一个用途是在交互用户配置确定特定情况下的bean组成的情况下。例如,引导系统的一部分使用getbean()通过一个带有scope='prototype'bean定义的数据库表进行循环,然后设置其他属性。大概,有一个UI可以调整数据库表,而不是尝试(重新)编写应用程序上下文XML。
但是,仍然有一些情况需要服务定位器模式。例如,我有一个控制器bean,这个控制器可能有一些默认的服务bean,可以通过配置注入依赖关系。虽然也可能有许多附加的或新的服务,但是这个控制器现在或以后可以调用这些服务,然后需要服务定位器来检索服务bean。