欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

spring与tomcat的关系逆袭前后的源码设计分析

程序员文章站 2022-03-07 15:22:00
...
补:
    最近回看了一下文章,由于间隔了一段,写过的东西都忘记了。所以需要一个好记的,简洁的,整体的东西来说明这一切,在大脑中形成索引框架,故加一些知识点。

---------------------------------- BEGIN 补-------------------------------
从xml配置到无xml配置,再到自动配置的发展

1.xml配置的方式
web.xml中有三个重要内容:

<!--转化为键值对,并交给ServletContext
-->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-context.xml</param-value>
</context-param>
<!--监听器得到值:ServletContext.getInitParameter("context-param的键");-->
<listener>
<description>配置Spring上下文监听器</description>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param><!--以下init-param是自定义SpringMVC的配置文件的位置 -->
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>


1. 先在ServletContext中存个参数,记录下根容器的配置xml文件
2. 当ContextLoaderListener监听到ServletContext初始化后的操作中,会产生一个mvc容器,产生时以从上面拿到这个xml配置文件为依据。根容器存在ServletContext中。
3. 接下来是servlet配置,其init参数包含了mvc容器的xml配置文件。在DispatcherServlet初始化时,会initWebApplicationContext产生mvc容器,再从ServletContext找到的根容器,作为mvc容器的父容器。mvc容器每个servlet自用,根容器多个servlet可以共用。
   tomcat这些就是servlet容器,让外部放servlet进去。所以spring的想法就是弄分发型的dispatcherServlet,它自己用的bean都放在一个springmvc的容器中。由于有共用的bean存在,再弄一个共用的根容器,根容器先产生即可。我想也可以在第一个servletspringmvc容器产生时产生根容器也可以,在servletContext启动时监听方式产生是作者是设计。

2.无xml配置的方式
   容器产生的方式不同这个好理解,我们知道ApplicationContext有三个实现类,ClassPathXmlApplicationContext/FileSystemXmlApplicationContext/AnnotationConfigApplicationContext。所以用@Configure标注的配置类可以代替xml。
   但是,spring的容器要被servletContext带起来的,这个要servlet的规范发展了。
   在 Servlet 3.0 环境下,Servlet 容器会在 classpath 下搜索实现了 javax.servlet.ServletContainerInitializer 接口的任何类,找到之后用它来初始化ServletContext容器。Spring 3.2 开始引入一个简易的 WebApplicationInitializer 实现类,这就是 AbstractAnnotationConfigDispatcherServletInitializer。你只需要继承它,并写两个spring容器的配置类就可以了。它在一个onstart操作中,产生根容器,还产生dispatcherServlet及mvc容器。
   根容器的配置类注解上@configure,并扫描各个包里的componet/service/dao...
   mvc的配置类注解@configure,并扫描各个包里的controller,另外这个类要实现WebMvcConfigurer,(或者继承WebMvcConfigurerAdapter,过时了),还有一点要@EnableWebMvc注解。这个注解把基本的mvc里的很多东西都配上了,还要加载你写的WebMvcConfigurer里的东东。

   后面介绍的【从Tomcat启动spring】就是这种情况。

3.springboot自动配置
   自动配置简单的说就是sprinboot会从meta-inf文件中自动配置类,这些类会按条件,延时把bean放到容器中去。还会从属性配置中找属性使用。
   springboot是内嵌比如tomcat的,虽然是spring启动tomcat,不过tomcat回调一个onstart的类(this::selfInitialize写法中匿名的类)来设置ServletContext。
    根容器先产生,因为spring为主,没有问题。通过这个匿名类放在ServletContext中的KV值了,与前面一样。
    匿名类还会找容器中所有实现了ServletContextInitializer的类进一步配置,比如DispatcherServletAutoConfiguration中的DispatcherServletRegistrationBean,它既然实现了那个接口,就把DispatcherServlet设置一下,放进ServletContext中了。DispatcherServlet当然也是这个自动配置类中的bean。而且先产生。
    还有一个WebMvcAutoConfiguration在DispatcherServletAutoConfiguration之后进行自动配置,比如会加入一些bean,比如:InternalResourceViewResolver。注意这个自动配置与WebMvcConfigurationSupport.class冲突,也就是与@EnableWebMvc注解冲突。他们都会有自己的默认的mvc的bean。自动配置的更全面,而@EnableWebMvc引入的少,一般要自己再加。
   @EnableWebMvc是springframwork的webmvc中的,在boot之前是给mvc的配置类上用的,里面的bean在mvc容器中。在boot里如果要用,它的bean就在根容器中。

    总的来说,无非就是讲根容器,mvc容器,以及dispatcherServlet三个东东。dispatcherServlet要持有mvc容器,才有工具干活。
    自动配置的dispatcherServlet是容器中的bean时,就会aware感知自己的容器,因为它有这个接口。而如果象前面,在代码中new的话,就要人工给它配备mvc容器。这时dispatcherServlet所aware后持有的是根容器,而在inti时要让它关联根容器,还是根容器把自己当父容器,所以所有的bean都在一起的。
    以下是DispatcherServlet源码中关于这一点的注解。
