1. springboot创建的微服务工程为何直接可以被打成jar包?
springboot创建的应用能够直接被打成jar包运行,而不用像传统的web应用一样需要部署到web容器中就能够直接启动,这是因为springboot内部内嵌了一个tomcat,jar包方式以main方法作为入口执行启动逻辑的时候,底层实际上启动了一个tomcat。
2. springboot内置tomcat
2.1 内置tomcat依赖
使用springboot开发web工程,pom.xml中一般会配置如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
点击进去就能看到SpringBoot tomcat的启动依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.6.1</version>
<scope>compile</scope>
</dependency>
<!-- 下面就是spring-boot-stater-web的pom中引入的内置tomcat的依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.6.1</version>
<scope>compile</scope>
</dependency>
开发阶段,我们一般使用内置tomcat就足够了,只需要在springboot工程的pom.xml中配置上述的spring-boot-starter-web依赖即可。
然后main入口如下:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
2.2 生产环境一般是打成war包进行发布
在生产环境上,我们一般是将springboot应用打成war包而不是jar包,然后需要排除内置tomcat,然后手动引入外置的tomcat来使用。
这主要是因为生产环境对tomcat的性能和安全要求比较高,不太信任springboot内置的tomcat。
生产环境上的springboot的pom文件中应该做如下配置:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<!-- 移除嵌入式tomcat依赖 -->
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加servlet-api依赖--->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<scope>provided</scope>
</dependency>
然后我们需要更新main函数:
@SpringBootApplication
public class Application extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return builder.sources(this.getClass());
}
}
为什么war包方式使用外置tomcat时,main函数需要继承SpringBootServletInitializer并重写configure方法呢?
(1)启动入口不一样。
jar包启动的入口是main方法,底层会启动一个springboot内置的tomcat;
而war包方式发布入口就是由外部的tomcat进行管理了,其会加载web.xml。
(2)为何要继承 SpringBootServletInitializer?
根本原因是servlet3.0 的新特性,不再需要web.xml配置文件了。因此需要继承 SpringBootServletInitializer 并复写configure方法。
3. springboot内嵌tomcat源码解析
3.1 从main方法说起
public ConfigurableApplicationContext run(String... args) {
long startTime = System.nanoTime();
DefaultBootstrapContext bootstrapContext = this.createBootstrapContext();
ConfigurableApplicationContext context = null;
this.configureHeadlessProperty();
SpringApplicationRunListeners listeners = this.getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
// 创建应用上下文
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
// 刷新上下文
this.refreshContext(context);
this.afterRefresh(context, applicationArguments);
Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), timeTakenToStartup);
}
listeners.started(context, timeTakenToStartup);
this.callRunners(context, applicationArguments);
} catch (Throwable var12) {
this.handleRunFailure(context, var12, listeners);
throw new IllegalStateException(var12);
}
try {
Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
listeners.ready(context, timeTakenToReady);
return context;
} catch (Throwable var11) {
this.handleRunFailure(context, var11, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var11);
}
}
3.2 创建上下文
context = this.createApplicationContext()主要用来创建上下文:
static class Factory implements ApplicationContextFactory {
Factory() {
}
public ConfigurableApplicationContext create(WebApplicationType webApplicationType) {
// 内嵌tomcat走这里
return webApplicationType != WebApplicationType.SERVLET ? null : new AnnotationConfigServletWebServerApplicationContext();
}
}
这里因为springboot默认就是SERVLET类型,因此会创建AnnotationConfigServletWebServerApplicationContext上下文。
而 AnnotationConfigServletWebServerApplicationContext 继承了 ServletWebServerApplicationContext,最终会进入到AbstractApplicationContext中。
3.3 刷新上下文
上面分析了,流程最终进入到AbstractApplicationContext中,因此刷新源码如下:
public void refresh() throws BeansException, IllegalStateException {
Object var1 = this.startupShutdownMonitor;
synchronized(this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
this.prepareRefresh();
ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
this.prepareBeanFactory(beanFactory);
try {
this.postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
this.invokeBeanFactoryPostProcessors(beanFactory);
this.registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
this.initMessageSource();
this.initApplicationEventMulticaster();
//调用各个子类的onRefresh()方法,也就说这里要回到子类:ServletWebServerApplicationContext,调用该类的onRefresh()方法
this.onRefresh();
this.registerListeners();
this.finishBeanFactoryInitialization(beanFactory);
this.finishRefresh();
} catch (BeansException var10) {
if (this.logger.isWarnEnabled()) {
this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
}
this.destroyBeans();
this.cancelRefresh(var10);
throw var10;
} finally {
this.resetCommonCaches();
contextRefresh.end();
}
}
}
上面这段代码老生常谈了。
3.4 ServletWebServerApplicationContext中创建web环境
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext:
protected void onRefresh() {
super.onRefresh();
try {
// 创建web服务器,就此进入创建web服务器的步骤。
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start web server", var2);
}
}
createWebServer方法:
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = this.getServletContext();
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
// 这里是创建webServer,但是还没有启动tomcat,这里是通过ServletWebServerFactory创建,那么接着看下ServletWebServerFactory
ServletWebServerFactory factory = this.getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
createWebServer.end();
this.getBeanFactory().registerSingleton("webServerGracefulShutdown", new WebServerGracefulShutdownLifecycle(this.webServer));
this.getBeanFactory().registerSingleton("webServerStartStop", new WebServerStartStopLifecycle(this, this.webServer));
} else if (servletContext != null) {
try {
this.getSelfInitializer().onStartup(servletContext);
} catch (ServletException var5) {
throw new ApplicationContextException("Cannot initialize servlet context", var5);
}
}
this.initPropertySources();
}
3.5 getWebServerFactory工厂
该方法会返回一个ServletWebServerFactory类型的实例,这个ServletWebServerFactory是一个接口。
protected ServletWebServerFactory getWebServerFactory() {
String[] beanNames = this.getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing ServletWebServerFactory bean.");
} else if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
} else {
return (ServletWebServerFactory)this.getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
}
ServletWebServerFactory的实现子类有如下:
org.springframework.boot.web.embedded.jetty.JettyServletWebServerFactory
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory
org.springframework.boot.web.embedded.undertow.UndertowServletWebServerFactory
而其中我们最常用的只有两个:JettyServletWebServerFactory和TomcatServletWebServerFactory。
3.6 TomcatServletWebServerFactory
因为我们使用内置tomcat,因此流程自动进入实现类TomcatServletWebServerFactory中。
调用的代码如下:
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory中如下:
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
// 创建tomcat对象
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
// 创建Connector对象
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
// 获取engine引擎
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
// 为tomcat准备上下文,可以理解为用代码写web.xml,tomcat启动的时候会去加载这里设置的信息
this.prepareContext(tomcat.getHost(), initializers);
// 获取tomcat应用服务器对象
return this.getTomcatWebServer(tomcat);
}
getTomcatWebServer:
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, this.getPort() >= 0, this.getShutdown());
}
getEngine:
// org.apache.catalina.startup.Tomcat
// 此时已经进入到tomcat的核心源码中了。
// Engine是最高级别容器,Host是Engine的子容器,Context是Host的子容器,Wrapper是Context的子容器
public Engine getEngine() {
Service service = this.getServer().findServices()[0];
if (service.getContainer() != null) {
return service.getContainer();
} else {
Engine engine = new StandardEngine();
engine.setName("Tomcat");
engine.setDefaultHost(this.hostname);
engine.setRealm(this.createDefaultRealm());
service.setContainer(engine);
return engine;
}
}
总的来说,getWebServer这个方法创建了Tomcat对象,并且做了两件重要的事情:
(1)把Connector对象添加到tomcat中;
(2)configureEngine(tomcat.getEngine());
getWebServer方法返回的是TomcatWebServer。
3.7 继续深入getTomcatWebServer方法
getWebServer方法传入了一堆ServletContextInitializer的实例作为参数进入,最终调用了getTomcatWebServer方法:
return this.getTomcatWebServer(tomcat);
protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
return new TomcatWebServer(tomcat, this.getPort() >= 0, this.getShutdown());
}
看下TomcatWebServer的构造。
org.springframework.boot.web.embedded.tomcat.TomcatWebServer:
// 这里调用了TomcatWebServer构造函数创建它的对象
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
this.monitor = new Object();
this.serviceConnectors = new HashMap();
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = shutdown == Shutdown.GRACEFUL ? new GracefulShutdown(tomcat) : null;
// 核心是这个tomcat server初始化方法
this.initialize();
}
tomcat server初始化方法如下:
private void initialize() throws WebServerException {
// 控制台会打印如下log
logger.info("Tomcat initialized with port(s): " + this.getPortsDescription(false));
Object var1 = this.monitor;
synchronized(this.monitor) {
try {
this.addInstanceIdToEngineName();
Context context = this.findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && "start".equals(event.getType())) {
this.removeServiceConnectors();
}
});
// 核心:在这里启动tomcat
this.tomcat.start();
this.rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
} catch (NamingException var5) {
;
}
// 开启阻塞后台线程
this.startDaemonAwaitThread();
} catch (Exception var6) {
this.stopSilently();
this.destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", var6);
}
}
}
3.8 tomcat源码
上面启动了tomcat,进入到org.apache.catalina.startup.Tomcat中查看究竟:
public void start() throws LifecycleException {
this.getServer();
this.server.start();
}
public void stop() throws LifecycleException {
this.getServer();
this.server.stop();
}
public void destroy() throws LifecycleException {
this.getServer();
this.server.destroy();
}
getServer方法如下:
// 一个tomcat会有一个server
public Server getServer() {
if (this.server != null) {
return this.server;
} else {
System.setProperty("catalina.useNaming", "false");
this.server = new StandardServer();
this.initBaseDir();
ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(this.basedir), (String)null));
this.server.setPort(-1);
Service service = new StandardService();
service.setName("Tomcat");
this.server.addService(service);
return this.server;
}
}
3.9 返回了一个TomcatWebServer后,应用就可以直接调用其内部的start和stop方法
源码在org.springframework.boot.web.embedded.tomcat.TomcatWebServer中。 start方法:
public void start() throws WebServerException {
Object var1 = this.monitor;
synchronized(this.monitor) {
if (!this.started) {
boolean var10 = false;
try {
var10 = true;
this.addPreviouslyRemovedConnectors();
Connector var2 = this.tomcat.getConnector();
if (var2 != null && this.autoStart) {
this.performDeferredLoadOnStartup();
}
this.checkThatConnectorsHaveStarted();
this.started = true;
logger.info("Tomcat started on port(s): " + this.getPortsDescription(true) + " with context path '" + this.getContextPath() + "'");
var10 = false;
} catch (ConnectorStartFailedException var11) {
this.stopSilently();
throw var11;
} catch (Exception var12) {
PortInUseException.throwIfPortBindingException(var12, () -> {
return this.tomcat.getConnector().getPort();
});
throw new WebServerException("Unable to start embedded Tomcat server", var12);
} finally {
if (var10) {
Context context = this.findContext();
ContextBindings.unbindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
}
}
Context context = this.findContext();
ContextBindings.unbindClassLoader(context, context.getNamingToken(), this.getClass().getClassLoader());
}
}
}
stop方法:
public void stop() throws WebServerException {
Object var1 = this.monitor;
synchronized(this.monitor) {
boolean wasStarted = this.started;
try {
this.started = false;
try {
if (this.gracefulShutdown != null) {
this.gracefulShutdown.abort();
}
this.stopTomcat();
this.tomcat.destroy();
} catch (LifecycleException var10) {
;
}
} catch (Exception var11) {
throw new WebServerException("Unable to stop embedded Tomcat", var11);
} finally {
if (wasStarted) {
containerCounter.decrementAndGet();
}
}
}
}
到这里为止,springboot内嵌tomcat的这个创建和启动就分析完毕了。
4. 内嵌tomcat如何初始化servlet容器?
传统的tomcat是独立外置的,在启动时会读取指定目录下的web.xml,从而加载我们web.xml中描述的各种servlet、filter、listener等,将这些组件纳入到tomcat容器中进行管理。
那么springboot内置tomcat又是如何管理我们定义的servlet、filter、listener呢?
看这里之前,建议先参考如下文章:springboot里面为啥没有web.xml?
参考完“springboot里面为啥没有web.xml?”这篇文章后,我们知道了,注册servlet可以在传统web.xml存在的情况下注册;也可以借助spring对servlet3.0的支持,通过SPI和ServletContainerInitializer废除web.xml的情况下进行注册。
不过这两种情况,都和springboot对servlet的注册没有多大关系。
springboot没有完全遵守 servlet3.0 的规范.
我们先来看看springboot中是如何注册servlet和filter的?
4.1 注册方式一:servlet3.0注解+@ServletComponentScan
springboot依旧兼容 servlet3.0 一系列以 @Web* 开头的注解:@WebServlet,@WebFilter,@WebListener。
比如:
@WebServlet("/hello")
public class HelloWorldServlet extends HttpServlet{}
@WebFilter("/hello/*")
public class HelloWorldFilter implements Filter {}
然后不要忘记让启动类去扫描到这些注解:
@SpringBootApplication
@ServletComponentScan
public class SpringBootServletApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootServletApplication.class, args);
}
}
我认为这是几种方式中最为简洁的方式,如果真的有特殊需求,需要在 springboot 下注册 servlet,filter,可以采用这样的方式,比较直观。
4.2 注册方式二:RegistrationBean
如下示例:
@Bean
public ServletRegistrationBean helloWorldServlet() {
ServletRegistrationBean helloWorldServlet = new ServletRegistrationBean();
myServlet.addUrlMappings("/hello");
myServlet.setServlet(new HelloWorldServlet());
return helloWorldServlet;
}
@Bean
public FilterRegistrationBean helloWorldFilter() {
FilterRegistrationBean helloWorldFilter = new FilterRegistrationBean();
myFilter.addUrlPatterns("/hello/*");
myFilter.setFilter(new HelloWorldFilter());
return helloWorldFilter;
}
ServletRegistrationBean 和 FilterRegistrationBean 都继承自 RegistrationBean ,RegistrationBean 是 springboot 中广泛应用的一个注册类,负责把 servlet,filter,listener 给容器化,使他们被 spring 托管,并且完成自身对 web 容器的注册。这种注册方式也值得推崇。
RegistrationBean结构图如下:
从图中可以看出 RegistrationBean 的地位,它的几个实现类作用分别是:
帮助容器注册 filter,servlet,listener,最后的 DelegatingFilterProxyRegistrationBean 使用的不多,但熟悉 SpringSecurity 的朋友不会感到陌生,SpringSecurityFilterChain 就是通过这个代理类来调用的。另外 RegistrationBean 实现了 ServletContextInitializer 接口,这个接口将会是下面分析的核心接口,大家先混个眼熟,了解下它有一个抽象实现 RegistrationBean 即可。
5.springboot如何加载servlet?
内嵌tomcat的创建和启动已经看到了,那么它是怎么初始化servlet容器的呢? springboot 内嵌的 tomcat 并没有完全遵守 servlet3.0 规范,主要区别就是对ServletContainerInitializer这个接口的处理:
(1)如果是外部tomcat,那么ServletContainerInitializer的实现类是通过SPI服务发现机制进行加载的;
(2)而内置的tomcat,是专门new了一个TomcatStarter作为ServletContainerInitializer的实现类进行容器初始化的。
不知道ServletContainerInitializer是啥东西的,继续回去看看springboot里面为啥没有web.xml?,外置tomcat加载servlet的流程也在这篇文章中分析过了。
下面我们只分析内嵌tomcat时,springboot对servlet等的加载源码!!!
5.1 内嵌tomcat完全走的另外一套逻辑
当使用内嵌的 tomcat 时,你会发现 springboot 完全走了另一套初始化流程,完全没有使用springboot里面为啥没有web.xml?中提到的 SpringServletContainerInitializer,实际上一开始我在各种 ServletContainerInitializer 的实现类中打了断点,最终定位到,根本没有运行到 SpringServletContainerInitializer 内部,而是进入了 TomcatStarter 这个类中。
ServletContainerInitializer类图:
并且,仔细扫了一眼源码的包,并没有发现有 SPI 文件对应到 TomcatStarter。于是我猜想,内嵌 tomcat 的加载可能不依赖于 servlet3.0 规范和 SPI!它完全走了一套独立的逻辑。为了验证这一点,我翻阅了 spring github 中的 issue,得到了 spring 作者肯定的答复:https://github.com/spring-projects/spring-boot/issues/321 :
This was actually an intentional design decision. The search algorithm used by the containers was problematic. It also causes problems when you want to develop an executable WAR as you often want a javax.servlet.ServletContainerInitializer for the WAR that is not executed when you run java -jar.
See the org.springframework.boot.context.embedded.ServletContextInitializerfor an option that works with Spring Beans.
springboot 这么做是有意而为之。springboot 考虑到了如下的问题,我们在使用 springboot 时,开发阶段一般都是使用内嵌 tomcat 容器,但部署时却存在两种选择:一种是打成 jar 包,使用 java -jar 的方式运行;另一种是打成 war 包,交给外置容器去运行。前者就会导致容器搜索算法出现问题,因为这是 jar 包的运行策略,不会按照 servlet3.0 的策略去加载 ServletContainerInitializer!最后作者还提供了一个替代选项:ServletContextInitializer,注意是 ServletContextInitializer!它和 ServletContainerInitializer 长得特别像,别搞混淆了,前者 ServletContextInitializer 是 org.springframework.boot.web.servlet.ServletContextInitializer,后者 ServletContainerInitializer 是 javax.servlet.ServletContainerInitializer,前文还提到 RegistrationBean 实现了 ServletContextInitializer 接口。
5.2 TomcatStarter在哪里加载的呢?
既然内嵌tomcat对ServletContainerInitializer接口的实现是完全自己new的一个实现类TomcatStarter,而不是通过SPI方式主动发现的,那么TomcatStarter在哪里被加载呢?
之前的源码请参见上面分析的,直接从org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory中开始:
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = this.baseDirectory != null ? this.baseDirectory : this.createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
this.customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
this.configureEngine(tomcat.getEngine());
Iterator var5 = this.additionalTomcatConnectors.iterator();
while(var5.hasNext()) {
Connector additionalConnector = (Connector)var5.next();
tomcat.getService().addConnector(additionalConnector);
}
// 为tomcat准备上下文,可以理解为用代码写web.xml,tomcat启动的时候会去加载这里设置的信息
this.prepareContext(tomcat.getHost(), initializers);
return this.getTomcatWebServer(tomcat);
}
prepareContext源码:
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
File documentRoot = this.getValidDocumentRoot();
// tomcat内嵌上下文
TomcatEmbeddedContext context = new TomcatEmbeddedContext();
if (documentRoot != null) {
context.setResources(new TomcatServletWebServerFactory.LoaderHidingResourceRoot(context));
}
context.setName(this.getContextPath());
context.setDisplayName(this.getDisplayName());
context.setPath(this.getContextPath());
File docBase = documentRoot != null ? documentRoot : this.createTempDir("tomcat-docbase");
context.setDocBase(docBase.getAbsolutePath());
context.addLifecycleListener(new FixContextListener());
context.setParentClassLoader(this.resourceLoader != null ? this.resourceLoader.getClassLoader() : ClassUtils.getDefaultClassLoader());
this.resetDefaultLocaleMapping(context);
this.addLocaleMappings(context);
try {
context.setCreateUploadTargets(true);
} catch (NoSuchMethodError var8) {
// Tomcat is < 8.5.39. Continue.
;
}
this.configureTldPatterns(context);
WebappLoader loader = new WebappLoader();
loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
loader.setDelegate(true);
context.setLoader(loader);
if (this.isRegisterDefaultServlet()) {
this.addDefaultServlet(context);
}
if (this.shouldRegisterJspServlet()) {
this.addJspServlet(context);
this.addJasperInitializer(context);
}
context.addLifecycleListener(new TomcatServletWebServerFactory.StaticResourceConfigurer(context));
ServletContextInitializer[] initializersToUse = this.mergeInitializers(initializers);
host.addChild(context);
// 重点: 设置上下文
this.configureContext(context, initializersToUse);
this.postProcessContext(context);
}
设置上下文:
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
// tomcatStater出现了!!!
// 这个TomcatStarter 实现的就是 servlet3.0 规范的ServletContainerInitializer接口.
// 我们可以看到完全就是手动new的而不是通过SPI机制主动发现的
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
// 如果context是内嵌的tomcat上下文
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext)context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
// 给context添加ServletContainerInitializer!
// 注意,这里的context已经是tomcat源码的东西了,注册的stater实际上就是一个ServletContainerInitializer对象。
// 注册进去后,tomcat启动时会遍历回调所有ServletContainerInitializer对象的onStartup方法。
context.addServletContainerInitializer(starter, NO_CLASSES);
Iterator var7 = this.contextLifecycleListeners.iterator();
while(var7.hasNext()) {
LifecycleListener lifecycleListener = (LifecycleListener)var7.next();
context.addLifecycleListener(lifecycleListener);
}
var7 = this.contextValves.iterator();
while(var7.hasNext()) {
Valve valve = (Valve)var7.next();
context.getPipeline().addValve(valve);
}
var7 = this.getErrorPages().iterator();
while(var7.hasNext()) {
ErrorPage errorPage = (ErrorPage)var7.next();
org.apache.tomcat.util.descriptor.web.ErrorPage tomcatErrorPage = new org.apache.tomcat.util.descriptor.web.ErrorPage();
tomcatErrorPage.setLocation(errorPage.getPath());
tomcatErrorPage.setErrorCode(errorPage.getStatusCode());
tomcatErrorPage.setExceptionType(errorPage.getExceptionName());
context.addErrorPage(tomcatErrorPage);
}
var7 = this.getMimeMappings().iterator();
while(var7.hasNext()) {
Mapping mapping = (Mapping)var7.next();
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
this.configureSession(context);
this.configureCookieProcessor(context);
(new DisableReferenceClearingContextCustomizer()).customize(context);
var7 = this.getWebListenerClassNames().iterator();
while(var7.hasNext()) {
String webListenerClassName = (String)var7.next();
context.addApplicationListener(webListenerClassName);
}
var7 = this.tomcatContextCustomizers.iterator();
while(var7.hasNext()) {
TomcatContextCustomizer customizer = (TomcatContextCustomizer)var7.next();
customizer.customize(context);
}
}
到这里,我们终于找到TomcatStarter了。
后续的源码就不继续深入了,我们只需要记住:
TomcatStarter就是在这里加载的,但是其onStartup方法是等内嵌的tomcat启动后触发的,需要注意的是内嵌的tomcat只会加载TomcatStarter,不会加载其他的ServletContainerInitializer 的实现类:
5.3 TomcatStarter如何注册servlet?
既然内嵌的tomcat在启动时会回调TomcatStarter的onStartup方法,我们来看下TomcatStarter的源码:
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package org.springframework.boot.web.embedded.tomcat;
import java.util.Set;
import javax.servlet.ServletContainerInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.boot.web.servlet.ServletContextInitializer;
// TomcatStarter 实现了接口 ServletContainerInitializer
class TomcatStarter implements ServletContainerInitializer {
private static final Log logger = LogFactory.getLog(TomcatStarter.class);
private final ServletContextInitializer[] initializers;
private volatile Exception startUpException;
// 构造方法接受一个集合:ServletContextInitializer类型
TomcatStarter(ServletContextInitializer[] initializers) {
this.initializers = initializers;
}
// onStartup方法说白了就是在遍历 ServletContextInitializer 对象,执行其中的onStartup方法。
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
try {
ServletContextInitializer[] var3 = this.initializers;
int var4 = var3.length;
for(int var5 = 0; var5 < var4; ++var5) {
// ServletContextInitializer 才是初始化Servlet的关键
ServletContextInitializer initializer = var3[var5];
initializer.onStartup(servletContext);
}
} catch (Exception var7) {
this.startUpException = var7;
if (logger.isErrorEnabled()) {
logger.error("Error starting Tomcat context. Exception: " + var7.getClass().getName() + ". Message: " + var7.getMessage());
}
}
}
Exception getStartUpException() {
return this.startUpException;
}
}
可以看到TomcatStarter 中的 org.springframework.boot.context.embedded.ServletContextInitializer 是 springboot 初始化 servlet,filter,listener 的关键。
通过上面的源码可以看出,TomcatStarter 的主要逻辑就是负责调用一系列 ServletContextInitializer 的 onStartup 方法,那么在 debug 中,ServletContextInitializer [] initializers 到底包含了哪些类呢?会不会有我们前面介绍的 RegisterBean 呢?
我们来一步步debug下看看。
createWebServer:
核心代码是:new ServletContextInitializer[]{this.getSelfInitializer()}
可以看到,传入了一个ServletContextInitializer类型的数组,其中只有一个ServletContextInitializer类型的元素,通过getSelfInitializer()方法返回的。
private ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
很明显,此处是一个lambda表达式,通过方法引用替代的lambda表达式。下面的selfInitialize()方法就是方法引用,表示一个lambda。
private void selfInitialize(ServletContext servletContext) throws ServletException {
this.prepareWebApplicationContext(servletContext);
this.registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(this.getBeanFactory(), servletContext);
Iterator var2 = this.getServletContextInitializerBeans().iterator();
while(var2.hasNext()) {
ServletContextInitializer beans = (ServletContextInitializer)var2.next();
beans.onStartup(servletContext);
}
}
不对啊,上面不是说initializers有3个吗?怎么这里只有一个呢?
继续debug:
进入到prepareContext()方法中: 到这里我们可以看到,prepareContext()方法中做了一个合并操作,合并后就成了3个对象了。
然后一直往下走,一直到加载TomcatStarter的地方,都是这3个对象:
进入到TomcatStarter里面:
所以,分析来分析去,发现核心实际上就是在createWebServer()方法中传入的ServletContextInitializer对象:
this.webServer = factory.getWebServer(new ServletContextInitializer[]{this.getSelfInitializer()});
注册servlet、filter、listener等的逻辑核心代码如下:
private ServletContextInitializer getSelfInitializer() {
// 返回一个ServletContextInitializer的匿名内部类实例
return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
this.prepareWebApplicationContext(servletContext);
this.registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(this.getBeanFactory(), servletContext);
// 核心在这
// getServletContextInitializerBeans返回的是spring容器中所有的ServletContextInitializer的实例
// 最终会调用每个ServletContextInitializer的onStartup方法对Servlet容器进行初始化
Iterator var2 = this.getServletContextInitializerBeans().iterator();
while(var2.hasNext()) {
ServletContextInitializer beans = (ServletContextInitializer)var2.next();
beans.onStartup(servletContext);
}
}
也就是说,getSelfInitializer()方法返回的这个ServletContextInitializer对象实际上就是一个lambda,它的主要逻辑是从spring容器中找寻所有的ServletContextInitializer实例对象,从而遍历调用其中的onStartup方法。
所以我们重点应该关注上面的 this.getServletContextInitializerBeans() 方法返回的是啥:
protected Collection<ServletContextInitializer> getServletContextInitializerBeans() {
// 可以看出ServletContextInitializerBeans本质是一个集合
// 并且该集合的元素是ServletContextInitializer
// getBeanFactory获取spring容器,这里就可以猜到
// 集合中的元素肯定是从Spring容器获取的
return new ServletContextInitializerBeans(this.getBeanFactory(), new Class[0]);
}
我们继续看一下ServletContextInitializerBeans的构造器,验证下我们的猜想:
org.springframework.boot.web.servlet.ServletContextInitializerBeans
@SafeVarargs
public ServletContextInitializerBeans(ListableBeanFactory beanFactory, Class... initializerTypes) {
// initializerTypes 默认是ServletContextInitializer.class
this.initializerTypes = initializerTypes.length != 0 ? Arrays.asList(initializerTypes) : Collections.singletonList(ServletContextInitializer.class);
// 从spring容器中获取所有类型是ServletContextInitializer的实例
this.addServletContextInitializerBeans(beanFactory);
this.addAdaptableBeans(beanFactory);
List<ServletContextInitializer> sortedInitializers = (List)this.initializers.values().stream().flatMap((value) -> {
return value.stream().sorted(AnnotationAwareOrderComparator.INSTANCE);
}).collect(Collectors.toList());
this.sortedList = Collections.unmodifiableList(sortedInitializers);
this.logMappings(this.initializers);
}
private void addServletContextInitializerBeans(ListableBeanFactory beanFactory) {
// initializerTypes 默认只有ServletContextInitializer.class
Iterator var2 = this.initializerTypes.iterator();
while(var2.hasNext()) {
// getOrderedBeansOfType 方法便是去容器中寻找注册过得 ServletContextInitializer
// 其中 RegistrationBean 继承自 ServletContextInitializer 还实现了 Ordered 接口,在这儿用于排序
Class<? extends ServletContextInitializer> initializerType = (Class)var2.next();
Iterator var4 = this.getOrderedBeansOfType(beanFactory, initializerType).iterator();
while(var4.hasNext()) {
Entry<String, ? extends ServletContextInitializer> initializerBean = (Entry)var4.next();
// 找到以后遍历每个实例,下面这个方法会根据每个实例具体Class类型进行分类收集
this.addServletContextInitializerBean((String)initializerBean.getKey(), (ServletContextInitializer)initializerBean.getValue(), beanFactory);
}
}
}
private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer, ListableBeanFactory beanFactory) {
// 根据不同的类型进行分类整理
if (initializer instanceof ServletRegistrationBean) {
// ServletRegistrationBean专门注册Servlet的
Servlet source = ((ServletRegistrationBean)initializer).getServlet();
this.addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source);
} else if (initializer instanceof FilterRegistrationBean) {
// FilterRegistrationBean专门注册Servlet的Filter的
Filter source = ((FilterRegistrationBean)initializer).getFilter();
this.addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
} else if (initializer instanceof DelegatingFilterProxyRegistrationBean) {
String source = ((DelegatingFilterProxyRegistrationBean)initializer).getTargetBeanName();
this.addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source);
} else if (initializer instanceof ServletListenerRegistrationBean) {
// ServletListenerRegistrationBean专门注册Servlet的Listener的
EventListener source = ((ServletListenerRegistrationBean)initializer).getListener();
this.addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source);
} else {
this.addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory, initializer);
}
}
private void addServletContextInitializerBean(Class<?> type, String beanName, ServletContextInitializer initializer, ListableBeanFactory beanFactory, Object source) {
// 最终存放到MultiValueMap,key是类型,value是一个集合
this.initializers.add(type, initializer);
if (source != null) {
this.seen.add(source);
}
if (logger.isTraceEnabled()) {
String resourceDescription = this.getResourceDescription(beanName, beanFactory);
int order = this.getOrder(initializer);
logger.trace("Added existing " + type.getSimpleName() + " initializer bean '" + beanName + "'; order=" + order + ", resource=" + resourceDescription);
}
}
我们来看下ServletContextInitializer到底是什么?
让我们再回到selfInitialize()方法,我们已经知道,这里遍历的实际上就是spring容器中所有类型为ServletContextInitializer的bean的onStartup()方法。
private void selfInitialize(ServletContext servletContext) throws ServletException {
this.prepareWebApplicationContext(servletContext);
this.registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(this.getBeanFactory(), servletContext);
// 核心在这
// getServletContextInitializerBeans返回的是spring容器中所有的ServletContextInitializer的实例
// 最终会调用每个ServletContextInitializer的onStartup方法对Servlet容器进行初始化
Iterator var2 = this.getServletContextInitializerBeans().iterator();
while(var2.hasNext()) {
ServletContextInitializer beans = (ServletContextInitializer)var2.next();
beans.onStartup(servletContext);
}
}
我们只看一个最常用的ServletContextInitializer的实现,ServletRegistrationBean的onStartup方法:
org.springframework.boot.web.servlet.RegistrationBean
package org.springframework.boot.web.servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.Ordered;
import org.springframework.util.StringUtils;
public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
private static final Log logger = LogFactory.getLog(RegistrationBean.class);
private int order = 2147483647;
private boolean enabled = true;
public RegistrationBean() {
}
public final void onStartup(ServletContext servletContext) throws ServletException {
String description = this.getDescription();
if (!this.isEnabled()) {
logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
} else {
// 调用 register ,具体实现在子类DynamicRegistrationBean中
this.register(description, servletContext);
}
}
protected abstract String getDescription();
protected abstract void register(String description, ServletContext servletContext);
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isEnabled() {
return this.enabled;
}
public void setOrder(int order) {
this.order = order;
}
public int getOrder() {
return this.order;
}
}
org.springframework.boot.web.servlet.DynamicRegistrationBean:
protected final void register(String description, ServletContext servletContext) {
// addRegistration 具体实现在子类
D registration = this.addRegistration(description, servletContext);
if (registration == null) {
logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
} else {
this.configure(registration);
}
}
protected abstract D addRegistration(String description, ServletContext servletContext);
org.springframework.boot.web.servlet.ServletRegistrationBean:
// springboot真正注册servlet的地方在这里,ServletRegistrationBean中负责注册servlet。可以看到,最终还是使用了servlet3.0提供的api进行注册。
protected Dynamic addRegistration(String description, ServletContext servletContext) {
String name = this.getServletName();
return servletContext.addServlet(name, this.servlet);
}
springboot内置tomcat加载servlet等大总结:
getWebServer()方法中传入了一个匿名的ServletContextInitializer对象。
这个匿名的 ServletContextInitializer 的 onStartup 方法会去容器中搜索到了所有的 RegisterBean 并按照顺序加载到 ServletContext 中。
这个匿名的 ServletContextInitializer 最终传递给 TomcatStarter,由 TomcatStarter 的 onStartup 方法去触发 ServletContextInitializer 的 onStartup 方法,最终完成servlet、filter、listener的装配!
扩展:第三种注册servlet、filter、listener的方式!
通过分析,我们知道了springboot使用内置tomcat,是没有完全遵守servlet3.0规范的,也就是内嵌tomcat没有使用SPI的方式加载ServletContainerInitializer,而是走了另外一套TomcatStater的方式来注册Servlet的。
虽然servlet3.0规范的ServletContainerInitializer不能被内嵌tomcat加载,但是spring搞出了另外一个东西ServletContextInitializer,ServletContextInitializer却能被springboot的内嵌容器加载到。
因此,我们可以通过向spring容器中直接注册一个ServletContextInitializer的实例对象,从而也能够实现加载servlet的效果。
实际开发中,还是以上面总结的一,二两种方法来注册为主,这里只是提供一个可能性,来让我们理解 springboot 的加载流程。
@Configuration
public class CustomServletContextInitializer implements ServletContextInitializer {
private final static String JAR_HELLO_URL = "/hello";
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("创建 helloWorldServlet...");
ServletRegistration.Dynamic servlet = servletContext.addServlet(
HelloWorldServlet.class.getSimpleName(),
HelloWorldServlet.class);
servlet.addMapping(JAR_HELLO_URL);
System.out.println("创建 helloWorldFilter...");
FilterRegistration.Dynamic filter = servletContext.addFilter(
HelloWorldFilter.class.getSimpleName(), HelloWorldFilter.class);
EnumSet<DispatcherType> dispatcherTypes = EnumSet.allOf(DispatcherType.class);
dispatcherTypes.add(DispatcherType.REQUEST);
dispatcherTypes.add(DispatcherType.FORWARD);
filter.addMappingForUrlPatterns(dispatcherTypes, true, JAR_HELLO_URL);
}
}
6. 总结
存在 web.xml 配置的 java web 项目,servlet3.0 的 java web 项目,springboot 内嵌容器的 java web 项目加载 servlet,filter,listener 的流程都是有所差异的,理解清楚这其中的原来,其实并不容易,至少得搞懂 servlet3.0 的规范,springboot 内嵌容器的加载流程等等前置逻辑。
在此之前误以为: TomcatStarter 既然继承了 ServletContainerInitializer,应该也是符合 servlet3.0 规范的,但实际上并没有被 SPI 加载。
文档信息
- 本文作者:Marshall