springboot内置Tomcat依赖图

springboot filter ServletResponse 定义 error_启动


如上图所示Tomcat在spring-boot-starter-web中自动引入

如何修改内置Tomcat属性

如果是正常的Tomcat容器可以通过在conf/web.xml、conf/server.xml文件来修改配置,但内置Tomcat并没有这两个文件,那么如何修改呢?
通过看官方文档知道可以通过server.port属性更改Tomcat端口,由我上篇文章提到的SpringBoot加载规则可以知道必然存在一个ServerProperties来设置默认的Tomcat相关属性,咱们来看下这个类

@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {

	/**
	 * Server HTTP port.
	 */
	private Integer port;

	/**
	 * Network address to which the server should bind.
	 */
	private InetAddress address;
	
	/**
	中间省略
	*/
    
}

看下哪里调用了getPort()

public class ServletWebServerFactoryCustomizer implements
		WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {

	private final ServerProperties serverProperties;

	public ServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
		this.serverProperties = serverProperties;
	}

	@Override
	public int getOrder() {
		return 0;
	}

	@Override
	public void customize(ConfigurableServletWebServerFactory factory) {
		PropertyMapper map = PropertyMapper.get().alwaysApplyingWhenNonNull();
		map.from(this.serverProperties::getPort).to(factory::setPort);
		map.from(this.serverProperties::getAddress).to(factory::setAddress);
		map.from(this.serverProperties.getServlet()::getContextPath)
				.to(factory::setContextPath);
		map.from(this.serverProperties.getServlet()::getApplicationDisplayName)
				.to(factory::setDisplayName);
		map.from(this.serverProperties.getServlet()::getSession).to(factory::setSession);
		map.from(this.serverProperties::getSsl).to(factory::setSsl);
		map.from(this.serverProperties.getServlet()::getJsp).to(factory::setJsp);
		map.from(this.serverProperties::getCompression).to(factory::setCompression);
		map.from(this.serverProperties::getHttp2).to(factory::setHttp2);
		map.from(this.serverProperties::getServerHeader).to(factory::setServerHeader);
		map.from(this.serverProperties.getServlet()::getContextParameters)
				.to(factory::setInitParameters);
	}

}

可以看到在这里把ServerProperties中的属性都设置到到ConfigurableServletWebServerFactory,所以从这里可以知道在SpringBoot中实际对web容器做定制实际上是修改ConfigurableServletWebServerFactory类的相关属性.再来看下这个customize方法的WebServerFactoryCustomizerBeanPostProcessor类

private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
		LambdaSafe
				.callbacks(WebServerFactoryCustomizer.class, getCustomizers(),
						webServerFactory)
				.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
				.invoke((customizer) -> customizer.customize(webServerFactory));
	}

通过上面的代码可以看到这里整个过程实际上是获取容器中所有的customizer,往customizer中传入WebServerFactory参数,然后调用customize()方法把ServerProperteis中的属性设置到WebServerFactory中,那我们是不是可以实现这种方式来达到配置服务器属性的目的呢?实际上是可以的,代码如下

@Bean
    public WebServerFactoryCustomizer configWebServer(){
        WebServerFactoryCustomizer webServerFactoryCustomizer = new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() {
            @Override
            public void customize(ConfigurableServletWebServerFactory factory) {
                factory.setPort(9000);
            }
        };
        return webServerFactoryCustomizer;
    }

再看SpringBoot官方也是通过这种方式来更改web服务器配置的;https://docs.spring.io/spring-boot/docs/2.0.0.RELEASE/reference/htmlsingle/#boot-features-embedded-container-application-context

总结一下就是修改web服务器配置有两种方式
1、在application.properties中通过server.XXX配置

server.port=8082
server.servlet.context-path=/test

2、通过实现WebServerFactoryCustomizer接口并用其提供的接口来通过ConfigurableServletWebServerFactory的方法来修改

@Bean
    public WebServerFactoryCustomizer configWebServer(){
        WebServerFactoryCustomizer webServerFactoryCustomizer = new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() {
            @Override
            public void customize(ConfigurableServletWebServerFactory factory) {
                factory.setPort(9000);
                factory.setContextPath("/test");
            }
        };
        return webServerFactoryCustomizer;
    }