* <p>As of Spring 3.1, {@code DispatcherServlet} may now be injected with a web
* application context, rather than creating its own internally. This is useful in Servlet
* 3.0+ environments, which support programmatic registration of servlet instances.
* See the {@link #DispatcherServlet(WebApplicationContext)} javadoc for details.
---------------------------------- END 补-------------------------------


简介
​ Tomcat与spring是最常用的东东。本文以Tomcat代表webServer,对比了从Tomcat这样的webServer,来启动spring应用,和最新的springboot启动Tomcat的源码实现过程。加深了对两个系统的了解,从大的方向上学习了系统之间如何组合及设计考量。

​ 学习了很多相关技术的贴子并阅读了源码,但目前没看到全面分析对比的文章。

​ 本文以功能为本,注重核心类与接口的关系,有助于整体上把握大系统的设计。不会有太多的代码,更不会分析不太重要的接口,不会有细节的类图与泳道图。本文以Servlet 3.0+环境为主,就不介绍太早的web.xml配置了。我看的springboot是2.2.0.BUILD-SNAPSHOT。

tomcat启动多个包含的应用 **VS** 一个spring应用通过web服务器展示感觉spring从规范tomcat下的一个应用,到了以应用为主,通过各种途经暴露自己的核心应用了,甚至react方式绕过servlet了。算是逆袭吧!

1. 从Tomcat启动spring
1.1 tomcat给外部系统的机会
​ 主要提供有ServletContainerInitializer接口与其上的@HandlesTypes注解类。从名字上可以知道,让外部提供一个参与初始化ServletContainer的类。该

接口方法onStartup(Set<Class<?>> c, ServletContext ctx)。参数是所有实现@HandlesTypes指明接口的实现类,与ServletContext 。

​ Tomcat提供的机会就这么多,它会从meta-inf/serivces/ServletContainerInitializer文件中找到具体实现了SpringServletContainerInitializer的类A,还会找到实现类上@HandlesTypes()注解里的接口的所有实现类Bs。最后调用A的onstartup方法,参数是Bs与给它的ServletContext 。

有一点奇怪,为何tomcat不少管一点,只调用A,让A自己找所有的Bs?毕竟B类型自己说了算的。

1.2 spring如何对接
对接ServletContainerInitializer

Spring-web中的meta-inf里面的文本文件里写的org.springframework.web.SpringServletContainerInitializer。这个类的注解是@HandlesTypes(WebApplicationInitializer.class) ,它的onStartup方法就是实现化所有的WebApplicationInitializer.class实现类,并调用它们的initializer.onStartup(servletContext);

servletContext是tomcat给过来的,现在交给了所有的WebApplicationInitializer.class实现类。它并没有核心功能,如同一个中介一样。

对接@HandlesTypes(WebApplicationInitializer.class)

Spring为了方便使用,引入了一个抽象类来实现WebApplicationInitializer.class,也就是 org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer 。因为我们最终要按自己的配置要求扩展它,代替web.xml的配置就是在这里。

因此当部署到 Servlet 3.0 容器中的时候,容器通过@HandlesTypes会自动发现它,通过中介SpringServletContainerInitializer,来配置Servlet上下文servletContext。

AbstractAnnotationConfigDispatcherServletInitializer要你实现什么哪些抽象方法?

方法一:Class<?>[] getRootConfigClasses();

得到@Configuration注解的类,给createRootApplicationContext()来用。我们知道这个注解通常可以生成AnnotationConfigWebApplicationContext类型的一个spring容器。这个是根容器。

方法二:Class<?>[] getServletConfigClasses();

得到@Configuration注解的类,给createServletApplicationContext()用。也是用来生成AnnotationConfigWebApplicationContext类型的spring容器,这个会是一个Servlet所属的子容器。目前还没有和根容器关联。

AbstractAnnotationConfigDispatcherServletInitializer类机制是怎么的?

补全了抽象方法,我们还是要知道这个类被中介调用的,中介调它onStartup方法,传入servletContext。这个方法主要有两段功能组合,一个是父类中,一个是本类中。

//<!------------------------onStartup方法解析(两段功能):----------------->
	@Override
	public void onStartup(ServletContext servletContext) throws ServletException {
		super.onStartup(servletContext);
		registerDispatcherServlet(servletContext);
	}

//----父类super.onStartup功能:
//先产生根spring容器,再把容器被包装进一个对servletContext监听的ContextLoaderListener。它实现接口的contextInitialized与contextDestroyed两个动作由servletContext触发。前者会把根spring容器作为servletContext中的一个KV项。 但servletContext何时初始化要等先被配置好。web.xml出有ContextLoaderListener的配置。
WebApplicationContext rootAppContext = createRootApplicationContext();
	if (rootAppContext != null) {
		ContextLoaderListener listener = new ContextLoaderListener(rootAppContext);
        //getRootApplicationContextInitializers(),一般不用。
		listener.setContextInitializers(getRootApplicationContextInitializers());
		servletContext.addListener(listener);
	}
	
	
	
//----子类中registerDispatcherServlet的功能:
//产生dispatcherServlet与它的MVC容器。最后把dispatcherServlet注册进servletContext。并设置启动,mapping等信息。在WEB.XML中也有这样的设置项目。
	WebApplicationContext servletAppContext = createServletApplicationContext();
		FrameworkServlet dispatcherServlet = createDispatcherServlet(servletAppContext);//new一个dispatcherServlet。
dispatcherServlet.setContextInitializers(getServletApplicationContextInitializers());
		ServletRegistration.Dynamic registration = servletContext.addServlet(servletName, dispatcherServlet);
		registration.setLoadOnStartup(1);
		registration.addMapping(getServletMappings());
		registration.setAsyncSupported(isAsyncSupported());


出现的DispatcherServlet特别说明一下

Servlet一般有init(),service(req,res),destroy()等方法。DispatcherServlet持有MVC容器,很好利用容器中的mapping与controller对象处理业务了。先关注一下init();真正的功能在initServletBean()中的initWebApplicationContext()。它注册进了servletContext,就可以从中拿到root窗口,并用setParent(rootContext)方法设置好它所持有的mvc容器的父级spring容器,这样@controller对象就好从父级中找到@service对象了。

上面把web服务启动后,主要功能都设置好了。不过servletContext的contextInitialized与DispatcherServlet的init()方法,只是配置好了功能,还需要等时机来trigger起来。参考spring中类被自动装配好才init,所以servletContext应该也等相关的配置(Servlet类型的,还有Listener、Filter类型)都好就,就可以init()了。 servlet的初始化有两种策略:lazy-loading和eager-loading;前者当第一个请求过来的时候才会调用init,后者是容器初始化的时候就调用init。反正是先根容器好了,才可以执行挂着子容器的操作。

1.3 总结
目标
​ Tomcat只给外部应用一个时机,让外部配置servletContext。而spring的目标是把两种容器(共用根容器与每个servlet的单独子容器)及DispatcherServlet等东东加进servletContext去,让DispatcherServlet用作请求转发处理。

实现
​ WebServer启动时,会启动一个文本文件中SPI的ServletContainerInitializer实现,把servletContext给它处理。这个spring的中介会实例化最终用户配置的一个AbstractDispatcherServletInitializer,把servletContext给它处理。

​ AbstractDispatcherServletInitializer处理时会根据配置类产生根容器,并使用一个监听在servletContext.init()时把根容器加为servletContext中的一个KV项。

​ 然后它根据另一个MVC配置类产生子容器与持有它的DispatcherServlet,并注册到servletContext。等DispatcherServlet.init()时会关联上面说的父容器。

1.4 引申
​ 前面介绍的AbstractAnnotationConfigDispatcherServletInitializer用起来很简单,只要设置两个spring容器的配置类就可以了,父子容器就都有了,用着爽。但要自己进行些处理就麻烦点了,你可以继承抽象类的父类多些灵活性。另外这个文章:https://my.oschina.net/521cy/blog/702864【零配置即使用Java Config + Annotation】中,没有去继承的抽象类,自己实现了相关接口,并详细介绍了与web.xml的对比进行配置,可以参考。其核心的功能还是一样的。如果你想配置多个Servlet,或者DispatcherServlet,都是比较容易实现的了。

这个文章中的方法:onStartup(ServletContext container),后面的container名称不妥,ServletContainer与ServletContext是不同层次的东西,前者更大,这样写名不副实。


2. 从spring启动Tomcat
​ 这个就是springboot的方式,用main启动,使用内嵌Tomcat。

2.1 spring的反客为主的思路
spring的根容器中都是核心业务,按说Tomcat只是一个暴露通讯方式,即可以用Tomcat,也可以用其它Servlet容器。还可以不用Servlet容器,比如WebApplicationType.REACTIVE类型,会绕过servlet容器。按说它还可以进一步适配各种通讯协议供外部使用核心业务。这就是反客为主。

注:看过一个区块链教程中,HTTPService httpService = new HTTPService(blockService, p2pService);用http通讯服务去整合核心服务与P2P通讯,明显不妥当。应该用核心去整合通讯方式并适配多种方式才是稳定的。

​ 既然以spring中的业务为主,它就会在启动中带动相关的其它外部应用模块,比如Tomcat容器的启动。

2.2 springboot的启动
​ 通常我们的应用中,在有@SpringBootApplication的主类的main中调用:SpringApplication.run(*.class),进而调用到下面的方法:

//1。启动后调用的方法:
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    //配置一个new出来的SpringApplication,并运行,产生一个springIoc容器。
	return new SpringApplication(primarySources).run(args);
}


