一直有好奇Spring是怎样做到零XML完成Spring在Web容器中启动的,这篇文章主要介绍WebApplicationInitializer接口。作为本篇的主角, WebApplicationInitializer接口位于org.springframework.web包下。很简单,只有一个方法:
public interface WebApplicationInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
它的子类如下:
AbstractContextLoaderInitializer类中完成了注册ContextLoadLIstener到ServletContext的过程,从而完成了Spring的初始化。
public void onStartup(ServletContext servletContext) throws ServletException {
registerContextLoaderListener(servletContext);
}
protected void registerContextLoaderListener(ServletContext servletContext) {
WebApplicationContext rootAppContext = createRootApplicationContext();
if (rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext));
}
else {
logger.debug("No ContextLoaderListener registered, as " +
"createRootApplicationContext() did not return an applicationcontext");
}
}
AbstractDispatcherServletInitializer类中完成了注册DispatcherServlet到ServletContext和Web容器拦截器到容器中的过程,从而完成了SpringMVC的初始化。
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
registerDispatcherServlet(servletContext);
}
protected void registerDispatcherServlet(ServletContext servletContext) {
String servletName = getServletName();
Assert.hasLength(servletName, "getServletName() may not return empty or null");
WebApplicationContext servletAppContext = createServletApplicationContext();
Assert.notNull(servletAppContext,
"createServletApplicationContext() did not return an application " +
"context for servlet [" + servletName + "]");
DispatcherServlet dispatcherServlet = new DispatcherServlet(servletAppContext);
ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
Assert.notNull(registration,
"Failed to register servlet with name '" + servletName + "'." +
"Check if there is another servlet registered under the same name.");
registration.setLoadOnStartup(1);
registration.addMapping(getServletMappings());
registration.setAsyncSupported(isAsyncSupported());
Filter[] filters = getServletFilters();
if (!ObjectUtils.isEmpty(filters)) {
for (Filter filter : filters) {
registerServletFilter(servletContext, filter);
}
}
customizeRegistration(registration);
}
所以,WebApplicationInitializer接口可以说是web.xml的替代品。那么,这个类是怎么发挥作用的呢?又是调用了它呢?我们可以发现是位于同位置的SpringServletContainerInitializer类。调用过程如下:
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<WebApplicationInitializer>();
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// Be defensive: Some servlet containers provide us with invalid classes,
// no matter what @HandlesTypes says...
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
initializers.add((WebApplicationInitializer) waiClass.newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
if (initializers.isEmpty()) {
servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
return;
}
AnnotationAwareOrderComparator.sort(initializers);
servletContext.log("Spring WebApplicationInitializers detected on classpath: " + initializers);
for (WebApplicationInitializer initializer : initializers) {
initializer.onStartup(servletContext);
}
}
突然上这样一段代码可以有点突兀,没关系,我们慢慢捋。首先,该方法有两个参数:Set<Class<?>> webAppInitializerClasses,和ServletContext servletContext。前者我们基本可以断定它是指WebAppInitializerClass接口子类的集合,后者是我们很熟悉的ServletContext。我们可以看到在代码中做了这样一件事:遍历所有的WebAppInitializerClass接口子类,找到不是接口不是抽象类的类后实例化一个对象并保存到一个列表中,最后逐个调用onStartup()方法。
那么,又是谁调用了SpringServletContainerInitializer类的onStartup()方法呢?我们可以发现,SpringServletContainerInitializer类的onStartup()方法源自ServletContainerInitializer接口的onStartup()方法。所以问题变成了谁调用了ServletContainerInitializer接机口的onStartup()方法呢?
我们引用一段官方的描述:为了支持可以不使用web.xml的情况,提供ServletContainerInitializer来完成这件事。它可以通过SPI(Service Provider Interface)机制,当启动web容器的时候,会自动到添加的相应jar包下找到META-INF/services下以ServletContainerInitializer的全路径名称命名的文件,它的内容为ServletContainerInitializer实现类的全路径,将它们实例化。那么SpringServletContainerInitializer作为ServletContainerInitializer的实现类,在Web容器启动时将被自动初始化并调用其onStartup()方法。
到这里,不知道看到这篇文章的你有没有这样的疑惑,既然在Web容器启动时ServletContainerInitializer实现类将被自动初始化并调用其onStartup()方法,那么作为参数的WebAppInitializerClass接口子类是怎么被作为参数传入的呢?换句话说,SpringServletContainerInitializer怎么知道它需要的是WebAppInitializerClass接口的子类集合呢?换个接口行不行?当然可以喽!你完全可以自定义一个SpringServletContainerInitializer类并实现ServletContainerInitializer接口,然后声明类注解@HandlesTypes()为你想要注入的接口即可。
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
}
}