`
herman_liu76
  • 浏览: 96742 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

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

阅读更多
补:
    最近回看了一下文章,由于间隔了一段,写过的东西都忘记了。所以需要一个好记的,简洁的,整体的东西来说明这一切,在大脑中形成索引框架,故加一些知识点。

---------------------------------- 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://stackoverflow.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多,可以专门继承一个用啊?
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics