为啥 Spring 的 ApplicationContext.getBean 被认为不好?

Posted

技术标签:

【中文标题】为啥 Spring 的 ApplicationContext.getBean 被认为不好?【英文标题】:Why is Spring's ApplicationContext.getBean considered bad?为什么 Spring 的 ApplicationContext.getBean 被认为不好? 【发布时间】:2010-10-23 04:22:04 【问题描述】:

我问了一个一般的 Spring 问题:Auto-cast Spring Beans,并且有多个人回答说应该尽可能避免调用 Spring 的 ApplicationContext.getBean()。这是为什么?

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

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

修正

我接受下面的答案,但这是 discusses the merits of Dependency Injection vs. using a Service Locator 的 Martin Fowler 的另一种看法(这与调用已包装的 ApplicationContext.getBean() 基本相同)。

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

【问题讨论】:

【参考方案1】:

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

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

【讨论】:

在创建new MyOtherClass() 对象时,是否有任何与 just 注释相关的内容?我知道@Autowired,但我只在字段上使用过它,它在new MyOtherClass().. ApplicationContext.getBean() 不是 IoC 是不正确的。你的类是否必须由 Spring 实例化 all。那是不恰当的教条。如果 ApplicationContext 本身是注入的,那么要求它以这种方式实例化一个 bean 是非常好的 - 它创建的 bean 可以是基于最初注入的 ApplicationContext 的不同实现。例如,我有一个场景,我根据一个在编译时未知但与我的 spring.xml 文件中定义的实现之一匹配的 bean 名称动态创建新的 bean 实例。 同意 Alex,我也有同样的问题,工厂类只能通过用户交互知道在运行时使用哪个 bean 或实现,我认为这就是 ContextAware 接口的用武之地跨度> @elbek: applicationContext.getBean 不是依赖注入:它直接访问框架,将其用作服务定位器 @herman:我不了解 Spring,因为我已经很久没有使用它了,但是在 JSR-330/Guice/Dagger 中,您可以通过注入 @987654335 来做到这一点@ 而不是 Foo 并在每次需要新实例时调用 provider.get()。不引用容器本身,您可以轻松创建Provider 进行测试。【参考方案2】:

选择服务定位器而不是控制反转 (IoC) 的原因是:

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

    IoC 对于调试配置问题非常糟糕。在某些类别的应用程序中,如果配置错误,应用程序将无法启动,您可能没有机会逐步了解调试器正在发生的事情。

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

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

    通常使用 Spring 以防您“以后可能想更改实现”。还有其他方法可以在没有 Spring IoC 的复杂性的情况下实现这一点。

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

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

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

【讨论】:

另外 - 您的 ServiceLocator 始终可以使用 Spring 中的 IoC,将您的代码从依赖于 Spring 中抽象出来,其中散布着 Spring 注释和无法解读的魔法。我最近将一堆代码移植到不支持 Spring 的 GoogleAppEngine。我希望我首先将所有 IoC 隐藏在 ServiceFactory 后面! IoC 鼓励贫乏的领域模型,我鄙视它。实体 bean 需要一种方法来查找它们的服务,以便它们可以实现自己的行为。在这一层,您无法真正绕过需要服务定位器。 比萨。我一直使用带有注释的 Spring。虽然确实涉及到一定的学习曲线,但现在,我在维护、调试、清晰度、可读性方面没有任何问题......我猜你如何构建事物是诀窍。【参考方案3】:

确实,在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 的需要,我一直认为这很难看。

【讨论】:

我也可以通过@Autowired 注释成功地使用此方法。【参考方案4】:

使用像 Spring 这样的东西最酷的好处之一是您不必将对象连接在一起。 Zeus 的脑袋裂开了,你的类出现了,它们的所有依赖项都已根据需要创建和连接。太神奇了。

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

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

【讨论】:

但是 OP 没有说“ClassINeed”,他说的是“BeanNameINeed”——它允许 IoC 容器在以任何方式配置的任何类上创建实例。也许它更像是“服务定位器”模式而不是 IoC,但它仍然会导致松散耦合。【参考方案5】:

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

将容器视为您的代码不可见的东西,神奇地满足其需求,无需被询问。

依赖注入是“服务定位器”模式的对立面。如果要按名称查找依赖项,不妨去掉 DI 容器并使用 JNDI 之类的东西。

【讨论】:

【参考方案6】:

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

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

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

【讨论】:

不。有时 @Autowired 或 ApplicationContext.getBean() 可能会产生完全不同的 bean。我不确定它是怎么发生的,但我现在正在努力解决这个问题。【参考方案7】:

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

【讨论】:

【参考方案8】:

其中一个原因是可测试性。假设你有这个课程:

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://***.com"));
    

如何测试这个 bean?例如。像这样:

class MyBeanTest 
    public void creatingMyBean_writes***PageToOutput() 
        // setup
        String ***html = "dummy";
        StringBuilder result = new StringBuilder();

        // execution
        new MyBean(Collections.singletonMap("https://***.com", ***Html)::get, result::append);

        // evaluation
        assertEquals(result.toString(), ***Html);
    

简单吧?

虽然您仍然依赖 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://***.com"));
    

class MyBeanTest 
    public void creatingMyBean_writes***PageToOutput() 
        // setup
        String ***Html = "dummy";
        StringBuilder result = new StringBuilder();
        ApplicationContext context = Mockito.mock(ApplicationContext.class);
        Mockito.when(context.getBean(HttpLoader.class))
            .thenReturn(Collections.singletonMap("https://***.com", ***Html)::get);
        Mockito.when(context.getBean(StringOutput.class)).thenReturn(result::append);

        // execution
        new MyBean(context);

        // evaluation
        assertEquals(result.toString(), ***Html);
    

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

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

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

对此进行测试需要付出巨大的努力,否则您的 bean 将尝试在每个测试中连接到 ***。一旦您出现网络故障(或者由于访问速率过高而导致 *** 的管理员阻止您),您将随机出现测试失败。

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

【讨论】:

【参考方案9】:

这个想法是您依赖依赖注入(inversion of control,或 IoC)。也就是说,您的组件配置了它们需要的组件。这些依赖项是注入的(通过构造函数或设置器)——你自己不会得到。

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

这使您可以轻松地使用不同的组件实现重新连接您的应用程序,或者通过提供模拟变体以简单的方式配置对象以进行测试(例如,模拟 DAO,这样您就不会在测试期间访问数据库)

【讨论】:

【参考方案10】:

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

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

【讨论】:

【参考方案11】:

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

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

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

【讨论】:

【参考方案12】:

还有一次使用 getBean 是有意义的。如果您正在重新配置一个已经存在的系统,其中依赖项没有在 spring 上下文文件中显式调用。您可以通过调用 getBean 来启动该过程,这样您就不必一次将其全部连接起来。通过这种方式,您可以慢慢建立您的弹簧配置,随着时间的推移将每个部件放置到位,并让位正确排列。对 getBean 的调用最终会被替换,但是当您了解代码结构或缺少代码结构后,您可以开始连接越来越多的 bean 并使用越来越少的 getBean 调用。

【讨论】:

【参考方案13】:

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

【讨论】:

【参考方案14】:

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

【讨论】:

以上是关于为啥 Spring 的 ApplicationContext.getBean 被认为不好?的主要内容,如果未能解决你的问题,请参考以下文章

Spring事件发布

Spring基本功能详解

Spring基本功能-IOC

Spring的JDBC Template

[Spring Boot] Singleton and Prototype

Spring与JDBC的整合使用