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(
            Classextends ConfigurableApplicationContext> applicationContextClass) {
        this.applicationContextClass = applicationContextClass;
        if (!isWebApplicationContext(applicationContextClass)) {
            this.webEnvironment = false;
        }
    }
}

方法调用栈如下所示:
spring boot单元测试之druid NullPointException_第1张图片

红色框内的代码如下所示,如果@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容器,是否也可以借鉴这个方法,达到我们的目的呢,答案是肯定的,在下一篇文章中将会给出具体的解决方法

你可能感兴趣的:(spring)