//2。上面的方法中的run里面:就是生成一个容器及常见的操作:prepareContext,refreshContext,afterRefresh
//而new 操作,根据类判断,可以产生三种容器,一般是servlet类型的特殊spring容器AnnotationConfigServletWebServerApplicationContext,这里还没确定是tomcat或者jetty呢
...
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
...
    
//3。AnnotationConfigServletWebServerApplicationContext父类的refresh中会调用onRefresh,里面有一句createWebServer();这里就开始生成Web服务器了。
    protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer();
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}


2.3 产生WebServer工厂的过程
​ 仅会生成特定的WebServer,并产生一个初始化工具(ServletContextInitializer实现类)给它。有点像新成立一个分公司,却只派了一个业务指导过去。

​ 看工具接口名字就知道是用来初始化ServletContext的。ServletContext的层次在Tomcat中并不高,上面还有container,且看分析。

//1。createWebServer();中主要有这两句,用一个工厂来生成WebServer,工厂包含mock的共有4种。同时把getSelfInitializer得到的一个实现了ServletContextInitializer的初始化工具给它。
	ServletWebServerFactory factory = getWebServerFactory();
	this.webServer = factory.getWebServer(getSelfInitializer());


//2。这个工具的写法有点独特this::selfInitialize,主要是实现了onStartup(是ServletContext servletContext接口)方法。方法体如下,却看不到方法名字:
private void selfInitialize(ServletContext servletContext) throws ServletException {
	prepareWebApplicationContext(servletContext);
	registerApplicationScope(servletContext);
	WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
	for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
		beans.onStartup(servletContext);
	}
}

