文章

SpringBoot MVC

在介绍Spring Web MVC的时候说过,springboot反转了调用关系,翻身做主人了。springboot启动内嵌的servlet容器,内嵌的servlet容器还和之前调用SpringMVC的方式一样,只不过这次调用的是springboot的组件,不再是SpringMVC了

  1. 蓄意内嵌servlet容器
  2. servlet context初始化
    1. ServletContainerInitializer
  3. 启动流程
    1. 启动springboot - ServletWebServerApplicationContext
    2. 创建WebServer - ServletWebServerFactory
    3. 启动tomcat
      1. 调用ServletContainerInitializer
        1. ServletContainerInitializer SPI探测去哪了?
      2. 调用ServletContextInitializer,注册DispatcherServlet
    4. 启动TomcatWebServer
  4. @ServletComponentScan
  5. 感想

蓄意内嵌servlet容器

springboot MVC不像SpringMVC一样,由servlet容器调用自己。而是启动一个自己的ApplicationContext,先像普通springboot app一样配置好各种bean,这些bean甚至可以包含一些包装了servlet的Servlet/Filter的wrapper bean。然后启动一个servlet容器,再把那些wrapper bean注册到servlet容器里

这样程序猿来说,配置servlet、filter的工作就可以以普通springboot bean的形式进行了。对于新手来说,甚至都不用太理解servlet规范就可以上手配置,启动一个servlet容器了。不得不说,springboot这一思路真的是6!

When using an embedded servlet container, you can register servlets, filters, and all the listeners (such as HttpSessionListener) from the servlet spec, either by using Spring beans or by scanning for servlet components.

Any Servlet, Filter, or servlet Listener instance that is a Spring bean is registered with the embedded container.

By default, if the context contains only a single Servlet, it is mapped to /. In the case of multiple servlet beans, the bean name is used as a path prefix. Filters map to /*.

另外借助springboot定义好的名为web的logging group,可以直接配置logging.level.web=debug以让所有的web相关日志都输出debug,非常方便。

servlet context初始化

那么springboot启动内嵌的tomcat之后,后面应该还是使用SPI自动监测servlet的ServletContainerInitializer或者SpringMVC的SpringServletContainerInitializer做初始化工作吧?也不是了。

servlet的初始化使用的还是ServletContainerInitializer,但不再是随随便便检测到的ServletContainerInitializer,也不再是SpringMVC的SpringServletContainerInitializer,而是springboot手动指定的自己的ServletContainerInitializer实现。按照springboot的说法,这样做可以避免别的实现了servlet的SPI规范的第三方依赖对servlet容器进行初始化。

Embedded servlet containers do not directly execute the jakarta.servlet.ServletContainerInitializer interface or Spring’s org.springframework.web.WebApplicationInitializer interface. This is an intentional design decision intended to reduce the risk that third party libraries designed to run inside a war may break Spring Boot applications.

这一安全考虑也可以参考Spring Web MVCWebApplicationInitializer是怎么被发现的”,因为tomcat确实会把classpath上所有的相关类都收集起来做servlet的初始化,比较失控

这么一看springboot管的挺宽啊!servlet都已经定义了ServletContainerInitializer的SPI规范,springboot却不让自己启动的内嵌servlet容器检测别的SPI规范实现者。按照上面的说法,它这么做是为了防止那些实现者在初始化的时候破坏了springboot app。

springboot怎么做到阻止别人的?

springboot在启动内嵌tomcat的时候,不使用SPI机制检测ServletContainerInitializer了,而是手动set ServletContainerInitializer到tomcat里,且只set springboot自己的ServletContainerInitializer实现类TomcatStarter。这样就能防止第三方随意注册,包括SpringMVC。而且因为掐断了SpringMVC,所以自定义SpringMVC的WebApplicationInitializer以初始化servlet容器的方法也失灵了

ServletContainerInitializer

现在,只有springboot的TomcatStarter会被servlet容器调用以进行初始化。springboot也把初始化的过程委托了出去:就像SpringMVC的SpringServletContainerInitializer提供了WebApplicationInitializer以初始化ServletContext(tomcat的)一样,springboot的TomcatStarter会调用ServletContextInitializer初始化ServletContext。现在我们可以实现ServletContextInitializer的实现类以自定义servlet容器了。

If you need to perform servlet context initialization in a Spring Boot application, you should register a bean that implements the org.springframework.boot.web.servlet.ServletContextInitializer interface. The single onStartup method provides access to the ServletContext and, if necessary, can easily be used as an adapter to an existing WebApplicationInitializer.

springboot的ServletContextInitializer和SpringMVC的WebApplicationInitializer,这俩用来自定义初始化servlet容器的接口长得一模一样,甚至连方法的javadoc都是抄的……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface WebApplicationInitializer {

	/**
	 * Configure the given {@link ServletContext} with any servlets, filters, listeners
	 * context-params and attributes necessary for initializing this web application. See
	 * examples {@linkplain WebApplicationInitializer above}.
	 * @param servletContext the {@code ServletContext} to initialize
	 * @throws ServletException if any call against the given {@code ServletContext}
	 * throws a {@code ServletException}
	 */
	void onStartup(ServletContext servletContext) throws ServletException;

}

