最近在使用swagger.json文件参数Rest API文档时,使用Spring boot的单元测试来执行,单元测试在跑的过程中一直出现了自定义过滤器(即拦截器)空指针的问题。和下文出现的问题一下,见文章:
最近在使用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)`注解时却没有完成Servlet、Filter、Listener的初始化。
要满足我们的好奇心,先从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容器,是否也可以借鉴这个方法,达到我们的目的呢,答案是肯定的,在下一篇文章中将会给出具体的解决方法。
转载于:https://www.smwenku.com/a/5b9edbff2b71773ebacea75c/zh-cn/