//3。上面的方法中有两个地方说明一下:
prepareWebApplicationContext(servletContext):主要是把自己这个spring根容器注册到servletContext中,简单粗暴,不象前者要在监听servletContext时才设置。
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this);

getServletContextInitializerBeans():
//方法中说明为:By default this method will first attempt to find
	 * {@link ServletContextInitializer}, {@link Servlet}, {@link Filter} and certain
	 * {@link EventListener} beans.
//这就是从根容器中找到所有实现了ServletContextInitializer接口的类,用来注册servlet/filter等东东进入ServletContext。与上面的selfInitialize是一个接口,不过是其内部调用的,作用不同。


​ 上面产生一个WebServer的工厂,另外就是传入了一个工具。这个工具被执行时,除了自己处理外,又从spring容器中找了一堆其它的工具来处理。所有的这两层工具都实现了ServletContextInitializer,不过前者注册根spring容器,后者注册servlet等东西。这些工具都在等着onStartup才 运行。onStartup后面会讲到。

2.4 产生TomcatServer的过程
工具们传了进来,具体又传给了谁?

public WebServer getWebServer(ServletContextInitializer... initializers) {
...
	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);
	}
//1。前面都是产生嵌入的Tomcat及它的内部对象。后面两句是重点。先分析第一个。
	prepareContext(tomcat.getHost(), initializers);
	return getTomcatWebServer(tomcat);
}

