tomcat 是 web容器(servlet 容器),不管请求是访问静态资源HTML、JSP还是java接口,对tomcat而言,都是通过servlet访问:
所谓 jsp 就是 html 加上 java 代码片段,JspServlet 最终输出的也是 html 而已。
了解 springboot 内嵌 tomcat 启动原理之前,应该了解“祖先” spring 怎么启动和加载上下文的,对 springboot 的理解才深刻。
spring 启动必须依赖 web 容器,这里以 tomcat 举例。
spring 项目中简单的 web 配置如下:
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
/WEB-INF/root-context.xml
app1
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
/WEB-INF/app1-context.xml
1
app1
/app1/*
tomcat 通过调用 spring 中的 servlet 对象(DispatcherServlet
),然后调用该对象的 init() 方法,作为入口启动spring。
如何调用到 spring 中的 servlet 对象,分为以下两种:
WebApplicationInitializer
接口,并现实 WebApplicationInitializer
的 onStartup
方法,在 onStartup
方法中创建 dispacherServlet
并指定使用。上述 java config 方式衍生出另一个问题:WebApplicationInitializer
的实现类怎么被 tomcat 调用到呢?
SPI
机制,找到 ServletContainerInitializer
接口的所有实现类,然后反射生成对象,并轮流执行实现类中 onStartup 方法。SpringServletContainerInitializer
就是 ServletContainerInitializer
实现类之一。SpringServletContainerInitializer
类上有@HandlesTypes({WebApplicationInitializer.class})
注释,这个注释是 servlet 规范中的,tomcat 会通过字节码加载技术(ASM),找到注解中指定类及子类(WebApplicationInitializer.class
及子类),然后将其作为参数传给 SpringServletContainerInitializer
的 onStartup
方法使用,WebApplicationInitializer
就这样被调用了 。所以我们实现了 WebApplicationInitializer
接口,就会被 tomcat 加载。
tomcat 通过调用 DispatcherServlet
对象的 init() 方法,加载 springContext
上下文,所以 DispatcherServlet
对象内有一个字段存放 springContext。
springcontext
,各 servlet 都有自己的上下文。springcontext
中新建,不会共用。不过一般都不会有多个servlet,通常常规项目中 web.xml 中 listener 标签根本就不需要配置。
上述说的 spring 启动,必须依赖 web 容器启动。由 web 容器通过 SPI 机制,加载 spring 自己的 servlet DispatcherServlet,再通过 servlet 对象创建 springContext
。
而 springboot 不需要外部 web 容器了,那它怎么监听端口接收请求呢,难道 springboot 内部又重新写了一个 servlet?当然不是,看下面代码:
@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
// 同时会创建 web 容器。
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
......
}
......
}
上述代码片很熟悉吧,如果不熟的查查spring初始化context流程,这里不做详细描述。
主要看 onRefresh()
方法,该方法启动容器的流程大概如下,以 tomcat 为例:
onRefresh()
方法内,会调用 createWebServer()
方法,createWebServer()
方法内会调用 tomcat/jetty/undertow jar 包依赖提供的方法,来创建 web 容器并启动。new Tomcat()
。Connector
进行配置,并将 DispatcherServlet
添加到 tomcat 容器中。tomcat.start()
启动容器。我们了解到了 springboot 会调用 createWebServer()
方法,创建“合适”的 web 容器。
实际上就是在 createWebServer()
方法里面判断的,该方法代码如下:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 选择用哪个容器
ServletWebServerFactory factory = getWebServerFactory();
// 创建及启动 web 容器
this.webServer = factory.getWebServer(getSelfInitializer());
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
上述代码片,getWebServerFactory()
方法会获得 tomcat/jetty/undertow 的 ServletWebServerFactory
, 用这个 factory 对象就能创建对应的 web 容器。
而在 getWebServerFactory()
方法内是按类型从 SpringContext
中获取 ServletWebServerFactory
类型的 bean。
所以只要 SpringContext
注入什么容器的 ServletWebServerFactory
,springboot 就会启动什么容器。
ServletWebServerFactory
呢?在 ServletWebServerFactoryConfiguration
这个配置类,将 tomcat/jetty/undertow 的 ServletWebServerFactory
注入进 springContext
,配置类伪代码如下:
@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {
// tomcat 的 factory
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedTomcat {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory( ... ) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
...
return factory;
}
}
// Jetty 的 factory
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, Server.class, Loader.class, WebAppContext.class })
@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
static class EmbeddedJetty {
@Bean
JettyServletWebServerFactory JettyServletWebServerFactory( ... ) {
JettyServletWebServerFactory factory = new JettyServletWebServerFactory();
...
return factory;
}
}
// Undertow 的 factory
.......
}
可以看出只要引入了某 web 容器的依赖,对应的 @ConditionalOnClass
就能满足,该 web 容器的 ServletWebServerFactory
就会被注入进 springboot。
还想使用哪个??getWebServerFactory()
方法内就直接报错了:Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : xxx
。
ServletWebServerFactoryConfiguration
类的代码,@ConditionalOnClass
中某些类肯定找不到,运行时不会报错吗?首先应该知道 spring 怎么判断是否是 bean 对象?常人理解 spring 是通过 JVM 反射获取类注解信息,来确定是否反射生成 bean 对象注入 SpringContext
中。
但实际 spring 并不是这样判断的,如果通过 JVM 获取类信息,那不是启动前要把所有类都加载一次,这和 JVM 用时加载的思想冲突了。所以spring 是通过 ASM 技术从 class 字节码文件中获取注解信息,来判断是否是需要的 bean。
之所以没有依赖也不会报错,是因为spring 会通过 ASM 技术取出 @ConditionalOnClass
注解中所有的 values后,会用 ClassLoader
尝试加载这些 values,如果加载不到,catch
住异常使其不会报错,同时这个类也被认为不符合注入条件,不会生成对象注入 springcontext。所以没引入所有 web 容器依赖也不会报错。