前言
上一篇【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()
方法中。那么两者结合一起的流程就是这样的:
- 首先Tomcat搜索ServletContainerInitializer的实现SpringServletContainerInitializer。
- 然后注解@HandlesTypes会去搜索所有实现了WebApplicationInitializer接口的类,并传入ServletContainerInitializer.onStartup()方法。
- 搜索到由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"]