//2。再产生了一个Web应用,放在host中。工具initializers又传给了它。
	protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
...
		TomcatEmbeddedContext context = new TomcatEmbeddedContext();
...
		File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
		context.setDocBase(docBase.getAbsolutePath());
...
		ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
		host.addChild(context);
//3。这句是重点,继续传initializers进去。
		configureContext(context, initializersToUse);
		postProcessContext(context);
	}

//4。configureContext()的主要内容如下,产生了一个ServletContainerInitializer接口的实现TomcatStarter,它赋给了TomcatEmbeddedContext的目的用来监听WebServer的启动的,而那些工具给了它备用。
		TomcatStarter starter = new TomcatStarter(initializers);
		if (context instanceof TomcatEmbeddedContext) {
			TomcatEmbeddedContext embeddedContext = (TomcatEmbeddedContext) context;
			embeddedContext.setStarter(starter);
			embeddedContext.setFailCtxIfServletStartFails(true);
		}
		context.addServletContainerInitializer(starter, NO_CLASSES);

//5。TomcatStarter作为ServletContainerInitializer,被WebServer启动调用其onStartup,同时会得到servletContext,最外面传入的工具备用在此,就是来初始化servletContext的。前面设置KV根少了一个监控ServletContext的东东,这里又多了一个监控WebServer的东东(叫ServletContainerInitializer这个名字表明了监控的目的,感觉完整的应该叫InitializeServletContainer_ServerListener吧:)。


//6。return getTomcatWebServer(tomcat);
//这句之前都是处理tomcat内部的host->TomcatEmbeddedContext->TomcatStarter。这里是外部包了一层,通常我们适配多种产品,都会分别外包一层,以抽象出公共的对象,让外部无感使用。这句内部有一句:
	this.tomcat.start();
//正式启动了tomcat了。前面配置好的ServletContainerInitializer开始工作了,传过来ServletContext了,ServletContextInitializer也都可以工作了。



该springboot中的接口ServletContextInitializer和前面介绍的Spring Web的另外一个接口WebApplicationInitializer看起来几乎一模一样。而且都被不同的ServletContainerInitializer接口类使用。但二者使用目的不同,初始化的目标不一样。Spring Web中,WebApplicationInitializer也是针对Servlet 3.0+环境,设计用于程序化配置ServletContext,跟传统的web.xml相对或者配合使用,WebApplicationInitializer实现类会被SpringServletContainerInitializer标识,从而被tomcat自动检测和调用。

2.5 第二层次的ServletContextInitializer工具们都在哪,怎么用?
​ 独特this::selfInitialize所产生的第一层次工具,把根spring容器记入ServletContext的KV项,又从容器中找二层工具。哪么都有哪些二层工具呢?怎么用呢?

springboot是自动配置机制的,重点关注这三个:

  • 1.EmbeddedServletContainerAutoConfiguration
  • 注入容器bean,根据当前包扫描,默认tomcat
  • 2.DispatcherServletAutoConfiguration
  • 默认dispatchServlet配置
  • 3.WebMvcAutoConfiguration