public interface ServletContextInitializer {

	/**
	 * Configure the given {@link ServletContext} with any servlets, filters, listeners
	 * context-params and attributes necessary for initialization.
	 * @param servletContext the {@code ServletContext} to initialize
	 * @throws ServletException if any call against the given {@code ServletContext}
	 * throws a {@code ServletException}
	 */
	void onStartup(ServletContext servletContext) throws ServletException;

}

所以这俩的设计思路是一模一样的。唯一的区别就是:在springboot里,SpringMVC被掐断了,所以SpringMVC提供的WebApplicationInitializer不能用了,用springboot提供的ServletContextInitializer吧。用法还和之前写SpringMVC的WebApplicationInitializer一样。

反而觉得springboot的名字起的更直白:所谓init web app,不就是init ServletContext嘛!

启动流程

springboot启动mvc的流程:

  1. 启动springboot app,配置bean;
  2. 启动内嵌servlet容器;
  3. servlet调用springboot的ServletContextInitializer以初始化servlet容器(和servlet调用SpringMVC一模一样);
  4. 将servlet相关的bean从springboot的wac里取出来,注册到servlet里;

启动springboot - ServletWebServerApplicationContext

启动springboot web,ApplicationContext用的是ServletWebServerApplicationContext,它是springboot对WebServerApplicationContextWebApplicationContext接口实现。它和SpringMVC的WebServerApplicationContext不同的地方在于:它会创建并启动内嵌servlet容器。

Under the hood, Spring Boot uses a different type of ApplicationContext for embedded servlet container support. The ServletWebServerApplicationContext is a special type of WebApplicationContext that bootstraps itself by searching for a single ServletWebServerFactory bean. Usually a TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory has been auto-configured.

创建servlet容器的事情实际是交给ServletWebServerFactory干的,比如TomcatServletWebServerFactory

创建WebServer - ServletWebServerFactory

内嵌servlet容器的抽象是WebServer,比如TomcatServletWebServer

ServletWebServerApplicationContext也是ApplicationContext,所以也有普通ApplicationContext的生命流程:

  1. postProcessBeforeInitialization的时候,会调用ServletWebServerFactoryCustomizer#customize,把程序猿自定义的tomcat的port、context path等属性全都设置到tomcat server里
  2. onRefresh的时候获取ServletWebServerFactory bean(通过autoconfig class自动配置的),用它实例化TomcatWebServer
    1. TomcatWebServer其实就是apache tomcat(org.apache.catalina.startup.Tomcat)的wrapper,所以要先创建一个真正的tomcat。此时connector、service、engine、valve、docBase,全都映入眼帘……废话,这些代码都是tomcat的……host注册到engine上,最后一个tomcat container是Context,注册到host上(servlet的Wrapper这个最底层Container比较特殊,是在start的时候才实例化出来的。其他几个上层Container是一开始就实例化出来的)
    2. 此时还手动注册了springboot的ServletContainerInitializer实现类TomcatStarterContext,见下文。
    3. 创建TomcatWebServer完毕。启动tomcat

启动tomcat

启动tomcat,内部调用的是Server#start。然后就是tomcat lifecycle那一长串的父子container的链式调用start了。

关于tomcat Server,参考(九)How Tomcat Works - Tomcat Service

这一串调用里会涉及到异步启动。但很明显,这个异步启动只是为了让下一级的各个子Container组件并行执行以加快启动时间。之后的阻塞式get()说明了依然是全都执行完毕后才能进行后面的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
        for (Container child : children) {
            results.add(startStopExecutor.submit(new StartChild(child)));
        }

        MultiThrowable multiThrowable = null;

        for (Future<Void> result : results) {
            try {
                result.get();
            } catch (Throwable e) {
                log.error(sm.getString("containerBase.threadedStartFailed"), e);
                if (multiThrowable == null) {
                    multiThrowable = new MultiThrowable();
                }
                multiThrowable.add(e);
            }

        }

调用ServletContainerInitializer

tomcat启动到Context container的时候,就涉及到ServletContainerInitializer的调用了。和SpringMVC的入口一样,springboot亦如此:

1
2
3
4
5
6
7
8
9
10
11
12
            // 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;
                }
            }

会调用所有的ServletContainerInitializer

tomcat的ServletContext的实现就叫org.apache.catalina.core.ApplicationContextApplicationContext implements ServletContext,重名了,就很魔性。getServletContext()获取的就是这个tomcat的ApplicationContext

springboot在创建embed tomcat的时候,就往Context container里手动set了一个ServletContainerInitializer的实现:TomcatStarter

还set了一个websocket相关的initializer,但那是另一回事儿了。所以可以理解为有且只有一个springboot的servlet initializer。所以现在不走SpringMVC那一套了。

