1. 程式人生 > >spring boot單元測試之druid NullPointException

spring boot單元測試之druid NullPointException

最近在使用spring bootController 進行單元測試時,發現 druid 竟然丟擲了空指標異常。原因是,使用了druid的監控,需要經過druidFilter 攔截器,但是spring boot test未呼叫 Filter#init()Filter 進行初始化。

異常程式碼

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc
public class MetaRestControllerTest {

    @Autowired
    MockMvc mockMvc;

    @Test
public void testGetInfo() throws Exception { String json = "{}"; MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders. post("/meta/info").contentType( MediaType.APPLICATION_JSON_UTF8 ) .accept( MediaType.APPLICATION_JSON_UTF8 ); requestBuilder.content( json ); // 發起請求
MvcResult result = mockMvc.perform( requestBuilder ) .andDo( MockMvcResultHandlers.print() ) .andReturn(); String response = result.getResponse().getContentAsString(); logger.info( "====Response====\n{}", response ); } }

Controller單元測試程式碼如上所示,在專案中由於要使用druid

的監控功能,因此需要加入WebStatFilter這個Filter,我們參考官方給出的單元測試程式碼,結果發現WebStatFilter丟擲了空指標異常,異常堆疊如下所示:

java.lang.NullPointerException
    at com.alibaba.druid.support.http.WebStatFilter.doFilter(WebStatFilter.java:94)
    at org.springframework.test.web.servlet.setup.PatternMappingFilterProxy.doFilter(PatternMappingFilterProxy.java:101)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:127)
    at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:197)
    at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
    at org.springframework.mock.web.MockFilterChain.doFilter(MockFilterChain.java:127)
    at org.springframework.test.web.servlet.MockMvc.perform(MockMvc.java:155)
    at net.dwade.driver.test.controller.MetaRestControllerTest.testGetInfo(MetaRestControllerTest.java:38)

解決方案

我們檢視WebStatFilter原始碼發現,有個變數竟然是null,而該變數是在Filter#init()進行賦值的,說明spring boot單元測試沒有對Filter進行初始化,但是Filter在請求過程中被執行了,因此丟擲了空指標異常。難道,官方給出的程式碼有問題?文件中對@SpringBootTest註解,有詳細的說明,我們可以指定webEnvironment屬性,預設是WebEnvironment.MOCK,它是不會對Filter、Servlet進行初始化的,因此我們在使用單元測試的時候要注意了。好在,spring為我們提供了WebEnvironment.RANDOM_PORTWebEnvironment.DEFINED_PORT,可以自動為我們初始化FilterServlet

於是我們裝飾上面的單元測試程式碼改成這樣,debug發現我們註冊的WebStatFilter被初始化了

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MetaRestControllerTest {
    //......
}

Why?

為什麼這個webEnvironment=WebEnvironment.MOCK引數可以控制Filter的初始化過程?接下來,我們分析下spring boot test的部分原始碼

其實,spring boot單元測試也是需要藉助 SpringApplication,為我們啟動spring容器,預設情況下是需要建立Servlet容器,為我們完成ServletFilter、Listener的初始化,但是當我們使用預設的@SpringBootTest(webEnvironment=WebEnvironment.MOCK)`註解時卻沒有。

要滿足我們的好奇心,先從spring boot原始碼說起,這裡我們只關注與單元測試相關的內容。預設情況下,當我們的classpath路徑下同時存在javax.servlet.Servletorg.springframework.web.context.ConfigurableWebApplicationContext時,便會為我們建立AnnotationConfigEmbeddedWebApplicationContext容器(ApplicationContext的實現類),而常見的Servlet容器像tomcat、jetty、Undertow都是靠它為我們啟動的。我們在以下程式碼打上斷點

public class SpringApplication {
    public void setWebEnvironment(boolean webEnvironment) {
        this.webEnvironment = webEnvironment;
    }
    public void setApplicationContextClass(
            Class<? extends ConfigurableApplicationContext> applicationContextClass) {
        this.applicationContextClass = applicationContextClass;
        if (!isWebApplicationContext(applicationContextClass)) {
            this.webEnvironment = false;
        }
    }
}

方法呼叫棧如下所示: image

紅色框內的程式碼如下所示,如果@SpringBootTest註解中的webEnvironment embedded值為false時,會為SpringApplication指定容器類GenericWebApplicationContext,而它是不會為我們建立servlet容器,也不會初始化Filter、Servlet、Listener

public class SpringBootContextLoader extends AbstractContextLoader {
    @Override
    public ApplicationContext loadContext(MergedContextConfiguration config) throws Exception {
        SpringApplication application = getSpringApplication();
        //省略SpringApplication賦值操作......
        if (config instanceof WebMergedContextConfiguration) {
            application.setWebEnvironment(true);
            if (!isEmbeddedWebEnvironment(config)) {
                // 如果@SpringBootTest註解中的webEnvironment embedded為false時,會執行以下程式碼
                new WebConfigurer().configure(config, application, initializers);
            }
        }
        else {
            application.setWebEnvironment(false);
        }
        application.setInitializers(initializers);
        ConfigurableApplicationContext context = application.run();
        return context;
    }
}

WebConfigurer指定SpringApplication需要初始化的Spring容器GenericWebApplicationContext

private static class WebConfigurer {

    private static final Class<GenericWebApplicationContext> WEB_CONTEXT_CLASS = GenericWebApplicationContext.class;

    void configure(MergedContextConfiguration configuration,
            SpringApplication application,
            List<ApplicationContextInitializer<?>> initializers) {
        WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration;
        addMockServletContext(initializers, webConfiguration);
        application.setApplicationContextClass(WEB_CONTEXT_CLASS);
    }
}

如果在我們的專案中,不需要啟動servlet容器,是否也可以借鑑這個方法,達到我們的目的呢,答案是肯定的,在下一篇文章中將會給出具體的解決方法