SpringBoot集成Tomcat
SpringBoot集成Tomcat
一、零配置原理
基于Spring新特性javaConfig,Spring JavaConfig是Spring社区的产品,使用java代码配置Spring IoC容器,不需要使用XML配置。
JavaConfig的优点:
- 面向对象的配置。配置被定义为JavaConfig类,因此用户可以充分利用Java中的面向对象功能。一个配置类可以继承另一个,重写它的@Bean方法等。
- 减少或消除XML配置。许多开发人员不希望在XML和Java之间来回切换。JavaConfig为开发人员提供了一种纯Java方法来配置与XML配置概念相似的Spring容器。
- 类型安全和重构友好。JavaConfig提供了一种类型安全的方法来配置Spring容器。由于Java 5.0对泛型的支持,现在可以按类型而不是按名称检索bean,不需要任何强制转换或基于字符串的查找
二、SpringMVC阶段配置Tomcat
通常都是在maven中创建web环境的项目,然后在web.xml中补充如下配置:
- ContextLoaderListener:监听servlet启动、销毁,初始化ioc完成依赖注入
- DispatcherServlet:接收tomcat解析之后的http请求,匹配controller处理业务
在Springmvc阶段中,通常是如下配置:
AnnotationConfigWebApplicationContext ac = new AnnotationConfigWebApplicationContext();
ac.register(Start.class);
//ac.refresh();
DispatcherServlet servlet = new DispatcherServlet(ac);
使用注解来替代xml配置,如果使用xml的话,如下所示:
applicationContext.xml:
component-scan:扫描 注解替换: @ComponentScan
注解替换: @Configuration +@Bean
Springmvc.xml:
component-scan:扫描 只扫描@Controller 注解替换: @ComponentScan
视图解析器,json转换,国际化,编码。。。。 注解替换: @Configuration +@Bean
代码替换:
@Configuration
@EnableWebMvc
public class MyConfig implements WebMvcConfigurer{
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter builder = new FastJsonHttpMessageConverter();
converters.add(builder);
}
}
web.xml
配置字符编码过滤器和DispatherServlet等
三、内嵌Tomcat原理
3.1、SpringBoot中为什么不需要web.xml
Tomcat启动通过SPI机制,找到ServletContainerInitializer对应的实现类,调用其中的onStartup方法。
在onStartup方法中,创建DispatcherServlet对象,然后onStartup方法传入的有ServletContext,说明可以在ServletContext中添加DispatcherServlet对象。
所以web.xml就已经失去了作用,因为之前在web.xml中配置的就是监听器和DispatcherServlet。所以web.xml失去了作用。
3.2、Tomcat的处理之WebApplicationInitializer如何被调用的
将官网上的一段代码拷贝下来:
public class MyWebApplicationInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Load Spring web application configuration
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(AppConfig.class);
// Create and register the DispatcherServlet
DispatcherServlet servlet = new DispatcherServlet(context);
ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet);
registration.setLoadOnStartup(1);
registration.addMapping("/app/*");
}
}
可以看到自定义类实现了WebApplicationInitializer接口并重写其中的onStartup方法。
首先这个方法和Tomcat中的ServletContainerInitializer长得特别像,而且类中都有onStartup方法。这个时候应该想到在omcat中的ServletContainerInitializer的一个实现类上看到过WebApplicationInitializer
@HandlesTypes({WebApplicationInitializer.class})
public class SpringServletContainerInitializer implements ServletContainerInitializer {
public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext) throws ServletException {
// .........
// .........
}
}
可以看到SpringServletContainerInitializer类上有一个注解,注解中的值是WebApplicationInitializer,那么对于Tomcat来说又是如何将WebApplicationInitializer进行处理的呢?这个时候还是得去看下Tomcat中的处理方式:
那么需要分析一下ServletContainerInitializer的onStartup是如何被加载的:
因为SpringServletContainerInitializer遵守了Tomcat的SPI机制,所以Tomcat会来找到META-INF\services\javax.servlet.ServletContainerInitializer这个类进行加载,在对应的时机进行调用。
那么直到这里,我们可以从onStartup方法上看到参数WebApplicationInitializer。那么现在的问题就是Set集合从哪里传递过来的?
那么从Tomcat中的SPI哪个地方来看呢?直接来看ContextConfig监听器部分来:org.apache.catalina.startup.ContextConfig#lifecycleEvent,然后来到org.apache.catalina.startup.ContextConfig#processServletContainerInitializers中进行处理:
/**
* Scan JARs for ServletContainerInitializer implementations.
*/
protected void processServletContainerInitializers() {
List<ServletContainerInitializer> detectedScis;
try {
/**
* 利用Tomcat的SPI机制找到ServletContainerInitializer接口实现类
*/
WebappServiceLoader<ServletContainerInitializer> loader = new WebappServiceLoader<>(context);
detectedScis = loader.load(ServletContainerInitializer.class);
} catch (IOException e) {
// log print
ok = false;
return;
}
// 遍历
for (ServletContainerInitializer sci : detectedScis) {
initializerClassMap.put(sci, new HashSet<Class<?>>());
// 看到HandlesTypes注解
HandlesTypes ht;
try {
// 找到上面有@HandlesTypes注解的值
ht = sci.getClass().getAnnotation(HandlesTypes.class);
} catch (Exception e) {
// log print
continue;
}
if (ht == null) {
continue;
}
// 获取得到type的值:WebApplicationInitializer.class
Class<?>[] types = ht.value();
if (types == null) {
continue;
}
// 开始进行遍历
for (Class<?> type : types) {
if (type.isAnnotation()) {
handlesTypesAnnotations = true;
} else {
handlesTypesNonAnnotations = true;
}
// 熟悉的Set集合
Set<ServletContainerInitializer> scis =
typeInitializerMap.get(type);
if (scis == null) {
scis = new HashSet<>();
// 将value对应的值作为key添加到集合中来
typeInitializerMap.put(type, scis);
}
scis.add(sci);
}
}
}
从这里看,WebApplicationInitializer.class会被放入到typeInitializerMap集合中来,而typeInitializerMap是当前类的成员属性
看看成员属性:org.apache.catalina.startup.ContextConfig#typeInitializerMap
Map<Class<?>, Set<ServletContainerInitializer>> typeInitializerMap = new HashMap<>();
注意:在这个Map中会存在KEY(Class)是WebApplicationInitializer.class的类
然后会将typeInitializerMap中的KEY和VALUE保存到org.apache.catalina.core.StandardContext#initializers中来
Map<ServletContainerInitializer,Set<Class<?>>> initializers =new LinkedHashMap<>();
具体转换的代码如下所示:org.apache.catalina.startup.ContextConfig#webConfig
// Step 11. Apply the ServletContainerInitializer config to the
// context
if (ok) {
// 转换过程
for (Map.Entry<ServletContainerInitializer,Set<Class<?>>> entry :initializerClassMap.entrySet()) {
if (entry.getValue().isEmpty()) {
context.addServletContainerInitializer(entry.getKey(), null);
} else {
context.addServletContainerInitializer(entry.getKey(), entry.getValue());
}
}
}
//================================================
//================================================
public void addServletContainerInitializer(
ServletContainerInitializer sci, Set<Class<?>> classes) {
initializers.put(sci, classes);
}
然后会在org.apache.catalina.core.StandardContext#startInternal中来遍历initializers集合:
// Call ServletContainerInitializers
for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry :initializers.entrySet()) {
try {
// 直接调用对应的方法。然后这里的值就会来进行传入
entry.getKey().onStartup(entry.getValue(),getServletContext());
} catch (ServletException e) {
log.error(sm.getString("standardContext.sciFail"), e);
ok = false;
break;
}
}
至此,会来调用WebApplicationInitializer的onStartup方法并将参数传入了进去。
3.3、如何内嵌Tomcat
依然是从官方代码开始进行入手:
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
代码中无非是从两个地方开始来进行入手:一个是@SpringBootApplication注解,另外一个是SpringApplication中的run方法。
下面首先来从SpringApplication中的run方法来进行入手分析,直接来到org.Springframework.boot.SpringApplication#run(java.lang.String...)中
public ConfigurableApplicationContext run(String... args) {
// ......
try {
// ......
// 根据当前条件来创建容器
// web环境创建AnnotationConfigServletWebServerApplicationContext容器
context = this.createApplicationContext();
// ......
this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 刷新容器--->启动Tomcat
this.refreshContext(context);
// ......
} catch (Throwable var10) {
// ......
}
}
AnnotationConfigServletWebServerApplicationContext是ServletWebServerApplicationContext类的子类,所以refreshContext方法,直接来到org.Springframework.context.support.AbstractApplicationContext#refresh方法中来:
// Initialize other special beans in specific context subclasses.
onRefresh();
//=========================================
protected void onRefresh() throws BeansException {
// For subclasses: do nothing by default.
}
这个方法是交给子类来进行实现的,对于Spring来说,是没有任何实现的。而在上面已经创建了一个容器:AnnotationConfigServletWebServerApplicationContext是ServletWebServerApplicationContext类的子类
所以直接来到org.Springframework.boot.web.servlet.context.ServletWebServerApplicationContext#onRefresh容器中来
protected void onRefresh() {
// 首先访问父类的onRefresh方法
super.onRefresh();
try {
// 创建WebServer
this.createWebServer();
} catch (Throwable var2) {
throw new ApplicationContextException("Unable to start web server", var2);
}
}
那么就看下createWebServer方法
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
// 利用main方法启动肯定是走这一步的
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();
}
这里为什么要判断一下webServer是否为空 || 是否不为空呢?因为Tomcat支持的web容器有:Tomcat、Jetty和Undertow。
在SpringBoot中有好几种启动方式:
- 1、jar包(main方法);
- 2、war包(丢到Tomcat的webapp目录下,这个时候ServletContext才是有值的)。
这里就是if....else...判断存在的意义。
分析一行代码:从IOC容器中找到ServletWebServerFactory对应的bean容器
ServletWebServerFactory factory = 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("yyyy");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("xxxx");
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
那么问题来了,此时此刻,从IOC容器中找到ServletWebServerFactory对应的bean容器,找到的是哪个ServletWebServerFactory对象?而且ServletWebServerFactory容器对象是在哪里配置的呢?
首先来看下ServletWebServerFactory的实现类:
可以看到有三个实现类,那么我们默认使用的是TomcatServletWebServerFactory。
3.3.1、ServletWebServerFactory的选择
既然是从IOC容器中来找TomcatServletWebServerFactory,那么什么时候添加到IOC容器中的呢?
又是利用Spring中的SPI机制,在Spring启动流程中再来说。在Spring.factories中有对应的配置。对应的路径在META-INF\Spring.factories中
org.Springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
在ServletWebServerFactoryAutoConfiguration类中
@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})
// 如果当前是SERVLET环境,那么就是就导入三个类
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties({ServerProperties.class})
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
// ......
}
利用@Import注解导入了EmbeddedTomcat类
static class EmbeddedTomcat {
@Bean
TomcatServletWebServerFactory tomcatServletWebServerFactory(ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers, ObjectProvider<TomcatContextCustomizer> contextCustomizers, ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
factory.getTomcatConnectorCustomizers().addAll((Collection)connectorCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatContextCustomizers().addAll((Collection)contextCustomizers.orderedStream().collect(Collectors.toList()));
factory.getTomcatProtocolHandlerCustomizers().addAll((Collection)protocolHandlerCustomizers.orderedStream().collect(Collectors.toList()));
return factory;
}
}
当添加到容器中后,所以能够从IOC容器中获取得到对应的TomcatServletWebServerFactory对应的Bean。
3.3.2、DispatcherServlet类是如何添加到容器中的
那么来看一下Tomcat的启动代码:
public WebServer getWebServer(ServletContextInitializer... initializers) {
if (this.disableMBeanRegistry) {
Registry.disableRegistry();
}
// 创建Tomcat对象
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);
}
// =============================================
// =============================================
// =============================================
private void initialize() throws WebServerException {
logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
synchronized (this.monitor) {
try {
addInstanceIdToEngineName();
Context context = findContext();
context.addLifecycleListener((event) -> {
if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
// Remove service connectors so that protocol binding doesn't
// happen when the service is started.
removeServiceConnectors();
}
});
// Start the server to trigger initialization listeners
// 启动Tomcat
this.tomcat.start();
// We can re-throw failure exception directly in the main thread
rethrowDeferredStartupExceptions();
try {
ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
}
catch (NamingException ex) {
// Naming is not enabled. Continue
}
// Unlike Jetty, all Tomcat threads are daemon threads. We create a
// blocking non-daemon to stop immediate shutdown
// 阻塞代码
startDaemonAwaitThread();
}
catch (Exception ex) {
stopSilently();
destroySilently();
throw new WebServerException("Unable to start embedded Tomcat", ex);
}
}
}
// =============================================
// =============================================
// =============================================
private void startDaemonAwaitThread() {
Thread awaitThread = new Thread("container-" + (containerCounter.get())) {
@Override
public void run() {
// 阻塞代码
TomcatWebServer.this.tomcat.getServer().await();
}
};
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
// 调用线程中的启动方法
awaitThread.start();
}
既然Tomcat已经启动起来了,那么对应的DiaptcherServlet类又是如何来进行添加启动的呢?
this.webServer = factory.getWebServer(getSelfInitializer());
在这行代码中,有一个方法入参,那么来看一下这里的方法入参,方法入参为一个lambda表达式
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);
// 看到这里!想象到了一个相似的类:ServletContainerInitializer
// 从IOC容器中找到接口的实现类
for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
beans.onStartup(servletContext);
}
}
selfInitialize方法作为ServletContextInitializer接口中的onStartup方法的方法体,这个时候作为参数的时候还是没有执行这里的lambda表达式的。
那么看一下ServletContextInitializer类,也有onStartup方法:
@FunctionalInterface
public interface ServletContextInitializer {
void onStartup(ServletContext servletContext) throws ServletException;
}
那么这里也是由Tomcat利用SPI机制调用的吗?在哪里ServletContextInitializer.onStartup被调用的呢?
只有lambda表达式的方法被触发的时候,ServletContextInitializer.onStartup才会被触发。
对应的执行流程如下所示:
ServletContextInitializer#Lambda--execute onStartUp-->selfInitialize方法执行---->ServletContextInitializer.onStartup
然后跟踪源码到:org.Springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory#getWebServer
public WebServer getWebServer(ServletContextInitializer... initializers) {
// ......
// 开始来准备Tomcat的上下文
prepareContext(tomcat.getHost(), initializers);
return getTomcatWebServer(tomcat);
}
protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
// ......
// 然后执行到这里
configureContext(context, initializersToUse);
postProcessContext(context);
}
而TomcatEmbeddedContext extends StandardContext,而在StandardContext启动的时候会触发ServletContainerInitializer.onStartup方法
protected void configureContext(Context context, ServletContextInitializer[] initializers) {
// TomcatStarter implements ServletContainerInitializer
// 但是在META-INF/services下没有找到这个类!
// 那么对应的onStartup又是如何启动的呢?
TomcatStarter starter = new TomcatStarter(initializers);
if (context instanceof TomcatEmbeddedContext) {
TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
embeddedContext.setStarter(starter);
embeddedContext.setFailCtxIfServletStartFails(true);
}
// 重点方法
context.addServletContainerInitializer(starter, NO_CLASSES);
for (LifecycleListener lifecycleListener : this.contextLifecycleListeners) {
context.addLifecycleListener(lifecycleListener);
}
for (Valve valve : this.contextValves) {
context.getPipeline().addValve(valve);
}
for (ErrorPage errorPage : getErrorPages()) {
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);
}
for (MimeMappings.Mapping mapping : getMimeMappings()) {
context.addMimeMapping(mapping.getExtension(), mapping.getMimeType());
}
configureSession(context);
new DisableReferenceClearingContextCustomizer().customize(context);
for (TomcatContextCustomizer customizer : this.tomcatContextCustomizers) {
customizer.customize(context);
}
}
因为TomcatStarter实现了ServletContainerInitializer接口,那么按照以前的思维,就是Tomcat利用SPI机制,来加载META-INF/services目录下对应的实现类进行进行加载,但是当前是没有找到对应的文件的,那么对于TomcatStarter来说,对应的onStartup方法又是如何进行调用的呢?
直接来到org.Springframework.boot.web.embedded.tomcat.TomcatStarter#onStartup方法中:
public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
// 触发ServletContextInitializer中的lambda方法
for (ServletContextInitializer initializer : this.initializers) {
initializer.onStartup(servletContext);
}
}
那么现在问题就是:org.Springframework.boot.web.embedded.tomcat.TomcatStarter中的onStartup如何被触发的?
public void addServletContainerInitializer(
ServletContainerInitializer sci, Set<Class<?>> classes) {
initializers.put(sci, classes);
}
既然利用了initializers来进行了put,那么Tomcat就会在对应的地方来进行执行。但是到了这里和我们的DispatcherServlet又有什么关系呢?那么再来看一下ServletContextInitializer接口的实现类:RegistrationBean,在当前类中也有一个方法(暂时不关注如何调用的)org.Springframework.boot.web.servlet.RegistrationBean#onStartup
public final void onStartup(ServletContext servletContext) throws ServletException {
String description = getDescription();
// ....
register(description, servletContext);
}
来到子类org.Springframework.boot.web.servlet.DynamicRegistrationBean#register中
protected final void register(String description, ServletContext servletContext) {
D registration = addRegistration(description, servletContext);
if (registration == null) {
logger.info(StringUtils.capitalize(description) + " was not registered (possibly already registered?)");
return;
}
configure(registration);
}
然后来到org.Springframework.boot.web.servlet.ServletRegistrationBean#addRegistration
protected ServletRegistration.Dynamic addRegistration(String description, ServletContext servletContext) {
String name = getServletName();
return servletContext.addServlet(name, this.servlet);
}
这里就是来注册一个Servlet,那么这个Servlet是怎么来的呢?
还是来到SpringBoot中的SPI机制,找到DispatcherServletAutoConfiguration,在这个类中可以看到:
@Bean(name = {"dispatcherServletRegistration"})
@ConditionalOnBean(
value = {DispatcherServlet.class},
name = {"dispatcherServlet"}
)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
registration.setName("dispatcherServlet");
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}
// ==============================================
// ==============================================
// ==============================================
// ==============================================
@Bean(name = {"dispatcherServlet"})
public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(httpProperties.isLogRequestDetails());
return dispatcherServlet;
}
而对应的DispatcherServletRegistrationBean就是要注入DispatcherServlet!