看看package org.springframework.boot.autoconfigure.web.servlet中的:

@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
    
...
    //DispatcherServlet,不再介绍了。
		@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServlet dispatcherServlet(HttpProperties httpProperties, WebMvcProperties webMvcProperties) {
			DispatcherServlet dispatcherServlet = new DispatcherServlet();
...
			return dispatcherServlet;
		}
    
    //DispatcherServletRegistrationBean
		@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
		@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
		public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
				WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
			DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
					webMvcProperties.getServlet().getPath());
			registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
			registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
			multipartConfig.ifAvailable(registration::setMultipartConfig);
			return registration;
		}
...
}


//----------------------------------------------------------
//DispatcherServletRegistrationBean的基父类中实现了ServletContextInitializer,所以它是二层工具。本类主要设置path。
public class DispatcherServletRegistrationBean extends ServletRegistrationBean<DispatcherServlet>

public class ServletRegistrationBean<T extends Servlet> extends DynamicRegistrationBean<ServletRegistration.Dynamic> {

public abstract class DynamicRegistrationBean<D extends Registration.Dynamic> extends RegistrationBean {

public abstract class RegistrationBean implements ServletContextInitializer, Ordered {
//本质还是ServletContextInitializer,onStartup方法被调用。
这些类的调用如下:
onStartup方法-->
register(description, servletContext);-->
D registration = addRegistration(description, servletContext);-->
servletContext.addServlet(name, this.servlet);


DispatcherServlet是实现implements ApplicationContextAware接口的,当然就会自动感知spring容器。它持有的就是根容器。不再是之前说的mvc子容器了。

2.6 springmvc容器没有了?

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {


没看到SpringMvc容器的代码,以下文章都提到了一个容器的问题:

https://segmentfault.com/a/1190000017327469【深入Spring Boot:Spring Context的继承关系和影响】

https://www.jianshu.com/p/6a869eabfe78【SpringMvc在SpringBoot环境和Web环境中上下文的关系】

在Web环境中是由Spring和SpringMvc两个容器组成的,在SpringBoot环境中只有一个容器AnnotationConfigEmbeddedWebApplicationContext。

2.2.0.BUILD-SNAPSHOT中的根容器叫AnnotationConfigServletWebServerApplicationContext,1.5.2中还是叫AnnotationConfigEmbeddedWebApplicationContext,看到都有WebApplicationContext,是为web应用而生吧。自动配置就只用这么一个容器了。

2.7 引申(多个容器)
默认是一个容器,也可以搞多个SpringMvc容器的,当你需要:

Spring Boot with multiple DispatcherServlet, each having their own @Controllers时。

https://*.com/questions/30670327

@SpringBootApplication(exclude=DispatcherServletAutoConfiguration.class)
public class Application {
    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
    @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;
    }
}


​ 上面两个SpringMvc容器,不同的配置文件,不同的DispatcherServlet,不同的mapping地址,不同的名字。不过在DispatcherServlet的init()时,都会找到共用的根容器的。注意要排除DispatcherServletAutoConfiguration,不能去自动配置了。

2.8 总结
目标
在产生根spring容器的同时,产生一个嵌入式的tomcat对象,把初始化ServletContext的所有两个层次的ServletContextInitializer工具们传进去。一层工具把根容器注册好,二层工具注册dispatcherServlet等东东。

实现
ServletContextInitializer工具们被传递到tomcat内部比较深的地方,由监听Webserver启动的ServletContainerInitializer的实现类TomcatStarter所持有。

这过程中要判断spring容器类型是servlet,再判断用Tomcat,然后真正工厂来实现化Tomcat及它内部的一些类,还要扩展内部的一个类,通过它才能把带着工具的TomcatStarter放进去。等Tomcat真正启动了,TomcatStarter用工具处理给它的ServletContext。

3. 回顾
前面分析了一通,但还需要从总体上分析一下作者的设计思路。

3.1 对象与生命周期
主要对象:

ServletContext:并非一个servlet对应一个ServletContext。而是一个web应用(webApp)对应一个ServletContext实例,这个实例是应用部署启动后,servlet容器为应用创建的。ServletContext实例包含了所有servlet共享的资源信息。通过提供一组方法给servlet使用,用来和servlet容器通讯,比如获取文件的MIME类型、分发请求、记录日志等。

ServletContainer:可以当作提供servlet功能的WebServer。

生命周期处理:

​ WebServer启动spring 情况下,调用ServletContainerInitializer的启动方法onStartup,给了外部一个配置这个web应用对应的ServletContext的机会。

当onStartup时,产生了根容器,但具体让一个ContextLoaderListener去监听ServletContext的初始化完成操作时候写入根容器到ROOT的KV值。等于是产生了根容器,但要等时机。(让我配置它,我先准备好材料,等contextInitialized这个时机点配置上去)
onStartup时,接着产生dispatcherServlet和它的容器,dispatcherServlet带着它的容器并注册到ServletContext中去。dispatcherServlet也有一个init()机会,这时候去找根容器,作为自己带的容器的父容器。(我先把材料放进去,你用它的时候初始化一下它就可以用了)
说明ServletContext初始化完成,要早于dispatcherServlet的init();。初始化应该晚于放servlet进去,加材料只是配置。一般设计一个类的生命周期,参考spring,有配置,再初始化及正式运行,最后销毁,重要的生命节点要通知监听者。简单的说就是先装配好,再监听,适当时候再进一步处理。

疑问:

