Fork me on GitHub
13 July 2018

Servlet 3 规范规定了只要Web项目中实现了ServletContainerInitializer接口,即可不通过web.xml注册Servlet和Filter。当使用代码的方式配置时,Web容器会寻找应用中实现了ServletContainerInitializer接口的类,并调用onStartup方法初始化应用。

如下所述:

The following methods are added to ServletContext since Servlet 3.0 to enable programmatic definition of servlets, filters and the url pattern that they map to. These methods can only be called during the initialization of the application either from the contexInitialized method of a ServletContextListenerimplementation or from the onStartup method of a ServletContainerInitializer implementation. In addition to adding Servlets and Filters, one can also look up an instance of a Registration object corresponding to a Servlet or Filter or a map of all the Registration objects for the Servlets or Filters. If the ServletContext passed to the ServletContextListener ’s contextInitialized method was neither declared in web.xml or web-fragment.xml nor annotated with @WebListener then an UnsupportedOperationException MUST be thrown for all the methods defined for programmatic configuration of servlets, filters and listeners.

— Java ™ Servlet Specification, Servlet 3.0

目录:

1. 代码编写

下面我们来看看如何在项目中使用这一新特性。

Spring MVC的SpringServletContainerInitializer实现了ServletContainerInitializer接口,因此可通过Java代码来配置Servlet和Filter等类。

在Spring MVC项目中,继承AbstractAnnotationConfigDispatcherServletInitializer即可。

/**
 * @author CofCool
 * @date 2018/7/13
 */
public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    /**
     * 上下文配置,
     * 也就是传web.xml中的ContextLoaderListener配置
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] { RootConfigure.class };
    }

    /**
     * MVC配置
     * 也就是web.xml中的的DispatcherServlet配置
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[] { WebConfigure.class };
    }

    /**
     * DispatcherServlet映射路径配置
     */
    @Override
    protected String[] getServletMappings() {
        return new String[] { "/*" };
    }

    /**
     * 注册Filter
     */
    @Override
    protected Filter[] getServletFilters() {
        return new Filter[] { new TestFilter() };
    }
}

RootConfigure: 配置包扫描路径等。

/**
 * @author CofCool
 * @date 2018/7/13
 */
@Configuration
@ComponentScan(basePackages = {"net.cofcool.test.spring"})
public class RootConfigure {

}

WebConfigure: MVC相关配置,Controller扫描,视图解析等。

/**
 * @author CofCool
 * @date 2018/7/13
 */
@Configuration
@EnableWebMvc
@ComponentScan("net.cofcool.test.spring.app")
public class WebConfigure extends WebMvcConfigurerAdapter{


    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/view/");
        resolver.setSuffix(".jsp");

        return resolver;
    }

    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
}

通过以上简单代码即可实现Spring MVC应用的基础配置,是不是比web.xml的方式更为简单容易!

完整示例可查看SpringTest

2. 实现分析

2.1 Tomcat

以下代码以Tomcat 9源码为例。

ContextConfig类负责扫描和添加ServletContainerInitializer。

/**
 * 扫描应用中ServletContainerInitializer的实现类
 */
protected void processServletContainerInitializers() {

    List<ServletContainerInitializer> detectedScis;
    try {
        WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
        detectedScis = loader.load(ServletContainerInitializer.class);
    } catch (IOException e) {
        log.error(sm.getString(
                "contextConfig.servletContainerInitializerFail",
                context.getName()),
            e);
        ok = false;
        return;
    }

    // 缓存扫描到的实现类
    for (ServletContainerInitializer sci : detectedScis) {
        initializerClassMap.put(sci, new HashSet<Class<?>>());

        HandlesTypes ht;
        try {
            ht = sci.getClass().getAnnotation(HandlesTypes.class);
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.info(sm.getString("contextConfig.sci.debug",
                        sci.getClass().getName()),
                        e);
            } else {
                log.info(sm.getString("contextConfig.sci.info",
                        sci.getClass().getName()));
            }
            continue;
        }
        if (ht == null) {
            continue;
        }
        Class<?>[] types = ht.value();
        if (types == null) {
            continue;
        }

        for (Class<?> type : types) {
            if (type.isAnnotation()) {
                handlesTypesAnnotations = true;
            } else {
                handlesTypesNonAnnotations = true;
            }
            Set<ServletContainerInitializer> scis =
                    typeInitializerMap.get(type);
            if (scis == null) {
                scis = new HashSet<>();
                typeInitializerMap.put(type, scis);
            }
            scis.add(sci);
        }
    }
}

// 项目启动配置时调用,一般在START_EVENT之前,BEFORE_START_EVENT之后
// Lifecycle.CONFIGURE_START_EVENT
protected void webConfig() {
    ...
    // Step 11. 添加ServletContainerInitializer到上下文中
        if (ok) {
            for (Map.Entry<ServletContainerInitializer,
                    Set<Class<?>>> entry :
                        initializerClassMap.entrySet()) {
                if (entry.getValue().isEmpty()) {
                    context.addServletContainerInitializer(
                            entry.getKey(), null);
                } else {
                    context.addServletContainerInitializer(
                            entry.getKey(), entry.getValue());
                }
            }
        }
}

Context的实现为StandardContext

// 调用onStartup方法
protected synchronized void startInternal() throws LifecycleException {
    ...
    for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :
        initializers.entrySet()) {
        try {
            entry.getKey().onStartup(entry.getValue(),
                    getServletContext());
        } catch (ServletException e) {
            log.error(sm.getString("standardContext.sciFail"), e);
            ok = false;
            break;
        }
    }
    ...
}

2.2 Spring MVC

SpringServletContainerInitializer实现了ServletContainerInitializer接口,并通过@HandlesTypes声明可以处理的类为WebApplicationInitializer

AbstractContextLoaderInitializer实现了WebApplicationInitializer,并定义了createRootApplicationContext,负责创建ContextLoaderListener。它的子类AbstractDispatcherServletInitializer定义了createServletApplicationContext,负责创建DispatcherServlet。

AbstractAnnotationConfigDispatcherServletInitializer继承自AbstractDispatcherServletInitializer,实现了createRootApplicationContextcreateServletApplicationContext,并定义了getRootConfigClasses和getServletConfigClasses,也即是我们上文中项目配置类中重写的两个方法。

类图WebApplicationInitializeronStartup(ServletContext servletContext)AbstractContextLoaderInitializerAbstractDispatcherServletInitializerAbstractAnnotationConfigDispatcherServletInitializer