ServletContainerInitializer SPI探测去哪了?

虽然springboot手动往embed tomcat只注册了自己的ServletContainerInitializer,它是怎么做到不让embed tomcat探测别人的SPI实现的?

tomcat的WebappServiceLoader会遍历所有的jar包,并从jar里加载文件:

1
uri = new URI("jar:" + baseExternal + "!/" + entryName);

这显然是为了使用SPI机制。

ContextConfig用于配置Context container,它会探测ServletContainerInitializer的SPI实现:

1
detectedScis = loader.load(ServletContainerInitializer.class);

它是默认的Context config类:the default context configuration class for deployed web applications.

在apache Tomcat的main函数里,tomcat在加载web app的时候,自动就注册上这个config类了:

1
tomcat.addWebapp(path, war.getAbsolutePath());

所以正常的tomcat一定会探测ServletContainerInitializer的SPI实现。

springboot启动的是embed tomcat。创建完Tomcat实例之后,手撸了一组tomcat的Container组件,手动配置了Context container,没有使用标准的ContextConfig类配置Context,所以失去了探测ServletContainerInitializer的SPI实现的功能。

springboot启动的是被魔改的tomcat。

因此,springboot就能放心给这个embed tomcat手动设置自己的ServletContainerInitializer实现了。

调用ServletContextInitializer,注册DispatcherServlet

和SpringMVC的原理一样,springboot的ServletContainerInitializer的实现类要调用springboot的ServletContextInitializer初始化servlet容器。ServletWebServerApplicationContext就提供了一个ServletContextInitializer——selfInitialize方法:

1
2
3
4
5
6
7
8
	private void selfInitialize(ServletContext servletContext) throws ServletException {
		prepareWebApplicationContext(servletContext);
		registerApplicationScope(servletContext);
		WebApplicationContextUtils.registerEnvironmentBeans(getBeanFactory(), servletContext);
		for (ServletContextInitializer beans : getServletContextInitializerBeans()) {
			beans.onStartup(servletContext);
		}
	}

它的最后一步,会把servlet bean、filter bean都从ServletWebServerApplicationContext里取出来,用来初始化ServletContext

ServletWebServerApplicationContext里注册的servlet相关的spring bean是各路RegistrationBean

  • FilterRegistrationBean:filter wrapper
  • DispatcherServletRegistrationBeanDispatcherServlet wrapper

DispatcherServletRegistrationBean有个autoconfig class:DispatcherServletAutoConfiguration,所以是启动过程中自动配置的。它创建的时候也创建了DispatcherServlet

这些RegistrationBean有个共同的onStartup方法,把自己注册到ServletContext里

如果加入了spring security支持,还会有DelegatingFilterProxyRegistrationBean

  • DelegatingFilterProxyRegistrationBean:springSecurityFilterChain

一般情况下只会有DispatcherServlet一个servlet(DispatcherServletRegistrationBean),如果还有其他的servlet,比如配置了h2 database web console,还会再产生一个普通的ServletRegistrationBean

  • ServletRegistrationBean:比如WebServlet,url=h2-console,启动了一个h2 web console。

启动TomcatWebServer

启动完tomcat,初始化完servlet container之后,springboot还给BeanFactory注册了启动关闭TomcatWebServer(embed tomcat wrapper)的lifecycle:

1
2
3
4
getBeanFactory().registerSingleton("webServerGracefulShutdown",
		new WebServerGracefulShutdownLifecycle(this.webServer));
getBeanFactory().registerSingleton("webServerStartStop",
		new WebServerStartStopLifecycle(this, this.webServer));

最后ServletWebServerApplicationContext#finishRefresh的时候,调起LifecycleProcessor#onRefresh,启动所有Lifecycle bean的start方法(有点儿像tomcat lifecycle的方式),这个时候就启动TomcatWebServer了!

注意这个时候启动的是TomcatWebServer,不是tomcat。tomcat在创建TomcatWebServer完成之前就启动了。

这个时候会输出那句著名的:

1
2
logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"
		+ getContextPath() + "'");

Tomcat started on port(s): 8081 (http) with context path ‘/wtf’ :D

然后发布TomcatWebServer启动完毕事件。

@ServletComponentScan

servlet提供了标准的@WebServlet/@WebFilter/@WebListener以注册servlet组件,标准tomcat会扫描并注册他们。但是springboot用的是embed tomcat,所以提供了@ServletComponentScan对他们提供支持。在使用standalone tomcat时,该注解不会生效,否则就实例化两份了

@ServletComponentScan has no effect in a standalone container, where the container’s built-in discovery mechanisms are used instead.

感想

第一次试图了解springboot tomcat还是四年前,刚工作一年整,对spring了解一般,springboot更是不了解。当时啥也没探究出来,太惨了……

加油吧菜鸡o(╥﹏╥)o,把前置知识都理解清楚了,一切都水到渠成了。

本文由作者按照 CC BY 4.0 进行授权