  • 为何不在装配时设置KV?我们知道ServletContext加KV值实际上就是webApp的全局共享变量,随时可以加,所以这操作都不能算在初始化中吧。
  • 但tomcat启动,通知一个ServletContainerInitializer实现类来处理ServletContext。既然是配置context,为何不叫ServletContextInitializer呢?也许启动给不同的webApp都同时进行ServletContext设置,多个context就不能叫ServletContextInitializer了,或者加个s,或者按更上层对象来命名吧。

在springboot中,它为Tomcat准备了一堆ServletContextInitializer对象,这是spring里用来处理context的接口,很明显这些要是处理一个WebApp的ServletContext的,只关心这个。

启动Tomcat前,给它要求生成了Connector/getHost等内部对象。还生成一个TomcatEmbeddedContext对象,host.addChild(context);这句加入到host中,然后通过它为中介,把一个ServletContainerInitializer接口对象TomcatStarter设置进去,这是前面提到过的接口,都是监控Webserver启动的。Tomcat启动了就通知到TomcatStarter了,并给它一个真正的context来处理,TomcatStarter早就持有一个多层次的ServletContextInitializer对象,就可以两个层次处理ServletContext了。

3.2 设计思考
两种情况下,真正设置tomcat的ServletContext都是从ServletContainerInitializer的被调用onStartup开始的。

前者由Tomcat启动从文件中找的对象,再通过实现了WebApplicationInitializer的AbstractAnnotationConfigDispatcherServletInitializer处理;它的接口名字与类名字感觉差别比较大,给tomcat调用是为了初始化WebApplication的,而在这个过程中又要产生Servlet并配置进ServletContext,名字如果叫WebApplication2ServletContextInitializer更准确吧。设置KV值还要通过监听来等个机会进行不知道为啥这麻烦?不过KV一定要放在ServletContext,被可能的多个MVC容器共享使用。

后者由springboot启动tomcat并设置ServletContainerInitializer实现类进去,再由tomcat反过来调用ServletContainerInitializer实现类的onStartup()方法开始真正配置ServletContext。后面处理ServletContext的多个类的接口统一叫ServletContextInitializer名字很准确,功能专一。也不再监听去等ServletContext的初始化后的机会设置KV值了,其中一个ServletContextInitializer直接一步就设置好了,其它的ServletContextInitializer都从总容器中拿。复杂的是onStartup前面的过程。

两种情况下,dispatcherServlet一旦被注册进了ServletContext,就由tomcat接手了三个生命周期。不同的是,前者dispatcherServlet带着新生成的mvc容器,并在init时找到父容器。后者因为aware了根容器了,就带着根容器进去的,父容器还是自己了。可能因为有了springboot自动配置机制的便利吧,一个容器就很好按条件自动配置内部的Bean了,之前为啥不搞一个容器呢?担心mvc容器特殊Bean多,可以专门继承一个用啊?