揭秘Spring Boot内嵌Tomcat原理

tomcat 介绍

tomcat 是 web容器(servlet 容器),不管请求是访问静态资源HTML、JSP还是java接口,对tomcat而言,都是通过servlet访问:

  • 访问静态资源,tomcat 会交由一个叫做DefaultServlet的类来处理。
  • 访问 JSP,tomcat 会交由一个叫做JspServlet的类来处理。
  • 访问 Servlet ,tomcat 会交由一个叫做 InvokerServlet的类来处理。

所谓 jsp 就是 html 加上 java 代码片段,JspServlet 最终输出的也是 html 而已。

tomcat 启动 spring 项目

了解 springboot 内嵌 tomcat 启动原理之前,应该了解"祖先" spring 怎么启动和加载上下文的,对 springboot 的理解才深刻。

web.xml配置

spring 启动必须依赖 web 容器,这里以 tomcat 举例。

spring 项目中简单的 web 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app> 
	<listener> 
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> 
	</listener> 
	<context-param> 
		<param-name>contextConfigLocation</param-name> 
		<param-value>/WEB-INF/root-context.xml</param-value> 
	</context-param> 
	<servlet> 
		<servlet-name>app1</servlet-name> 
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> 
		<init-param> 
			<param-name>contextConfigLocation</param-name> 
			<param-value>/WEB-INF/app1-context.xml</param-value> 
		</init-param> 
		<load-on-startup>1</load-on-startup> 
	</servlet> 
	<servlet-mapping> 
		<servlet-name>app1</servlet-name> 
		<url-pattern>/app1/*</url-pattern> 
	</servlet-mapping> 
</web-app>

spring 怎么启动?

tomcat 通过调用 spring 中的 servlet 对象(DispatcherServlet),然后调用该对象的 init() 方法,作为入口启动spring。

如何调用到 spring 中的 servlet 对象,分为以下两种:

  • web.xml 配置文件方式:tomcat 通过 web.xml 配置文件中 servlet 标签指定的 servlet 类路径,反射生成 servlet 对象。
  • java config 方式:没有 web.xml 配置文件了,项目中必须实现 WebApplicationInitializer 接口,并现实 WebApplicationInitializeronStartup 方法,在 onStartup 方法中创建 dispacherServlet 并指定使用。

上述 java config 方式衍生出另一个问题:WebApplicationInitializer 的实现类怎么被 tomcat 调用到呢?

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {
				......
			}
}
  • tomcat 会通过SPI机制,找到 ServletContainerInitializer 接口的所有实现类,然后反射生成对象,并轮流执行实现类中 onStartup 方法。SpringServletContainerInitializer 就是 ServletContainerInitializer 实现类之一。
  • SpringServletContainerInitializer 类上有@HandlesTypes({WebApplicationInitializer.class})注释,这个注释是 servlet 规范中的,tomcat 会通过字节码加载技术(ASM),找到注解中指定类及子类(WebApplicationInitializer.class 及子类),然后将其作为参数传给 SpringServletContainerInitializeronStartup方法使用。我们自定义的WebApplicationInitializer 实现类就这样被调用到了 。

所以我们实现了 WebApplicationInitializer 接口,就会被 tomcat 加载。

怎么加载 springContext?

tomcat 通过调用 DispatcherServlet 对象的 init() 方法,加载 springContext 上下文,所以 DispatcherServlet 对象内有一个字段存放 springContext。

若配置多个 servlet,springContext 有几个?

  • 每个 servlet 创建过程中,都会创建 springcontext,各 servlet 都有自己的上下文。
  • 所以各 servlet 的 init-param 标签中,指定的扫描路径如果有重复的,重复的 bean 对象也会在各 servlet 的 springcontext 中新建,不会共用。
  • 那么重复的 bean 对象是不是有点浪费内存呢?确实如此,所以在多个servlet的情况下,需要配 listener 标签,表示配置父容器(root context)。可以合理配置父容器加载 service 和 repository 层的 bean,这部分可以共用,而 controller 层的 bean 是在各 servlet 中的子容器中加载,因为涉及到servlet路由嘛。

不过一般都不会有多个servlet,通常常规项目中 web.xml 中 listener 标签根本就不需要配置。

springboot 启动 tomcat

启动 web 容器

上述说的 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 容器并启动。
  • 比如 tomcat,它是这样创建:new Tomcat()
  • 创建容器后,对 tomcat 容器的Connector进行配置,并将 DispatcherServlet 添加到 tomcat 容器中。
  • 最后 tomcat.start() 启动容器。

tomcat\jetty\undertow springboot用哪个?

我们了解到了 springboot 会调用 createWebServer() 方法,创建"合适"的 web 容器。

  • 那 springboot 怎么判断该创建 tomcat、jetty 还是 undertow 容器呢?

实际上就是在 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||undertowServletWebServerFactory, 用这个 factory 对象就能创建对应的 web 容器。

而在 getWebServerFactory() 方法内是按类型从 SpringContext 中获取 ServletWebServerFactory 类型的 bean。

所以只要 SpringContext 注入什么容器的 ServletWebServerFactory,springboot 就会启动什么容器。

  • 什么地方注入 ServletWebServerFactory 呢?

ServletWebServerFactoryConfiguration 这个配置类,将tomcat||jetty||undertowServletWebServerFactory 注入进 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。

  • 那项目如果引入了多个 web 容器依赖,springboot 使用哪一个?

还想使用哪个??getWebServerFactory() 方法内就直接报错了:Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : xxx

  • 我们平时使用 tomcat ,那 jetty、undertow 等容器依赖便不会引入。那上述 ServletWebServerFactoryConfiguration 类的代码,@ConditionalOnClass 中某些类肯定找不到,运行时不会报错吗?

首先应该知道 spring 怎么判断是否是 bean 对象?常人理解 spring 是通过 JVM 反射获取类注解信息,来确定是否反射生成 bean 对象注入 SpringContext 中。

但实际 spring 并不是这样判断的,如果通过 JVM 获取类信息,那不是启动前要把所有类都加载一次,这和 JVM 用时加载的思想冲突了。所以spring 是通过 ASM 技术从 class 字节码文件中获取注解信息,来判断是否是需要的 bean。

之所以没有依赖也不会报错,是因为spring 会通过 ASM 技术取出 @ConditionalOnClass 注解中所有的 values后,会用 ClassLoader 尝试加载这些 values,如果加载不到,catch住异常使其不会报错,同时这个类也被认为不符合注入条件,不会生成对象注入 springcontext。所以没引入所有 web 容器依赖也不会报错。

相关推荐
小_太_阳34 分钟前
Scala_【1】概述
开发语言·后端·scala·intellij-idea
智慧老师44 分钟前
Spring基础分析13-Spring Security框架
java·后端·spring
lxyzcm1 小时前
C++23新特性解析:[[assume]]属性
java·c++·spring boot·c++23
搬码后生仔2 小时前
asp.net core webapi项目中 在生产环境中 进不去swagger
chrome·后端·asp.net
迷糊的『迷』2 小时前
vue-axios+springboot实现文件流下载
vue.js·spring boot
凡人的AI工具箱2 小时前
每天40分玩转Django:Django国际化
数据库·人工智能·后端·python·django·sqlite
Lx3523 小时前
Pandas数据重命名:列名与索引为标题
后端·python·pandas
小池先生3 小时前
springboot启动不了 因一个spring-boot-starter-web底下的tomcat-embed-core依赖丢失
java·spring boot·后端
百罹鸟3 小时前
【vue高频面试题—场景篇】:实现一个实时更新的倒计时组件,如何确保倒计时在页面切换时能够正常暂停和恢复?
vue.js·后端·面试
苹果醋34 小时前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx