Spring WebMVC 源码分析(4)-Tomcat 服务器启动流程

1. 前言

Spring WebFlux源码分析(1)-服务启动流程 中笔者大致分析了 Spring WebFlux 的启动流程,其实这部分可以说是 SprintBoot 的启动主流程,Spring WebMVC 的启动也由此触发。需要注意的是Spring WebMVC 框架默认集成 Tomcat 作为底层服务器,如果不指定 WebApplicationType, 那 AbstractApplicationContext 的实现类就变成了 AnnotationConfigServletWebServerApplicationContext,该类继承于 ServletWebServerApplicationContextSpring WebMVC 框架中的重要逻辑基本都在 ServletWebServerApplicationContext 中,本文的分析将从这个类开始

2. Tomcat 服务器的启动分析

在这里插入图片描述

2.1 Tomcat 服务器的启动

  1. 我们知道 SpringBoot 框架启动过程中最核心的容器刷新方法 AbstractApplicationContext#refresh() 将被调用,而作为其子类的 ServletWebServerApplicationContext#onRefresh() 方法将被回调,可以看到这个方法的核心逻辑是调用 ServletWebServerApplicationContext#createWebServer() 方法去创建底层服务器

    @Override
     protected void onRefresh() {
     	super.onRefresh();
     	try {
     		createWebServer();
     	}
     	catch (Throwable ex) {
     		throw new ApplicationContextException("Unable to start web server", ex);
     	}
     }
    
  2. ServletWebServerApplicationContext#createWebServer() 方法的实现如下,比较重要的处理是调用 ServletWebServerApplicationContext#getWebServerFactory() 方法确定一个 Servlet 服务器工厂实例,使用 Tomcat 的话就会取到一个 TomcatServletWebServerFactory 实例,随后使用这个工厂去获取服务器实例,此处需要注意 ServletWebServerApplicationContext#getSelfInitializer() 方法的返回值将作为参数

    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();
     }
    
  3. ServletWebServerApplicationContext#getSelfInitializer() 方法非常简单,就是将私有方法ServletWebServerApplicationContext#selfInitialize() 作为函数式接口 ServletContextInitializer 的实现返回,此处是将上层的 Servlet/Filter 注入到 Tomcat 中的关键

    private org.springframework.boot.web.servlet.ServletContextInitializer getSelfInitializer() {
     	return this::selfInitialize;
     }
    
  4. TomcatServletWebServerFactory#getWebServer() 方法将上层的 ServletContextInitializer 初始化对象作为入参,开始创建 Tomcat 服务器对象实例及其重要组件,关键步骤如下,不了解 Tomcat 组件的读者可参考 Tomcat源码分析(1)-结构组成与核心组件

    1. 新建 Tomcat 内部的 Tomcat 实例
    2. 新建 Tomcat 的 Connector 组件,其内部包含的组件都将被创建
    3. tomcat.getService().addConnector() 将先创建 StandardServer 对象,随后创建 StandardService 对象
    4. 调用 TomcatServletWebServerFactory#customizeConnector() 方法将外部对 Connector 组件的定制化配置载入
    5. tomcat.getHost().setAutoDeploy() 首先创建顶级容器对象 StandardEngine,之后将创建代表一个站点的 StandardHost 对象
    6. 调用 TomcatServletWebServerFactory#prepareContext() 方法创建代表一个 web 应用的 TomcatEmbeddedContext 对象,并使用上层的 ServletContextInitializer 对其进行配置
    7. 调用 TomcatServletWebServerFactory#getTomcatWebServer() 方法获取一个面向上层的 WebServer 对象,并启动 Tomcat 服务器
    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);
     }
    
  5. Tomcat#getService() 方法的内部逻辑是首先尝试获取 Tomcat 的 Server 实例,没有则创建新的 StandardServer 并为其创建 StandardService 组件

     public Service getService() {
         return getServer().findServices()[0];
     }
    
     public Server getServer() {
    
         if (server != null) {
             return server;
         }
    
         System.setProperty("catalina.useNaming", "false");
    
         server = new StandardServer();
    
         initBaseDir();
    
         // Set configuration source
         ConfigFileLoader.setSource(new CatalinaBaseConfigurationSource(new File(basedir), null));
    
         server.setPort( -1 );
    
         Service service = new StandardService();
         service.setName("Tomcat");
         server.addService(service);
         return server;
     }
    
  6. Tomcat#getHost() 方法主要负责在服务启动时创建 Tomcat 的重要容器,其内部逻辑与步骤5类似,首先创建顶级顶级容器对象 StandardEngine,之后将创建代表一个站点的 StandardHost 对象

    public Host getHost() {
         Engine engine = getEngine();
         if (engine.findChildren().length > 0) {
             return (Host) engine.findChildren()[0];
         }
    
         Host host = new StandardHost();
         host.setName(hostname);
         getEngine().addChild(host);
         return host;
     }
    
    public Engine getEngine() {
         Service service = getServer().findServices()[0];
         if (service.getContainer() != null) {
             return service.getContainer();
         }
         Engine engine = new StandardEngine();
         engine.setName( "Tomcat" );
         engine.setDefaultHost(hostname);
         engine.setRealm(createDefaultRealm());
         service.setContainer(engine);
         return engine;
     }
    
  7. TomcatServletWebServerFactory#prepareContext() 将创建 Tomcat 的 Context 容器并对其进行配置,本文关注的是 TomcatServletWebServerFactory#configureContext() 方法配置 Context 容器的逻辑

    protected void prepareContext(Host host, ServletContextInitializer[] initializers) {
     	File documentRoot = getValidDocumentRoot();
     	TomcatEmbeddedContext context = new TomcatEmbeddedContext();
     	if (documentRoot != null) {
     		context.setResources(new LoaderHidingResourceRoot(context));
     	}
     	context.setName(getContextPath());
     	context.setDisplayName(getDisplayName());
     	context.setPath(getContextPath());
     	File docBase = (documentRoot != null) ? documentRoot : createTempDir("tomcat-docbase");
     	context.setDocBase(docBase.getAbsolutePath());
     	context.addLifecycleListener(new FixContextListener());
     	context.setParentClassLoader((this.resourceLoader != null) ? this.resourceLoader.getClassLoader()
     			: ClassUtils.getDefaultClassLoader());
     	resetDefaultLocaleMapping(context);
     	addLocaleMappings(context);
     	context.setUseRelativeRedirects(false);
     	try {
     		context.setCreateUploadTargets(true);
     	}
     	catch (NoSuchMethodError ex) {
     		// Tomcat is < 8.5.39. Continue.
     	}
     	configureTldSkipPatterns(context);
     	WebappLoader loader = new WebappLoader(context.getParentClassLoader());
     	loader.setLoaderClass(TomcatEmbeddedWebappClassLoader.class.getName());
     	loader.setDelegate(true);
     	context.setLoader(loader);
     	if (isRegisterDefaultServlet()) {
     		addDefaultServlet(context);
     	}
     	if (shouldRegisterJspServlet()) {
     		addJspServlet(context);
     		addJasperInitializer(context);
     	}
     	context.addLifecycleListener(new StaticResourceConfigurer(context));
     	ServletContextInitializer[] initializersToUse = mergeInitializers(initializers);
     	host.addChild(context);
     	configureContext(context, initializersToUse);
     	postProcessContext(context);
     }
    
  8. TomcatServletWebServerFactory#configureContext() 方法的重要逻辑如下:

    1. 将上层传入的 ServletContextInitializer 列表封装到容器初始化类 TomcatStarter 中,通过 context.addServletContainerInitializer() 添加到 Context 容器内部
    2. 借助 TomcatContextCustomizer 将外部对 Context 容器的定制化配置载入
    protected void configureContext(Context context, ServletContextInitializer[] initializers) {
     	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);
     	}
     }
    
  9. TomcatServletWebServerFactory#getTomcatWebServer() 方法将创建一个面向上层的 TomcatWebServer 对象,并在这个类的构造方法中调用 TomcatWebServer#initialize() 方法,最终调用 Tomcat#satrt() 方法启动 Tomcat 服务器

    protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
     	return new TomcatWebServer(tomcat, getPort() >= 0);
     }
    
    public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
     	Assert.notNull(tomcat, "Tomcat Server must not be null");
     	this.tomcat = tomcat;
     	this.autoStart = autoStart;
     	initialize();
     }
    
    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
     			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);
     		}
     	}
     }
    
  10. Tomcat#satrt() 方法其实就是使用 StandardServer 对象开始整个服务器的启动流程,这部分不再赘述,有兴趣的读者可以参考 Tomcat源码分析(3)-容器 Container 的启动,这个过程将一直调用到 Context 容器的启动方法,也就是 StandardContext#startInternal(),到这一步开始进入下一节分析

     public void start() throws LifecycleException {
        getServer();
        server.start();
    }
    