Servlet容器自动配置原理

我们首先看下ServletWebServerFactoryAutoConfiguration这个类

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(ServletRequest.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(ServerProperties.class)
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
//中间省略
}

这里引入的几个类比较关键,首先咱们看下BeanPostProcessorsRegistrar

public static class BeanPostProcessorsRegistrar
			implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
			
			    		@Override
		public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
				BeanDefinitionRegistry registry) {
			if (this.beanFactory == null) {
				return;
			}
			registerSyntheticBeanIfMissing(registry,
					"webServerFactoryCustomizerBeanPostProcessor",
					WebServerFactoryCustomizerBeanPostProcessor.class);
			registerSyntheticBeanIfMissing(registry,
					"errorPageRegistrarBeanPostProcessor",
					ErrorPageRegistrarBeanPostProcessor.class);
		}
			}

这个类实现了spring中的ImportBeanDefinitionRegistrar、BeanFactoryAware接口,并在上面的方法中注册了WebServerFactoryCustomizerBeanPostProcessor;而这个类就很眼熟了,我们上面刚刚分析过,这里我把代码再贴一下

public class WebServerFactoryCustomizerBeanPostProcessor
		implements BeanPostProcessor, BeanFactoryAware {
	
	//在初始化之前执行	
	@Override
	public Object postProcessBeforeInitialization(Object bean, String beanName)
			throws BeansException {
		//如果当前类是一个WebSerFactory
		if (bean instanceof WebServerFactory) {
			postProcessBeforeInitialization((WebServerFactory) bean);
		}
		return bean;
	}
	
	//调用WebServerFactoryCustomizer的customize对当前的WebServerFactory进行设置
	private void postProcessBeforeInitialization(WebServerFactory webServerFactory) {
	LambdaSafe
			.callbacks(WebServerFactoryCustomizer.class, getCustomizers(),
						webServerFactory)
			.withLogger(WebServerFactoryCustomizerBeanPostProcessor.class)
			.invoke((customizer) -> customizer.customize(webServerFactory));
	}
}

这个过程就很清晰了,++当spring启动时ServletWebServerFactoryAutoConfiguration会添加WebServerFactoryCustomizerBeanPostProcessor类,WebServerFactoryCustomizerBeanPostProcessor会在bean初始化之前判是否存在WebServerFactory,如果存在的话使用WebServerFactoryCustomizer来对WebServerFactory进行定制,定制的属性是从ServerProperties中读取的++

那么现在还剩下一个问题,WebServerFactory是在哪里定义的?
再看ServletWebServerFactoryAutoConfiguration引入的另外一个类ServletWebServerFactoryConfiguration

class ServletWebServerFactoryConfiguration {

	@Configuration
	//当系统中有tomcat的关键类,即有tomcat容器时,则注入TomcatServletWebServerFactory
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedTomcat {

		@Bean
		public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
			return new TomcatServletWebServerFactory();
		}

	}

	
	@Configuration
	@ConditionalOnClass({ Servlet.class, Server.class, Loader.class,
			WebAppContext.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedJetty {

		@Bean
		public JettyServletWebServerFactory JettyServletWebServerFactory() {
			return new JettyServletWebServerFactory();
		}

	}

	/**
	 * Nested configuration if Undertow is being used.
	 */
	@Configuration
	@ConditionalOnClass({ Servlet.class, Undertow.class, SslClientAuthMode.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	public static class EmbeddedUndertow {

		@Bean
		public UndertowServletWebServerFactory undertowServletWebServerFactory() {
			return new UndertowServletWebServerFactory();
		}

	}

}

这里可以看到当系统中有Tomcat、Jetty、Undertow容器时,会分别往系统中添加对应的WebServerFactory

那这里我们又可以发现一个事情,也就是说我们想启用jetty或者undertow的话直接引入相关的组件就行了(如果在一个项目中同时引用多个servlet容器默认使用tomcat启动的,感兴趣的可以去了解下为什么)

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
			<exclusions>
		        <artifactId>spring-boot-starter-tomcat</artifactId>
			    <groupId>org.springframework.boot</groupId>
			</exclusions>
		</dependency>
		<dependency>
			<artifactId>spring-boot-starter-jetty</artifactId>
			<groupId>org.springframework.boot</groupId>
		</dependency>
		//或者
		<dependency>
			<artifactId>spring-boot-starter-undertow</artifactId>
			<groupId>org.springframework.boot</groupId>
		</dependency>

我们再来总结一下SpringBoot管理servlet容器的整个过程

  1. SpringBoot根据导入的依赖情况给容器添加响应的WebServerFactory
  2. 当有组件要创建对象时会惊动WebServerFactoryCustomizerBeanPostProcessor,如果是WebServerFactory则会进行后续处理
  3. 后续处理,从容器中获取所有的WebServerFactoryCustomizer对WebServerFactory进行定制
  4. 定制属性从ServerProperties中获取

Servlet容器启动原理

现在我们来分析一下内嵌的servlet容器时在什么时候启动的
这里大家可以再对应的WebServerFactory的getWebServer()方法上打个断点,这样通过栈帧能清楚的看到webServer启动的逻辑,我这里以TomcatServletWebServerFactory为例说一下几个比较关键的点

public class ServletWebServerApplicationContext extends GenericWebApplicationContext
		implements ConfigurableWebServerApplicationContext {
	@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}
	
	private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			ServletWebServerFactory factory = getWebServerFactory();
			this.webServer = factory.getWebServer(getSelfInitializer());
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context",
						ex);
			}
		}
		initPropertySources();
	}
}

