spring boot單元測試之druid NullPointException
最近在使用spring boot
對 Controller
進行單元測試時,發現 druid
竟然丟擲了空指標異常。原因是,使用了druid
的監控,需要經過druid
的 Filter
攔截器,但是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_PORT
、WebEnvironment.DEFINED_PORT
,可以自動為我們初始化Filter
、Servlet
。
於是我們裝飾上面的單元測試程式碼改成這樣,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
容器,為我們完成Servlet
、Filter、
Listener的初始化,但是當我們使用預設的
@SpringBootTest(webEnvironment=WebEnvironment.MOCK)`註解時卻沒有。
要滿足我們的好奇心,先從spring boot原始碼說起,這裡我們只關注與單元測試相關的內容。預設情況下,當我們的classpath
路徑下同時存在javax.servlet.Servlet
、org.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;
}
}
}
方法呼叫棧如下所示:
紅色框內的程式碼如下所示,如果@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容器,是否也可以借鑑這個方法,達到我們的目的呢,答案是肯定的,在下一篇文章中將會給出具體的解決方法