2.2 Filter 过滤器的注入

  1. StandardContext#startInternal() 这个方法比较长,本文主要关注上层组件的注入流程。在上一节步骤8中笔者提到了上层容器初始化类 TomcatStarter 对象已经添加到 Context 容器内部,此处则主要关注其方法回调的流程

    1. 首先调用 StandardContext#getServletContext() 获取 ServletContext 对象,此处会创建一个 ApplicationContext 对象,并将其包装类 ApplicationContextFacade 对象作为返回值返回
    2. 在启动过程中,会遍历 Context 容器内部初始化器列表并回调ServletContainerInitializers接口方法,则此处将回调到 TomcatStarter#onStartup() 方法
    protected synchronized void startInternal() throws LifecycleException {
    
             ......
    
             // 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;
                 }
             }
    
             ......
     }
    
      public ServletContext getServletContext() {
         if (context == null) {
             context = new ApplicationContext(this);
             if (altDDName != null)
                 context.setAttribute(Globals.ALT_DD_ATTR,altDDName);
         }
         return context.getFacade();
     }
    
  2. TomcatStarter#onStartup()逻辑非常简单,同样是回调内部的初始化器ServletContextInitializer接口方法,读者如没有印象,可以回到上节步骤3,则可以知道此处将回调到 Spring 上层的 ServletWebServerApplicationContext#selfInitialize() 方法

    public void onStartup(Set<Class<?>> classes, ServletContext servletContext) throws ServletException {
     	try {
     		for (ServletContextInitializer initializer : this.initializers) {
     			initializer.onStartup(servletContext);
     		}
     	}
     	catch (Exception ex) {
     		this.startUpException = ex;
     		// Prevent Tomcat from logging and re-throwing when we know we can
     		// deal with it in the main thread, but log for information here.
     		if (logger.isErrorEnabled()) {
     			logger.error("Error starting Tomcat context. Exception: " + ex.getClass().getName() + ". Message: "
     					+ ex.getMessage());
     		}
     	}
     }
    
  3. ServletWebServerApplicationContext#selfInitialize() 方法的实现并不复杂,关键是调用 ServletWebServerApplicationContext#getServletContextInitializerBeans() 方法新建一个 ServletContextInitializerBeans对象,在该对象的构造方法中获取 Spring 容器内实现了 ServletContextInitializer 接口的类实例并保存在内部,最后遍历这个新对象并逐一回调其保存的实例的接口方法。 ServletContextInitializer 接口的继承结构如下:
    在这里插入图片描述

    private void selfInitialize(ServletContext servletContext) throws ServletException {
     	prepareWebApplicationContext(servletContext);
     	registerApplicationScope(servletContext);
     	WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
     	for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
     		beans.onStartup(servletContext);
     	}
     }
    
  4. 本文以 Filter 过滤器 为例分析上层组件注入 Tomcat 容器的过程,则接上一步骤可以知道 FilterRegistrationBean#onStartup() 方法将被触发。这个方法实际由其父类RegistrationBean#onStartup() 实现,核心的逻辑是调用子类实现的 DynamicRegistrationBean#register() 方法

    public final void onStartup(ServletContext servletContext) throws ServletException {
     	String description = getDescription();
     	if (!isEnabled()) {
     		logger.info(StringUtils.capitalize(description) + " was not registered (disabled)");
     		return;
     	}
     	register(description, servletContext);
     }
    
  5. DynamicRegistrationBean#register() 方法的重要逻辑分为两步:

    1. 调用子类实现的 addRegistration() 方法,返回一个注册器对象,此处将调用到 AbstractFilterRegistrationBean#addRegistration() 方法
    2. 通过这个注册器对象对 Tomcat 服务器做进一步配置,将调用到子类实现 AbstractFilterRegistrationBean#configure() 方法
    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);
     }
    
  6. AbstractFilterRegistrationBean#addRegistration() 方法比较简单,核心就是调用本节步骤1创建的 ServletContext 对象方法将上层封装在FilterRegistrationBean中的过滤器添加到 Tomcat 内部,此处最终将触发 ApplicationContext#addFilter() 方法

    protected Dynamic addRegistration(String description, ServletContext servletContext) {
     	Filter filter = getFilter();
     	return servletContext.addFilter(getOrDeduceName(filter), filter);
     }
    
  7. ApplicationContext#addFilter() 方法核心逻辑如下:

    1. 首先将上层的过滤器 Filter 封装为 FilterDef,并将其添加到 Tomcat 的 Context 容器内部列表中
    2. 返回一个封装了 Context 容器的 ApplicationFilterRegistration 对象
    private FilterRegistration.Dynamic addFilter(String filterName,
             String filterClass, Filter filter) throws IllegalStateException {
    
         if (filterName == null || filterName.equals("")) {
             throw new IllegalArgumentException(sm.getString(
                     "applicationContext.invalidFilterName", filterName));
         }
    
         if (!context.getState().equals(LifecycleState.STARTING_PREP)) {
             //TODO Spec breaking enhancement to ignore this restriction
             throw new IllegalStateException(
                     sm.getString("applicationContext.addFilter.ise",
                             getContextPath()));
         }
    
         FilterDef filterDef = context.findFilterDef(filterName);
    
         // Assume a 'complete' FilterRegistration is one that has a class and
         // a name
         if (filterDef == null) {
             filterDef = new FilterDef();
             filterDef.setFilterName(filterName);
             context.addFilterDef(filterDef);
         } else {
             if (filterDef.getFilterName() != null &&
                     filterDef.getFilterClass() != null) {
                 return null;
             }
         }
    
         if (filter == null) {
             filterDef.setFilterClass(filterClass);
         } else {
             filterDef.setFilterClass(filter.getClass().getName());
             filterDef.setFilter(filter);
         }
    
         return new ApplicationFilterRegistration(filterDef, context);
     }
    
  8. 回到本节步骤5第二步,AbstractFilterRegistrationBean#configure() 方法会将 Filter 过滤器 和它应该拦截的 URL 通过新建的注册器对象 ApplicationFilterRegistration#addMappingForUrlPatterns() 方法添加到 Tomcat 的 Context 容器中

    protected void configure(FilterRegistration.Dynamic registration) {
     	super.configure(registration);
     	EnumSet<DispatcherType> dispatcherTypes = this.dispatcherTypes;
     	if (dispatcherTypes == null) {
     		dispatcherTypes = EnumSet.of(DispatcherType.REQUEST);
     	}
     	Set<String> servletNames = new LinkedHashSet<>();
     	for (ServletRegistrationBean<?> servletRegistrationBean : this.servletRegistrationBeans) {
     		servletNames.add(servletRegistrationBean.getServletName());
     	}
     	servletNames.addAll(this.servletNames);
     	if (servletNames.isEmpty() && this.urlPatterns.isEmpty()) {
     		registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter, DEFAULT_URL_MAPPINGS);
     	}
     	else {
     		if (!servletNames.isEmpty()) {
     			registration.addMappingForServletNames(dispatcherTypes, this.matchAfter,
     					StringUtils.toStringArray(servletNames));
     		}
     		if (!this.urlPatterns.isEmpty()) {
     			registration.addMappingForUrlPatterns(dispatcherTypes, this.matchAfter,
     					StringUtils.toStringArray(this.urlPatterns));
     		}
     	}
     }
    
  9. ApplicationFilterRegistration#addMappingForUrlPatterns() 方法如下,可以看到此处新建了一个 FilterMap 对象,并随后将其添加到了 Context 容器中。在网络请求到来时,Tomcat 将会把 Servlet 和对应的 Filter 封装到一个 ApplicationFilterChain 执行链对象中,先执行过滤器逻辑再执行 Servlet 逻辑,读者不清楚可参考 Tomcat源码分析(4)-网络请求处理至此 Spring MVC 框架 中 Tomcat 服务器的整合分析告一段落

     public void addMappingForUrlPatterns(
             EnumSet<DispatcherType> dispatcherTypes, boolean isMatchAfter,
             String... urlPatterns) {
    
         FilterMap filterMap = new FilterMap();
    
         filterMap.setFilterName(filterDef.getFilterName());
    
         if (dispatcherTypes != null) {
             for (DispatcherType dispatcherType : dispatcherTypes) {
                 filterMap.setDispatcher(dispatcherType.name());
             }
         }
    
         if (urlPatterns != null) {
             // % decoded (if necessary) using UTF-8
             for (String urlPattern : urlPatterns) {
                 filterMap.addURLPattern(urlPattern);
             }
    
             if (isMatchAfter) {
                 context.addFilterMap(filterMap);
             } else {
                 context.addFilterMapBefore(filterMap);
             }
         }
         // else error?
    
     }
    
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值