Why is Spring's ApplicationContext.getBean considered bad?

前端 未结 14 1812
一整个雨季
一整个雨季 2020-11-22 14:33

I asked a general Spring question: Auto-cast Spring Beans and had multiple people respond that calling Spring\'s ApplicationContext.getBean() should be avoided

相关标签:
14条回答
  • 2020-11-22 15:06

    Others have pointed to the general problem (and are valid answers), but I'll just offer one additional comment: it's not that you should NEVER do it, but rather that do it as little as possible.

    Usually this means that it is done exactly once: during bootstrapping. And then it's just to access the "root" bean, through which other dependencies can be resolved. This can be reusable code, like base servlet (if developing web apps).

    0 讨论(0)
  • 2020-11-22 15:08

    Using @Autowired or ApplicationContext.getBean() is really the same thing. In both ways you get the bean that is configured in your context and in both ways your code depends on spring. The only thing you should avoid is instantiating your ApplicationContext. Do this only once! In other words, a line like

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

    should only be used once in your application.

    0 讨论(0)
  • 2020-11-22 15:10

    I mentioned this in a comment on the other question, but the whole idea of Inversion of Control is to have none of your classes know or care how they get the objects they depend on. This makes it easy to change what type of implementation of a given dependency you use at any time. It also makes the classes easy to test, as you can provide mock implementations of dependencies. Finally, it makes the classes simpler and more focused on their core responsibility.

    Calling ApplicationContext.getBean() is not Inversion of Control! While it's still easy to change what implemenation is configured for the given bean name, the class now relies directly on Spring to provide that dependency and can't get it any other way. You can't just make your own mock implementation in a test class and pass that to it yourself. This basically defeats Spring's purpose as a dependency injection container.

    Everywhere you want to say:

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

    you should instead, for example, declare a method:

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

    And then in your configuration:

    <bean id="myClass" class="MyClass">...</bean>
    
    <bean id="myOtherClass" class="MyOtherClass">
       <property name="myClass" ref="myClass"/>
    </bean>
    

    Spring will then automatically inject myClass into myOtherClass.

    Declare everything in this way, and at the root of it all have something like:

    <bean id="myApplication" class="MyApplication">
       <property name="myCentralClass" ref="myCentralClass"/>
       <property name="myOtherCentralClass" ref="myOtherCentralClass"/>
    </bean>
    

    MyApplication is the most central class, and depends at least indirectly on every other service in your program. When bootstrapping, in your main method, you can call applicationContext.getBean("myApplication") but you should not need to call getBean() anywhere else!

    0 讨论(0)
  • 2020-11-22 15:14

    It's true that including the class in application-context.xml avoids the need to use getBean. However, even that is actually unnecessary. If you are writing a standalone application and you DON'T want to include your driver class in application-context.xml, you can use the following code to have Spring autowire the driver's dependencies:

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

    I've needed to do this a couple of times when I have some sort of standalone class that needs to use some aspect of my app (eg for testing) but I don't want to include it in application-context because it is not actually part of the app. Note also that this avoids the need to look up the bean using a String name, which I've always thought was ugly.

    0 讨论(0)
  • 2020-11-22 15:19

    One of the reasons is testability. Say you have this 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"));
        }
    }
    

    How can you test this bean? E.g. like this:

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

    Easy, right?

    While you still depend on Spring (due to the annotations) you can remove you dependency on spring without changing any code (only the annotation definitions) and the test developer does not need to know anything about how spring works (maybe he should anyway, but it allows to review and test the code separately from what spring does).

    It is still possible to do the same when using the ApplicationContext. However then you need to mock ApplicationContext which is a huge interface. You either need a dummy implementation or you can use a mocking framework such as 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);
        }
    }
    

    This is quite a possibility, but I think most people would agree that the first option is more elegant and makes the test simpler.

    The only option that is really a problem is this one:

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

    Testing this requires huge efforts or your bean is going to attempt to connect to stackoverflow on each test. And as soon as you have a network failure (or the admins at stackoverflow block you due to excessive access rate) you will have randomly failing tests.

    So as a conclusion I would not say that using the ApplicationContext directly is automatically wrong and should be avoided at all costs. However if there are better options (and there are in most cases), then use the better options.

    0 讨论(0)
  • 2020-11-22 15:21

    One of Spring premises is avoid coupling. Define and use Interfaces, DI, AOP and avoid using ApplicationContext.getBean() :-)

    0 讨论(0)
提交回复
热议问题