可以看到这个类重写了spring的ConfigurableApplicationContext类的onRefresh()方法,在onRefresh()方法中调用的createWebServer()方法在createWebServer()中通过调用ServletWebServerFactory的getWebServer()方法来获取webServer.咱们来看下TomcatServletWebServerFactory的getWebServer()做了那些事

@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
	   //new一个tomcat组件
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null ? this.baseDirectory
				: createTempDir("tomcat"));
		tomcat.setBaseDir(baseDir.getAbsolutePath());
		//设置必要的连接器组件
		Connector connector = new Connector(this.protocol);
		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);
	}

在getTomcatWebServer()方法中有个很重要的地方

springboot filter ServletResponse 定义 error_springboot_02


这样tomcat启动的流程咱们也清楚了

  1. ServletWebServerApplicationContext重写容器的onRefresh(),并在onRefresh()中调用createWebServer()
  2. createWebServer()获取容器中的ServletWebServerFactory,并调用getWebServer()
  3. getWebServer()对servlet容器进行初始化并启动

SpringBoot外部servelt容器启动原理

上面我们讲了SpringBoot通过内嵌servlet启动的相关的东西,下面我们分析下SpringBoot在外部servlet容器中是如何启动的
大家都知道要把SpringBoot放到外置servlet容器中运行需要以下几个步骤

  1. 设置SpringBoot打包的方式为war包
  2. 添加依赖
<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
  1. 在应用中继承SpringBootServletInitializer类
public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(SpringbootApplicationStarter.class);
    }
}

这里咱们对比以下内嵌、外部两种方式的启动区别:
内嵌servlet容器:jar包,执行SpringBoot主程序的main(),启动ioc容器,启动servlet容器
外部servlet容器:war包,启动servlet容器,启动SpringBoot,启动ioc容器

想明白这种区别的原理要从servlet3.0的规范说起(见servlet规范 8.2.4 共享库 / 运行时可插拔性)

  1. 对于每一个应用,应用启动时,由容器创建一个ServletContainerInitializer实例
  2. ServletContainerInitializer实现必须绑定在jar包的META-INF/services目录中的一个叫做javax.servlet.ServletContainerInitializer 的文件
  3. 在ServletContainerInitializer实现上的HandlesTypes注解用于表示感兴趣的一些类

根据这三点规范我们看下SpringBoot的启动流程

  1. 启动Tomcat
  2. spring-web模块中META-INF/services/javax.servlet.ServletContainerInitializer,这个文件中定义了org.springframework.web.SpringServletContainerInitializer
  3. 这个类实现了ServletContainerInitializer
    这里我贴一下关键代码
  4. springboot filter ServletResponse 定义 error_tomcat_03

  5. 可以看到WebApplicationInitializer是一个接口,那看一下它的实现(如下图),可以看到SpringBootServletInitializer(也就是应用中要继承的类)被创建了实例,并调用了onStartUp()方法
  6. springboot filter ServletResponse 定义 error_springboot_04

  7. 咱们把SpringBootServletInitializer关键代码贴出来
