在前面几章的讲解中,我们知道,当我们执行以下代码时,springboot会启动一个内置的tomcat,并且加载对应的starter.那么如果我们不采用java -jar的方式启动springboot的应用,他也就没有去执行run方法,那么他又是如何做到自动装配的呢?
在说这些之前,我们先要了解一个东西,SPI。关于SPI可以去了解我的另一篇文章
我们看spring-web这个项目的spi文件javax.servlet.ServletContainerInitializer
文件内容如下
org.springframework.web.SpringServletContainerInitializerSpringServletContainerInitializer代码如下
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
//省略其余代码
}可以看到这个类继承了一个叫ServletContainerInitializer的接口
public interface ServletContainerInitializer {
public void onStartup(Set<Class<?>> c, ServletContext ctx)
throws ServletException;
}这个接口是干嘛的呢?ServletContainerInitializer 是 Servlet 3.0 新增的一个接口,主要用于在容器启动阶段通过编程风格注册Filter, Servlet以及Listener,以取代通过web.xml配置注册。这样就利于开发内聚的web应用框架。容器启动阶段依据java spi获取到所有ServletContainerInitializer的实现类,然后执行其onStartup方法.
也就是说,我们把 ServletContainerInitializer的实现类写在 META-INF / services / javax.servlet.ServletContainerInitializer 文件中,那么Tomcat等容器启动的时候就会去调用所有实现类的onStartup方法。
@HandlesTypes注解是干嘛的呢?
SpringServletContainerInitializer就是ServletContainerInitializer的实现类,可以看到SpringServletContainerInitializer加上了一个@HandlesTypes(WebApplicationInitializer.class)的注解,这个注解的作用就是容器启动的时候调用实现类的onStartup方法的时候,会把注解中标注的接口的实现类当做参数传递进去。
我们看SpringServletContainerInitializer的onStartup方法,在容器启动的时候会调用这个方法,同时set集合参数webAppInitializerClasses即为@HandlesTypes中标注的WebApplicationInitializer的实现类
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
throws ServletException {
//省略其余代码
for (WebApplicationInitializer initializer : initializers) {
//依次回调参数实现类的onStartup方法
initializer.onStartup(servletContext);
}
}最后一段代码initializer.onStartup(servletContext);就是把所有的WebApplicationInitializer的实现类的onStartup方法调用一遍。我们看看这个类的所有实现类
看到实现类中有一个SpringBootServletInitializer,这个类是我们要重点关注的对象,先来看看这个类的注释
也就是说这个类是当我们以war包的方式让外部tomcat运行时才需要关注的类。
我们接着看这个类的onStartup方法
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
WebApplicationContext rootAppContext = createRootApplicationContext(
servletContext);
if (rootAppContext != null) {
servletContext.addListener(new ContextLoaderListener(rootAppContext) {
@Override
public void contextInitialized(ServletContextEvent event) {
// no-op because the application context is already initialized
}
});
}
}createRootApplicationContext方法
protected WebApplicationContext createRootApplicationContext(
ServletContext servletContext) {
//其余代码略
return run(application);
}run方法
protected WebApplicationContext run(SpringApplication application) {
return (WebApplicationContext) application.run();
}可以看到最终上诉会调用到SpringApplication的无参run方法,那么到了这一步,就跟我们通过main方法启动是一个道理了
public static void main(String[] args) {
SpringApplication.run(HppaApplication.class, args);
}SpringBoot通过打成war包的方式运行,其本质上是利用了Servlet3.0规范中的Tomcat启动时会去调用ServletContainerInitializer接口的onStartup方法,同时把使用类注解@HandlesTypes中标注的接口的实现类作为参数传入到onStartup中,并依次调用其实现类的onStartup方法。而SpringServletContainerInitializer实现了ServletContainerInitializer,同时标注了@HandlesTypes(WebApplicationInitializer.class),
@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
//省略其余代码
}那么也就是说,Tomcat启动时,最终会去调用WebApplicationInitializer的实现类的onStartup方法,而SpringBootServletInitializer实现了WebApplicationInitializer
public abstract class SpringBootServletInitializer implements WebApplicationInitializer那也就是最终会调用SpringBootServletInitializer的onStartup方法,而这个onStartup方法最终其实是调用了application.run(),也就类似于我们通过Main方法启动了。为了方便理解,我画了下图