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

Vin*_*nie 262 java spring

我问了一个普通的Spring问题:自动转换Spring Beans并让多个人回应,ApplicationContext.getBean()应该尽可能避免调用Spring .这是为什么?

我还应该如何获得我配置Spring创建的bean的权限?

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

修订

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

部分,福勒状态," 随着服务定位器应用程序类由一个消息给该定位器询问它[服务]明确地随着喷射没有明确请求时,服务出现在应用程序的类-控制的,因此反转.控制反转是框架的共同特征,但它的东西,是有代价的,它往往是很难理解,当你试图调试导致的问题.所以,整体来说,我宁可不去,[控制反转除非我需要它.这并不是说它是一件坏事,只是因为我认为它需要通过更直接的选择来证明自己的合理性. "

Col*_*inD 199

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

呼叫ApplicationContext.getBean()不是控制倒置!虽然更改为给定bean名称配置的实现仍然很容易,但是现在该类直接依赖于Spring来提供该依赖性,并且无法以任何其他方式获取它.您不能只在测试类中创建自己的模拟实现,并自己将其传递给它.这基本上违背了Spring作为依赖注入容器的目的.

到处都想说:

MyClass myClass = applicationContext.getBean("myClass");
Run Code Online (Sandbox Code Playgroud)

例如,您应该声明一个方法:

public void setMyClass(MyClass myClass) {
   this.myClass = myClass;
}
Run Code Online (Sandbox Code Playgroud)

然后在你的配置中:

<bean id="myClass" class="MyClass">...</bean>

<bean id="myOtherClass" class="MyOtherClass">
   <property name="myClass" ref="myClass"/>
</bean>
Run Code Online (Sandbox Code Playgroud)

Spring会自动注入myClassmyOtherClass.

以这种方式声明一切,并在它的根部都有类似的东西:

<bean id="myApplication" class="MyApplication">
   <property name="myCentralClass" ref="myCentralClass"/>
   <property name="myOtherCentralClass" ref="myOtherCentralClass"/>
</bean>
Run Code Online (Sandbox Code Playgroud)

MyApplication是最核心的类,至少间接地依赖于程序中的每个其他服务.引导时,在你的main方法中,你可以打电话,applicationContext.getBean("myApplication")但你不应该getBean()在其他任何地方打电话!

  • ApplicationContext.getBean()不是IoC,这不是事实.Niether必须让你的所有类都由Spring实例化.那是不恰当的教条.如果ApplicationContext本身被注入,那么要求它以这种方式实例化bean是完全正确的 - 它创建的bean可以是基于最初注入的ApplicationContext的不同实现.例如,我有一个场景,我根据编译时未知的bean名称动态创建新的bean实例,但匹配我的spring.xml文件中定义的实现之一. (66认同)
  • @herman:我不知道Spring,因为我很久没有使用它了,但是在JSR-330/Guice/Dagger中,你可以通过注入一个`Provider <Foo>`而不是`来做到这一点. Foo`并在每次需要新实例时调用`provider.get()`.没有对容器本身的引用,您可以轻松地创建一个`Provider`进行测试. (6认同)
  • 在创建一个`new MyOtherClass()`对象时,是否有与此相关的_just_注释?我知道@Autowired,但我只是在字段上使用它,它打破了`new MyOtherClass()`.. (3认同)
  • 同意Alex,我有一个相同的问题,即工厂类只能通过用户交互在运行时知道要使用哪个bean或实现,我认为这是ContextAware接口的所在 (3认同)
  • @elbek:`applicationContext.getBean`不是依赖注入:它直接访问框架,使用它作为_service locator_. (3认同)

小智 64

更喜欢服务定位器而不是控制反转(IoC)的原因是:

  1. 服务定位器让其他人更容易在您的代码中跟踪.IoC是"神奇的",但维护程序员必须了解您的复杂Spring配置和所有无数的位置,以弄清楚如何连接对象.

  2. IoC很难调试配置问题.在某些类别的应用程序中,如果配置错误,应用程序将无法启动,您可能无法逐步完成调试器的操作.

  3. IoC主要基于XML(Annotations改进了一些东西,但仍有很多XML).这意味着开发人员无法处理您的程序,除非他们知道Spring定义的所有魔术标记.再也不懂Java了.这阻碍了较少经验的程序员(即,当更简单的解决方案,例如服务定位器,将满足相同的要求时,使用更复杂的解决方案实际上是糟糕的设计).此外,对诊断XML问题的支持远远弱于对Java问题的支持.

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

  5. 通常使用Spring以防"以后可能想要更改实现".如果没有Spring IoC的复杂性,还有其他方法可以实现这一目标.

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

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

Spring非常适合事务管理并具有一些优势.只是IoC在许多情况下可能过度工程化并为维护者带来无根据的复杂性.不考虑先不使用IoC的方法,不要自动使用IoC.

  • 另外 - 您的ServiceLocator可以始终使用Spring中的IoC,将您的代码抽象为依赖于Spring,充斥着Spring注释和无法解析的magik.我最近将一堆代码移植到了GoogleAppEngine,不支持Spring.我希望我首先将所有IoC隐藏在ServiceFactory后面! (7认同)
  • Bizar.我一直使用Spring注释.虽然确实存在一定的学习曲线,但现在,我在维护,调试,清晰度和可读性方面没有任何问题....我猜你如何构建事物就是诀窍. (4认同)

ift*_*itz 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;    
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Bra*_*ugh 21

使用像Spring这样的东西最酷的好处之一就是你不必将对象连接在一起.Zeus的头部分开,你的类出现了,完全形成了所有的依赖关系,根据需要创建和接线.这是神奇而奇妙的.

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

也就是说,显然需要创建第一个对象.你的main方法通过getBean()获取一两个bean没有任何问题,但你应该避免它,因为每当你使用它时,你并没有真正使用Spring的所有魔法.


eri*_*son 16

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

把容器想象成你的代码看不见的东西,神奇地满足它的需要,而不被问到.

依赖注入是"服务定位器"模式的对应点.如果您要按名称查找依赖项,您可能还要删除DI容器并使用类似JNDI的东西.


小智 10

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

ApplicationContext context = new ClassPathXmlApplicationContext("AppContext.xml");
Run Code Online (Sandbox Code Playgroud)

应该只在您的应用程序中使用一次.


sou*_*els 5

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


yan*_*kee 5

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

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"));
    }
}
Run Code Online (Sandbox Code Playgroud)

你怎么能测试这个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);
    }
}
Run Code Online (Sandbox Code Playgroud)

很简单,对吧?

虽然您仍然依赖 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

这是很有可能的,但我认为大多数人都会同意第一个选项更优雅并使测试更简单。

唯一真正有问题的选择是这个:

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

测试这需要付出巨大的努力,否则您的 bean 将在每次测试时尝试连接到 stackoverflow。一旦您遇到网络故障(或者 stackoverflow 的管理员由于访问率过高而阻止您),您的测试将随机失败。

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