1.写在前面
上一篇博客中介绍了SpringBoot的事件管理机制,当时留下了一个坑,就是在事件管理器中要配置线程池,不然在执行监听器中逻辑代码的时候如果报错了,这个时候会影响事件管理器主线程中的代码,这样对我们来说是不允许的,这儿最好用异步执行,所以我们这儿要配置一个线程池,但是怎么配置,上篇博客没有讲,这篇博客会讲,而今天博客最主要的内容还是SpringBoot中Web容器。
2.SpringBoot中配置事件监听器的线程池
要知道怎么在事件管理器中使用线程池的方法,首先我们需要看怎么获取线程池的,然后才知道怎么配置线程池,具体的代码如下:
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
//获取线程池
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
@Nullable
protected Executor getTaskExecutor() {
return this.taskExecutor;
}
从上面可以知道是通过一个成员变量taskExecutor
来获取的,这个是SimpleApplicationEventMulticaster
的成员变量,那么我们怎么去改变这个成员变量的值呢?这个时候我们需要看下这个类是怎么注入到初始化的,看看有没有什么方法?让这个类在初始化的时候,我们来改变一下这个成员变量?具体的代码如下:
protected void initApplicationEventMulticaster() {
ConfigurableListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) {
this.applicationEventMulticaster =
beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class);
if (logger.isTraceEnabled()) {
logger.trace("Using ApplicationEventMulticaster [" + this.applicationEventMulticaster + "]");
}
}
else {
this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory);
beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster);
if (logger.isTraceEnabled()) {
logger.trace("No '" + APPLICATION_EVENT_MULTICASTER_BEAN_NAME + "' bean, using " +
"[" + this.applicationEventMulticaster.getClass().getSimpleName() + "]");
}
}
}
可以看到上面的代码只要容器中有applicationEventMulticaster
类名的Bean,Spring就不会创建SimpleApplicationEventMulticaster
那么我们这儿就可以注入一个名称为applicationEventMulticaster
的SimpleApplicationEventMulticaster
的Bean,然后修改其中的属性,就可以配置线程池了。具体代码如下:
package com.example.springbootevent.config;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.SimpleApplicationEventMulticaster;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
@Configuration
public class AppConfig {
@Bean("applicationEventMulticaster")
public SimpleApplicationEventMulticaster applicationEventMulticaster(BeanFactory beanFactory,
ThreadPoolTaskExecutor threadPoolExecutor) {
SimpleApplicationEventMulticaster simpleApplicationEventMulticaster = new
SimpleApplicationEventMulticaster(beanFactory);
simpleApplicationEventMulticaster.setTaskExecutor(threadPoolExecutor);
return simpleApplicationEventMulticaster;
}
@Bean
public ThreadPoolTaskExecutor threadPoolExecutor() {
ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
threadPoolTaskExecutor.setMaxPoolSize(15);
threadPoolTaskExecutor.setCorePoolSize(10);
threadPoolTaskExecutor.setQueueCapacity(30);
threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
threadPoolTaskExecutor.initialize();
return threadPoolTaskExecutor;
}
}
这样我们就注入了线程池了,我们来验证一下,运行的结果如下:
通过debug的方式,我们已经发现我们刚才设置的线程池,已经加入到对应的事件管理器中去了。算是我把上篇博客的一个坑给填了吧。继续我们今天的内容。
3.SpringBoot中Web容器的参数如何配置
主要有三种方式。
第一种通过配置文件来配置对应的参数,具体的内容如下:
server.port=8081
主要是通过修改application.properties
或者application.yml
配置文件来修改对应Web容器中参数,运行的结果如下:
可以发现我们的端口已经映射到8081上去了。
第二种就是通过修改TomcatServletWebServerFactory
Bean的属性来修改对应的参数,具体的如下:
package com.example.springbootweb.config;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.setPort(8082);
return tomcatServletWebServerFactory;
}
}
运行结果如下:
可以发现我们的端口已经映射到8082上去了。
第三种就是通过修改WebServerFactoryCustomizer
Bean的属性来修改对应的参数,具体的如下:
package com.example.springbootweb.config;
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
public WebServerFactoryCustomizer<TomcatServletWebServerFactory> webServerFactoryCustomizer() {
return factory -> factory.setPort(8083);
}
}
运行结果如下:
可以发现我们的端口已经映射到8083上去了。
那么现在大家可能有一个疑问了,就是这三种方式那个优先级最高呢?我们来测试一下看看,先看运行结果:
可以发现是我们的8083映射上了,所以第三种方式,WebServerFactoryCustomizer
的方式是最高的,然后我们再来看次之的,先注释掉WebServerFactoryCustomizer
部分,再来看运行结果,如下:
可以发现是我们的8081映射上了,所以是第一种方式,配置文件的方式的优先级次之,所以我们得出以下的结果:
优先级最高的是WebServerFactoryCustomizer
次之配置文件,最末TomcatServletWebServerFactory
4.SpringBoot如何使用别的Web容器
我们这儿就使用Jetty服务器,那么如何使用呢?先修改我们的pom文件,具体的如下:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!--剔除Tomcat依赖-->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--引入jetty依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
运行结果如下:
可以看到我们这儿已经使用了Jetty服务器了。那我们这儿为什么要剔除Tomcat的依赖呢?这个时候需要简单看下源码吧!具体的如下:
可以看到这儿是先Import Tomcat的,所以要剔除Tomcat的依赖,才能使用Jetty服务器。
5.SpringBoot启动Web容器的原理
这里SpringBoot的启动方式主要有两种,一种是基于jar的内嵌的web服务器,一种是基于war包的外部服务器。两种方式这儿,我都会讲一下,其实区别很小。首先我们先看基于jar的内嵌的web服务器。
5.1基于jar启动方式的web的服务器启动原理
我们就直接看对应的调用链,大家可以根据对应的调用链,来看对应的代码,具体的如下:
org.springframework.boot.SpringApplication#run(java.lang.Class<?>, java.lang.String...)
--->org.springframework.boot.SpringApplication#run(java.lang.Class<?>[], java.lang.String[])
--->org.springframework.boot.SpringApplication#run(java.lang.String...)
--->org.springframework.boot.SpringApplication#refreshContext
--->org.springframework.boot.SpringApplication#refresh(org.springframework.context.ApplicationContext)
--->org.springframework.boot.SpringApplication#refresh
(org.springframework.context.ConfigurableApplicationContext)
--->org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#refresh
--->org.springframework.context.support.AbstractApplicationContext#refresh
--->org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh
--->org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext#createWebServer
经过一系列的方法的调用,最终调用的我们的核心的代码,具体的如下:
private void createWebServer() {
WebServer webServer = this.webServer;
//获取servlet上下文对象
ServletContext servletContext = getServletContext();
//由于我们这儿是基于jar启动的,所以这儿这儿两个的值都是空
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
//获取servlet工厂,这儿的代码下面会带着大家看一遍
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
//创建web容器
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
//注册两个生命周期的回调函数
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
//这儿是基于war包启动的,这儿下一节再讲
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
经过上面的简单的分析,我们先看看创建工厂的方法getWebServerFactory()
,具体的代码如下:
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
这儿可以看见这儿是从容器中获取ServletWebServerFactory
的实现类,并且这儿实现类只允许有一个,如果出现多个,springboot这儿直接报错,那么先看看这儿类ServletWebServerFactory
有几个实现类吧。具体的如下:
可以看到,就是springBoot自己适配的几个Web服务器,所以这儿配置的话,就只能配置一个服务器,不能配置多,配置多,springBoot会直接报错。我们再来看factory.getWebServer(getSelfInitializer());
方法,由于这儿是调用接口的getWebServer
方法,所以每个工厂都有着对应的实现,我们今天还是只看看Tomcat怎么做的吧。于是我们打开TomcatServletWebServerFactory
类中getWebServer
方法,具体的代码如下:
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
Tomcat tomcat = new Tomcat();
File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
tomcat.setBaseDir(baseDir.getAbsolutePath());
Connector connector = new Connector(this.protocol);
connector.setThrowOnFailure(true);
tomcat.getService().addConnector(connector);
customizeConnector(connector);
tomcat.setConnector(connector);
tomcat.getHost().setAutoDeploy(false);
configureEngine(tomcat.getEngine());
for (Connector additionalConnector : this.additionalTomcatConnectors) {
tomcat.getService().addConnector(additionalConnector);
}
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
可以看到上面的代码就是我们Tomcat的启动的代码,至此,基于jar的启动的springboot的启动web容器的原理,我这儿已经讲清楚了。这儿有个需要注意点,我们先看下官网,如下:
上面的意思大概就是springBoot在使用内嵌的web容器,servlet3.0的spi特性就会失效,这儿时候需要实现ServletContextInitializer
并且实现这个接口的对应的方法,这儿才会在Tomcat启动的时候调用,具体的测试如下:
package com.example.springbootweb.config;
import org.springframework.stereotype.Component;
import org.springframework.web.WebApplicationInitializer;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@Component
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("MyWebApplicationInitializer.onStartup");
}
}
package com.example.springbootweb.config;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.stereotype.Component;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
@Component
public class MyServletContextInitializer implements ServletContextInitializer {
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
System.out.println("MyServletContextInitializer.onStartup");
}
}
我们写出了两种方式,看看在springboot启动的时候,会调用哪个方法,具体的测试如下:
可以发现实现ServletContextInitializer
接口的方法调用了对应的启动的方法,也验证了官网所说的那句话。那么原理又是什么呢?
这个时候我们还需要看下原来创建容器的方法createWebServer()
方法,具体的代码如下:
private void createWebServer() {
WebServer webServer = this.webServer;
//获取servlet上下文对象
ServletContext servletContext = getServletContext();
//由于我们这儿是基于jar启动的,所以这儿这儿两个的值都是空
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
//获取servlet工厂,这儿的代码下面会带着大家看一遍
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
//创建web容器
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
//注册两个生命周期的回调函数
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
//这儿是基于war包启动的,这儿下一节再讲
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
在调用getWebServer(getSelfInitializer());
方法,往其中传入一个参数,这个参数是getSelfInitializer()
的返回值,具体的代码如下:
private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
return this::selfInitialize;
}
private void selfInitialize(ServletContext servletContext) throws ServletException {
prepareWebApplicationContext(servletContext);
registerApplicationScope(servletContext);
WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
这儿利用了java8的方法引用,就是在调用onStartup()
方法的时候,会用下面的方法来替代。同时这个类还传给了Tomcat,后面Tomcat在启动的时候就会调用上面的selfInitialize
方法。而这儿的逻辑就是找到我们项目中所有实现了ServletContextInitializer
接口的Bean。我为什么这么说呢?可以看看下图:
可以发现调用getServletContextInitializerBeans()
方法,这儿获取到了我们自己实现的ServletContextInitializer
接口的类MyServletContextInitializer
类,然后调用其中的onStartup()
方法,这儿的原理就讲清楚了,这个时候我们需要看基于war包的启动方式的web的容器启动原理。
5.2基于war包的启动方式的web的容器启动原理
我们先看打成war包的时候,要加的一条关键的代码,具体的如下:
package com.example.springbootweb;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
@SpringBootApplication
public class SpringBootWebApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
return super.configure(builder).sources(SpringBootWebApplication.class);
}
public static void main(String[] args) {
SpringApplication.run(SpringBootWebApplication.class, args);
}
}
我们可以发现我们是继承了一个SpringBootServletInitializer
这个类,而这个类实现了WebApplicationInitializer
,这个类大家都很熟悉吧。Tomcat在启动的时候,都会调用这个类的实现类中的onStartup()
方法,所以这儿Tomcat启动的时候,就会调用SpringBootServletInitializer
类中的onStartup()
方法,具体的代码如下:
public void onStartup(ServletContext servletContext) throws ServletException {
// Logger initialization is deferred in case an ordered
// LogServletContextInitializer is being used
this.logger = LogFactory.getLog(getClass());
WebApplicationContext rootApplicationContext = createRootApplicationContext(servletContext);
if (rootApplicationContext != null) {
servletContext.addListener(new SpringBootContextLoaderListener(rootApplicationContext, servletContext));
}
else {
this.logger.debug("No ContextLoaderListener registered, as createRootApplicationContext() did not "
+ "return an application context");
}
}
上面的代码就是在调用createRootApplicationContext(servletContext)
方法中启动Web容器的,具体的代码如下:
protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
SpringApplicationBuilder builder = createSpringApplicationBuilder();
builder.main(getClass());
ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
if (parent != null) {
this.logger.info("Root context already created (using as parent).");
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
builder.initializers(new ParentContextApplicationContextInitializer(parent));
}
builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
builder.contextFactory((webApplicationType) -> new AnnotationConfigServletWebServerApplicationContext());
builder = configure(builder);
builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
SpringApplication application = builder.build();
if (application.getAllSources().isEmpty()
&& MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
application.addPrimarySources(Collections.singleton(getClass()));
}
Assert.state(!application.getAllSources().isEmpty(),
"No SpringApplication sources have been defined. Either override the "
+ "configure method or add an @Configuration annotation");
// Ensure error pages are registered
if (this.registerErrorPageFilter) {
application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
}
application.setRegisterShutdownHook(false);
return run(application);
}
前面的代码都不用在乎,看到最后的代码run(application);
点开的代码内容如下:
protected WebApplicationContext run(SpringApplication application) {
return (WebApplicationContext) application.run();
}
再点开,具体的代码如下:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//比较重要的代码
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
你会发现一个比较重要的方法调用refreshContext(context);
这个方法是不是很熟悉?如果你有认真阅读前面的内容,你会发现后面的调用逻辑和基于jar方法调用的逻辑是一样的了。于是最终会走到下面的方法,具体的如下:
private void createWebServer() {
WebServer webServer = this.webServer;
//获取servlet上下文对象
ServletContext servletContext = getServletContext();
//由于我们这儿是基于jar启动的,所以这儿这儿两个的值都是空
if (webServer == null && servletContext == null) {
StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
//获取servlet工厂,这儿的代码下面会带着大家看一遍
ServletWebServerFactory factory = getWebServerFactory();
createWebServer.tag("factory", factory.getClass().toString());
//创建web容器
this.webServer = factory.getWebServer(getSelfInitializer());
createWebServer.end();
//注册两个生命周期的回调函数
getBeanFactory().registerSingleton("webServerGracefulShutdown",
new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
new WebServerStartStopLifecycle(this, this.webServer));
}
//这儿是基于war包启动的,这儿下一节再讲
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
这儿会走下面的else if 的分支,然后调用实现的ServletContextInitializer
接口中的onStartup
方法,也就是说这儿实现其他的两个接口类中onStartup方法会调用,实现这个ServletContextInitializer
接口中的onStartup
方法也是会调用的,只不过是最后调用的。最终我们得到:基于jar包启动方式的springBoot,内嵌式的Tomcat,在启动的时候,会调用org.springframework.boot.web.servlet.ServletContextInitializer
接口的实现类中的onStartup
方法。而基于war包启动方式的springBoot,外部的Tomcat,在启动的Tomcat的时候,会调用实现javax.servlet.ServletContainerInitializer
接口和实现org.springframework.web.WebApplicationInitializer
接口以及实现org.springframework.boot.web.servlet.ServletContextInitializer
接口的实现类中的onStartup
方法。
6.写在最后
这篇博客,我大概的介绍了SpringBoot中如何使用其他的web容器,同时也介绍了SpringBoot如何启动web容器。