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报错

tomcat 启动SpringMVC报错 无法访问javax.servlet.ServletException

springmvc 修改方法体需要重启tomcat吗