public abstract class SpringBootServletInitializer implements WebApplicationInitializer {
    protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
        //创建SpringBuilder
        SpringApplicationBuilder builder = this.createSpringApplicationBuilder();
        builder.main(this.getClass());
        ApplicationContext parent = this.getExistingRootWebApplicationContext(servletContext);
        if (parent != null) {
            this.logger.info("Root context already created (using as parent).");
            servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, (Object)null);
            builder.initializers(new ApplicationContextInitializer[]{new ParentContextApplicationContextInitializer(parent)});
        }
        
        builder.initializers(new ApplicationContextInitializer[]{new ServletContextApplicationContextInitializer(servletContext)});
        builder.listeners(new ApplicationListener[]{new ServletContextApplicationListener(servletContext)});
        builder.contextClass(AnnotationConfigEmbeddedWebApplicationContext.class);
        //调用configure方法,此方法被子类重写,将SpringBoot主程序传入进来
        builder = this.configure(builder);
        //使用SpringBuilder创建spring应用
        SpringApplication application = builder.build();
        if (application.getSources().isEmpty() && AnnotationUtils.findAnnotation(this.getClass(), Configuration.class) != null) {
            application.getSources().add(this.getClass());
        }

        Assert.state(!application.getSources().isEmpty(), "No SpringApplication sources have been defined. Either override the configure method or add an @Configuration annotation");
        if (this.registerErrorPageFilter) {
            application.getSources().add(ErrorPageFilter.class);
        }
        //启动spring应用,在此方法中通过this.refreshContext(context);来启动ioc容器
        return this.run(application);
    }
}

Servlet三大组件的注册(Servlet、Filter、Listener)

如果是一个普通的web应用三大组件的注册都是在webapp/web-inf/web.xml中添加配置,而嵌入式的web服务器并没有这个web.xml文件,通过查阅官方文档可以知道,可以通过如下三个组件进行注册ServletRegistrationBean,FilterRegistrationBean,ServletListenerRegistrationBean
这里就贴一下servlet怎么注册,其他就不讲了

public class TestServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        this.doPost(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        resp.getWriter().println("this is servlet");
    }
}

然后在SpringBoot中注册此servlet

@Bean
    public ServletRegistrationBean servletRegistrationBean() {
        return new ServletRegistrationBean(new TestServlet(), "/test");// ServletName默认值为首字母小写,即myServlet
    }

那这里又引申出一个问题,这个servlet是怎么注册到DispatcherServlet的呢?
这里可以看下DispatcherServletAutoConfiguration

@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public ServletRegistrationBean<DispatcherServlet> dispatcherServletRegistration(DispatcherServlet dispatcherServlet) {
    ServletRegistrationBean<DispatcherServlet>
    //这里this.serverProperties.getServlet().getServletMapping()的值是"/",既默认dispatcherServlet默认处理当前项目下所有请求
    registration = new ServletRegistrationBean<>(
					dispatcherServlet,this.serverProperties.getServlet().getServletMapping());
	registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
	registration.setLoadOnStartup(	this.webMvcProperties.getServlet().getLoadOnStartup());
	if (this.multipartConfig != null) {
		registration.setMultipartConfig(this.multipartConfig);
	}			
	return registration;
}

看了上面的代码我们可以知道,如果想在SpringBoot中注册多个DispatcherServlet只需要注册ServletRegistrationBean即可,大致代码如下

@Bean
    public ServletRegistrationBean foo() {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();   
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(FooConfig.class);
        dispatcherServlet.setApplicationContext(applicationContext);
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/foo/*");
        servletRegistrationBean.setName("foo");
        return servletRegistrationBean;
    }
    @Bean
    public ServletRegistrationBean bar() {
        DispatcherServlet dispatcherServlet = new DispatcherServlet();
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.register(BarConfig.class);
        dispatcherServlet.setApplicationContext(applicationContext);
        ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(dispatcherServlet, "/bar/*");
        servletRegistrationBean.setName("bar");
        return servletRegistrationBean;
    }