Why is Spring's ApplicationContext.getBean considered bad?

前端 未结 14 2080
一整个雨季
一整个雨季 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: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.

提交回复
热议问题