Tomcat是咋启动SpringMVC的?
Posted 菜鸟封神记
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat是咋启动SpringMVC的?相关的知识,希望对你有一定的参考价值。
之前的文章介绍了Spring容器的启动过程,详情可参考。但是呢,这个容器的启动过程只是完整项目启动流程中的一部分,那么将Spring项目部署到Tomcat中之后,在Tomcat启动的时候是如何启动Spring容器的呢?它的入口在什么地方呢?
别着急,下面将对这一过程做个简单的分析。
先回顾一下我们常规方法开发和部署Spring项目的过程?大的过程分为如下的三步:
第一步: 项目搭建,配置项目中需要的配置文件,例如:spring-mvc.xml,spring.xml,spring-datasource.xml...一堆xml。或者使用没有配置文件的方式,全部用基于注解的JavaConfig的方式,具体示例可以参考:
第二步: 系统功能开发,测试
第三步: 测试完成之后,打包编译成war包,然后放到Tomcat的webapps目录下,启动Tomcat即可
我们又知道,SpringMVC底层封装的是原生的Servlet,根据Servlet规范来支持Web框架所具有的功能的。例如处理Http请求所需要用的HttpServlet和用来处理会话所需要用的HttpSession等。
通过查看Servlet3.0的规范,发现Servlet3.0中提供了一个名字为ServletContainerInitilizer的类,简称为SCI。那么这个类是个啥玩意儿呢?
1、简介
该类是Web容器中提供的一个扩展点,用来完成Web容器启动时的相关回调操作。该类是从Servlet3.0开始支持的。对于通过无web.xml,而是通过注解方式来启动的SpringMVC项目来说,这就是其项目启动的一个入口。定义如下:
public interface ServletContainerInitializer {
void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}
2、执行过程
那么这类中的onStartup是干嘛的?是如何触发执行的呢?
2.1、onStartup方法
通过查看其源码上面的注释,大概说的是:在web应用程序启动期间,来接收web应用程序中类(这个类需要与javax.servlet.annotation.HanlderTypes注解定义的条件匹配)的通知。
是不是有点懵逼?
用易懂的话来讲就是在web应用启动过程中的某一个时刻,这个onStartup方法会被调用,调用的时候会传入两个入参,第一个Set类型的入参是满足一定规则的一个Class的集合,第二个入参就是当前的Servlet上下文对象。这个扩展点其实就是SpringMVC和SpringBoot启动过程中初始化Spring容器的一个切入点。
那么这个Set集合的入参对应的Class规则是如何指定的呢?
我们已经知道了SpringMVC是通过这个类来启动Spring容器的,那么我们来从源码中找一下这个类在哪用到了!可以全局搜索之后发现这个类是被一个名称为SpringServletContainerInitializer的类实现的,这个类位于spring-web模块下,具体的代码如下:
@HandlesTypes(value = {WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
// ...
}
}
我们可以看到有一个@HandlersTypes的注解,这个注解是干嘛的呢?
这个注解是Tomcat中定义的一个注解,这个注解的作用是在Tomcat启动时,会将classpath下面的所有实现了注解中value属性所指定的类的子类全部放入到一个Set<Class<?>>集合中,然后回调onStartup方法的时候作为入参传入。
例如在如上的声明"@HandlesTypes(value = {WebApplicationInitializer.class})"中,就会在启动过程中,将WebApplicationInitializer的所有子类放入Set集合中,然后作为入参传入,value可以指定多个类型,具体的这个查找子类的过程是在Tomcat中实现的。
2.2、执行原理
在spring-web模块中,我们会发现,在其resources目录下有一个META-INF/services文件夹,文件夹中有一个名称为javax.servlet.ServletContainerInitializer的文件,如下:
这个文件的内容就是SpringServletContainerInitializer,所以很明显,这个类是通过SPI机制被加载的,那么在哪被加载的呢?
这就要翻开Tomcat的源码了,打开Tomcat源码中的org.apache.catalina.startup.ContextConfig这个类,查看这个类中的webConfig()这个方法,然后找到其中的processServletContainerInitializers方法,这个方法中就能看到,有一个名字为WebappServiceLoader的SPI加载器,通过这个加载器从META-INF/services下面加载了名称为ServletContainerInitializer的文件中的类,如下:
protected void processServletContainerInitializers() {
List<ServletContainerInitializer> detectedScis;
try {
WebappServiceLoader<ServletContainerInitializer> loader =
new WebappServiceLoader<>(
context);
// 通过SPI加载ServletContainerInitializer的实现
detectedScis = loader.load(ServletContainerInitializer.class);
} catch (IOException e) {
ok = false;
return;
}
// ...
}
加载的方法如下:
// 通过ServiceLoader加载SPI中指定的SCI
public List<T> load(Class<T> serviceType) throws IOException {
// 通过SPI加载META-INF/services下的ServletContainerInitializer的实现.
String configFile = SERVICES + serviceType.getName();
LinkedHashSet<String> applicationServicesFound = new LinkedHashSet<>();
LinkedHashSet<String> containerServicesFound = new LinkedHashSet<>();
ClassLoader loader = servletContext.getClassLoader();
Enumeration<URL> resources;
if (loader == null) {
resources = ClassLoader.getSystemResources(configFile);
} else {
resources = loader.getResources(configFile);
}
// 解析SPI中指定的类到containerServicesFound中
while (resources.hasMoreElements()) {
parseConfigFile(containerServicesFound, resources.nextElement());
}
// 根据正则匹配过滤需要的ServletContainerIntilizier
if (containerSciFilterPattern != null) {
Iterator<String> iter = containerServicesFound.iterator();
while (iter.hasNext()) {
if (containerSciFilterPattern.matcher(iter.next()).find()) {
iter.remove();
}
}
}
containerServicesFound.addAll(applicationServicesFound);
// load the discovered services
if (containerServicesFound.isEmpty()) {
return Collections.emptyList();
}
// 并实例化SPI中的类
return loadServices(serviceType, containerServicesFound);
}
// 加载并实例化SPI中指定的类
private List<T> loadServices(Class<T> serviceType, LinkedHashSet<String> servicesFound)
throws IOException {
ClassLoader loader = servletContext.getClassLoader();
List<T> services = new ArrayList<T>(servicesFound.size());
for (String serviceClass : servicesFound) {
try {
Class<?> clazz = Class.forName(serviceClass, true, loader);
services.add(serviceType.cast(clazz.newInstance()));
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException | ClassCastException e) {
throw new IOException(e);
}
}
return Collections.unmodifiableList(services);
}
加载完成之后了,下边Tomcat去回调onStartup的时候就能把初始化Spring容器需要的类初始化器传入进去了,然后在onStartup方法中调用Spring的类初始化器去初始化Spring的容器。
// 可以传入多个类型
@HandlesTypes(value = {WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
@Override
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
List<WebApplicationInitializer> initializers = new LinkedList<>();
// webAppInitializerClasses表示传入的WebApplicationInitializer的子类
if (webAppInitializerClasses != null) {
for (Class<?> waiClass : webAppInitializerClasses) {
// 判断传入的类是否 (不是接口 && 不是抽象类 && 是WebApplicationInitializer的子类或者子接口)
if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
try {
// 通过反射调用创建传入的类的实例,并且加入到initializers中
initializers.add((WebApplicationInitializer)
ReflectionUtils.accessibleConstructor(waiClass).newInstance());
}
catch (Throwable ex) {
throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
}
}
}
}
// ...中间的其他校验逻辑省略
// 对初始化器排序
AnnotationAwareOrderComparator.sort(initializers);
for (WebApplicationInitializer initializer : initializers) {
// 调用子类的onStartUp方法,子类即WebApplicationInitializer的子类,需要开发者自己实现
// 这是留给开发者的一个扩展点,如果启动时需要tomcat去做一些初始化,可以通过实现WebApplicationInitializer接口
// 然后实现onStartUp方法,tomcat启动时就会调用自定义的onStartUp方法.
initializer.onStartup(servletContext);
}
}
}
通过注解方式实现的SpringMVC通常会实现WebApplicationInitializer接口。再具体的onStartup方法中实现SpringMVC父子容器的初始化,那个属于SpringMVC的启动过程了,后面文章介绍!
3、总结
打成war包之后部署到Tomcat下的应用启动步骤如下:
-
Tomcat启动,通过类加载器加载classpath下的所有class,包括引入的一些第三方的jar包中的class文件,例如spring-web,spring-webmvc等; -
通过SPI机制加载所有jar文件中的META-INF/services下的SPI配置文件中的配置类。此处主要是加载 org.springframework.web.SpringServletContainerInitializer,这个类是实现了Tomcat SCI的一个实现类; -
加载完成之后,Tomcat在启动的过程中会回调该类的onStartup方法; -
在onStartup方法中,又会有SpringMVC的一系列初始化器,通过这些初始化器来完成SpringMVC中父子容器的初始化工作,父子容器的初始化工作即Spring容器的启动过程,之前文章有介绍,可以参考。
近期精彩回顾:
常驻内容:
源码搭建:
关注菜鸟封神记,定期分享技术干货!
点赞和在看是最大的支持,感谢↓↓↓
以上是关于Tomcat是咋启动SpringMVC的?的主要内容,如果未能解决你的问题,请参考以下文章
maven+SpringMVC框架开发启动tomcat报监听异常
大家来找茬-SpringMVC中Tomcat正常启动,始终访问不了Controller,出404错
无法使用从相同 Maven 原型创建的两个 SpringMVC webapps 启动 Tomcat 服务器
intellij idea创建SpringMVC项目启动tomcat报错