SpringMVC新版本(Spring 5.3.0)官网详解(二)源码探究

本文解析了Tomcat如何通过Servlet3.0的SPI机制自动发现并调用Spring中的WebApplicationInitializer实现类,完成Web应用初始化。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

上一篇【SpringMVC新版本(Spring 5.3.0)官网详解】最后留了一个问题。当启动Tomcat的时候,Tomcat自动去访问了Spring框架中实现了WebApplicationInitializer接口的类。那么这个问题就是Tomcat是怎么知道Spring中有一个实现类去初始化各种Web环境的呢?本篇就从源码里探究一下这个是怎么做到的。更多Spring内容进入【Spring解读系列目录】

调用过程

要弄清楚这个问题,首先要知道这个方法的调用链是什么。所以最快的方法就是在WebApplicationInitializer#onStartup()方法里打个断点,去一步一步的跟踪调用链,如下:
在这里插入图片描述

可以看到红框处刚刚开始使用SpringFramework的包,也就是说之前的apache的包应该就是Tomcat的代码,由于这里是plugin,已经无法看到源码。那就跟着调用链进入上一个SpringServletContainerInitializer#onStartup()方法里面,整个类只有这一个方法,代码很少全贴出来。

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
   public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
            throws ServletException {
      //new出来initializers列表
      List<WebApplicationInitializer> initializers = new LinkedList<>();
      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);
               }
            }
         }
      }
      if (initializers.isEmpty()) {
         servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
         return;
      }
      servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
      //排序,为了更快的找到WebApplicationInitializer对象
      AnnotationAwareOrderComparator.sort(initializers);
      for (WebApplicationInitializer initializer : initializers) {
         initializer.onStartup(servletContext);
      }
   }
}

跟着调试进入就到了initializer.onStartup(servletContext);,一个initializer调用了onStartup(),但是这个对象依然是WebApplicationInitializer类型的。那么就要找到for循环中的initializers是从哪里来的。往上走initializers经过了一个判断,然后通过waiClass反射构造出来。waiClass又是从webAppInitializerClasses里面经过循环和判断条件选择出来的,所以可以判定initializer是传入进来的。

那么到此其实可以有一个合理的推断,就是Tomcat底层会去搜索所有ServletContainerInitializer的类,如果找到了就执行它的onStartup(),进而调用我们自己写的onStartup()方法。

Service Provider Interface

为什么Tomcat能够去搜索并调用Spring的这个类呢?之所以能够做到这一点是因为Servlet3.0中增加了一个新的功能(注:Servlet已经更新到4.x了,此功能是3.0就添加了)。新功能的名字叫做Service Provider Interface(简称SPI),是JDK为厂商和插件提供的一种服务提供发现机制。当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录下创建一个以接口命名的文件,在文件里面配置上实现类。就可以使得外部程序调用这个服务的时候通过配置的文件找到具体实现类,并通过反射机制完成这个服务的注入。这个功能是由java.util.ServiceLoader工具类实现的。

好像越说越饶了。简单说,在META-INF/services/目录下的文件名,可以被自动扫描并识别文件名字,再通过文件内部写好的实现类名,完成一个自动注入的过程。文件名字必须是一个接口名,内容必须是这个接口的实现类。

SpringServletContainerInitializer类实现的接口就在Spring-Web的包里配置到了spring-web-5.2.8.RELEASE.jar\META-INF\services\javax.servlet.ServletContainerInitializer。目录配置的文件名就是接口ServletContainerInitializer
在这里插入图片描述

而里面的内容就是ServletContainerInitializer的实现类SpringServletContainerInitializer
在这里插入图片描述

通过这样的配置就可以完成一个自动插拔的Java服务。

注解HandlesTypes的作用

介绍完上面的SPI的内容,就要说到这个注解了。@HandlesTypes作用是将注解指定的Class对象作为参数传递到onStartup()方法中。那么两者结合一起的流程就是这样的:

  1. 首先Tomcat搜索ServletContainerInitializer的实现SpringServletContainerInitializer。
  2. 然后注解@HandlesTypes会去搜索所有实现了WebApplicationInitializer接口的类,并传入ServletContainerInitializer.onStartup()方法。
  3. 搜索到由SpringServletContainerInitializer.onStartup()去完成这个实现,然后循环调用WebApplicationInitializer.onStartup()方法,去运行并完成一个Web启动配置。

这也就是为什么Spring-MVC可以通过纯Java的模式和Tomcat组成一个直接能够运行的Web项目。但是要注意分清楚,这里一共有两个onStartup()方法:一个是ServletContainerInitializer.onStartup()方法,由Spring和Tomcat共同实现;一个是WebApplicationInitializer.onStartup()方法,由用户进行Web配置。

总结

SpringMVC就是充分利用了Servlet3.0加入的用户自定义接口的特性进行的扫描Web配置,并且动态的添加Servlet。用这个功能可以做一个动态插拔的外部接口,但是xml绝对无法实现。

附:动态插拔的外部服务

这里就还已ServletContainerInitializer为例子,因为其onStartup()非常合适做自插拔功能。
功能接口MyHandle:

public interface MyHandle {
}

实现类MyHandleTest:

public class MyHandleTest implements MyHandle {
}

自动执行类MyServletContainerInitializer:

@HandlesTypes(MyHandle.class)//这里将会找到所有MyHandle接口的实现而不是找到这个类,传递到onStartup方法里
public class MyServletContainerInitializer implements ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException {
        //直接写绝对打印不了,只有加入了META-INF/services下才可以
        System.out.println("this is my onStartup()");
        System.out.println(c.toArray()[0].toString());
    }
}

文件配置:

文件名: javax.servlet.ServletContainerInitializer
文件内容:com.demo.app.MyServletContainerInitializer

位置:
在这里插入图片描述
运行输出:

Nov 09, 2020 4:09:39 PM org.apache.coyote.AbstractProtocol init
INFO: Initializing ProtocolHandler ["http-bio-8080"]
Nov 09, 2020 4:09:39 PM org.apache.catalina.core.StandardService startInternal
INFO: Starting service Tomcat
Nov 09, 2020 4:09:39 PM org.apache.catalina.core.StandardEngine startInternal
INFO: Starting Servlet Engine: Apache Tomcat/7.0.47
Nov 09, 2020 4:09:41 PM org.apache.catalina.core.ApplicationContext log
INFO: 1 Spring WebApplicationInitializers detected on classpath
this is my onStartup()   //这里就是测试中的打印
class com.demo.app.MyHandleTest  //这里就是测试中的打印
[INFO] Initializing Servlet 'myDS'
Nov 09, 2020 4:09:42 PM org.apache.catalina.core.ApplicationContext log
INFO: Initializing Spring DispatcherServlet 'myDS'
[INFO] Completed initialization in 249 ms
Nov 09, 2020 4:09:42 PM org.apache.coyote.AbstractProtocol start
INFO: Starting ProtocolHandler ["http-bio-8080"]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值