最近在使用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 {
//......
}
为什么这个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 WEB_CONTEXT_CLASS = GenericWebApplicationContext.class;
void configure(MergedContextConfiguration configuration,
SpringApplication application,
List> initializers) {
WebMergedContextConfiguration webConfiguration = (WebMergedContextConfiguration) configuration;
addMockServletContext(initializers, webConfiguration);
application.setApplicationContextClass(WEB_CONTEXT_CLASS);
}
}
如果在我们的项目中,不需要启动servlet容器,是否也可以借鉴这个方法,达到我们的目的呢,答案是肯定的,在下一篇文章中将会给出具体的解决方法