我从网上找了一张web项目的目录结构,如下:
是不是很熟悉的赶脚^-^
几年之前这是当之无愧的主流,但是随着技术的发展,servlet3.0、springboot的诞生,基于xml配置的web项目一去不复返了,取而代之的是springboot。今天不谈springboot是如何玩的,今天的主题是web.xml是如何消失的。因为当下主流的技术就是spring全家桶,探究的目的也是spring是如何让web.xml消失的。springmvc中的核心是DispacherServlet,在使用的时候会在web.xml中配置<servlet>标签,那么当web.xml消失了,DispacherServlet如何注册到容器中的呢?解决了这个问题也就能回答web.xml消失的原因了。
Servlet3.0
从servlet3.0开始,支持ServletContainerInitializer接口,也就是说提供了一种通过代码层面来初始化servlet容器的方式,比如注册servlet、listener、filter。官方文档中关于ServletContainerInitializer的介绍如下:
ServletContainerInitializer 类通过 jar services API 查找。对于每一个应用,应用启动时,由容器创建一个 ServletContainerInitializer 实例。框架提供的 ServletContainerInitializer 实现必须绑定在 jar 包的 META-INF/services 目录中的一个叫做 javax.servlet.ServletContainerInitializer 的文件,根据 jar services API, 指定 ServletContainerInitializer 的实现。 除 ServletContainerInitializer 外,我们还有一个注解—HandlesTypes。在 ServletContainerInitializer 实现上的 HandlesTypes 注解用于表示感兴趣的一些类,它们可能指定了 HandlesTypes 的 value 中的注解(类型、方 法或自动级别的注解),或者是其类型的超类继承/实现了这些类之一。
简单来说,只要在jar中的特定目录文件下配置ServletContainerInitializer的实现类,servlet容器在启动的过程中就会去调用这个实现类,从而可以达到注册servlet、listener、filter的功能,那么web.xml的用处完全可以被取代了。
spring-web包
对于使用spring很熟悉的朋友肯定知道,在开发web应用时spring-web包是必不可少的,来看看这个包的目录结构
是不是很惊喜?针对servlet3.0,spring对其ServletContainerInitializer的特性做了支持,看到这里,心里有底了,spring是支持通过这种方式来注册servlet的。下面我们来看看SpringServletContainerInitializer是怎么实现的。上源码
//1
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
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 {
//2
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(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;
}
servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
//3
initializer.onStartup(servletContext);
}
}
}
//1 通过@HandlesTypes注解获取感兴趣的类(WebApplicationInitializer及其子类),作为集合webAppInitializerClasses
传入到onStartup方法中
//2 筛选出webAppInitializerClasses集合中的实现类
//3 遍历WebApplicationInitializer的实现类,调用onStartup方法
WebApplicationInitializer的实现类
如图所示,红框中的抽象类具体帮助实现了DispatcherServlet的注册。除此之外还看到了一个熟悉的类,有没有?对,就是SpringBootServletInitializer,熟悉吧,没有有一种万物归宗的赶脚。Springboot如果需要使用外部的web容器启动,需要在之前的基础上改造一下Application,其中之一便是继承SpringBootServletInitializer,这里不展开讲,有兴趣的可以自己去研究下,后续我也会springboot相关的文章,想知道细节的朋友可以关注我^-^
好了,来看看这三个抽象类
我们关注的是onStartup方法,AbstractDispatcherServletInitializer中诠释了onStartup的实现
首先调用父类AbstractContextLoaderInitializer的onStartup方法,然后注册DispatcherServlet
AbstractContextLoaderInitializer中的onStartup方法
第一步,创建根容器(父容器),在AbstractDispatcherServletInitializer中实现的
创建AnnotationConfigWebApplicationContext,注册@Configuration注释的类,一般包括service、dao(repository)、bean
2、创建ContextLoaderListener,并注册到servlet容器中。当servlet容器启动后,refresh根容器。
注册DispatcherServlet
第一步,createServletApplicationContext(),也是在AbstractAnnotationConfigDispatcherServletInitializer中实现的,它实际上是一个AnnotationConfigWebApplicationContext,作为子容器和DispatcherServlet绑定在一起
初始化一个AnnotationConfigWebApplicationContext实例,注册@Configuration注释的类,一般是controller,子容器中还会存放springmvc相关的bean,比如ViewRover、Interceptor。
第二步,创建DispatcherServlet,将子容器作为属性绑定到DispatcherServlet中
第三步,将DispatcherServlet添加到Servlet容器中,并且设置servlet-mapping路径,loadOnstartup顺序,所以当servlet容器启动后便会调用DispatcherServlet的init方法,当中主要调用了initServletBean方法,具体实现在FrameworkServlet,上代码
看initWebApplicationContext方法
先获取根容器,然后把根容器设置为字容器的父亲(这里也印证了父容器、字容器说法的来源),最后refresh字容器(熟悉spring ioc的朋友应该很清楚refresh对于spirng容器的重要性)
到这里,WebApplicationInitializer之下的三个继承类已经介绍完毕,是不是恍然大悟,原来spring是这么玩的。其实还没有完。还记得最开始的时候我说SpringServletContainerInitializer在执行onStartup方法时会传入感兴趣的WebApplicationInitailizer类及其子类,然后剔除掉接口和抽象类,刚刚我们介绍的全是抽象类,是不是有种想打我的冲动?稍安勿躁。我以前也很困惑,为啥没有提供实现类呢?由于现在做的是springboot项目,它把这个问题隐藏了,实际上sprigboot压根就不走这条路来注册DispatcherServlet到servlet容器中。这里不提springboot是怎么玩的,如果没有用springboot,又想走这条路来注册servlet该怎么办呢?很简单,没有实现类,创建一个不就好了,是不是很简单,我困惑了好久(555)。上代码
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class AnnotationConfigDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{RootConfig.class};
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{ServletConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/*"};
}
}
@Configuration
@ComponentScan(excludeFilters = {@Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
class RootConfig {
}
@Configuration
@ComponentScan(useDefaultFilters = false, includeFilters = {@Filter(type = FilterType.ANNOTATION, classes = {Controller.class})})
class ServletConfig
主要的逻辑抽象类已经帮忙实现了,我只需要实现三个方法即可,getRootConfigClasses帮助根容器的创建,getServletConfigClasses帮助子容器的创建,getServletMappings帮助配置DispatcherServlet的映射路径。