SpringBoot是如何集成SpringMVC的

在传统的SpringMVC项目中,我们都会提供一个web.xml文件,用于配置Servlet、Filter、Listener,然后将项目打包成war包通过外部的Servlet容器(如tomcat,jetty)等来启动项目,但是在SpringBoot中提供了内嵌的Servlet容器(默认是tomcat,原生还支持jetty、undertow),接下来我们就来看看SpringBoot是如何操作的,还有我们的web.xml文件去哪里了?

这一切都要从ServletContainerInitializer说起

ServletContainerInitializer

ServletContainerInitializer用于支持以code-based的方式来配置ServletContainer,而不是以往的web.xml-based方式

/**
 * @since Servlet 3.0
 */
public interface ServletContainerInitializer {

    /**
     * Receives notification during startup of a web application of the classes
     * within the web application that matched the criteria defined via the
     * {@link javax.servlet.annotation.HandlesTypes} annotation.
     *
     * @param c     The (possibly null) set of classes that met the specified
     *              criteria
     * @param ctx   The ServletContext of the web application in which the
     *              classes were discovered
     *
     * @throws ServletException If an error occurs
     */
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

ServletContainerInitializers是通过spring-web.jar/META-INF/services/javax.servlet.ServletContainerInitializer文件定义的entry来注册的,该文件必须要定义在该位置并且必须在依赖中包含该jar包,这其实就是我们之前讲的SPI机制,这里是JDK自带的SPI机制,文件名为类全限定名,值为其实现类
image.png
从文件中可以看到该类的实现类是org.springframework.web.SpringServletContainerInitializer

该接口提供了一个onStartup方法,接收两个参数,一个是元素为Class的Set,另一个就是ServletContext,该方法会在WebApplication启动的时候被调用,它的第一个参数是由@HandlesTypes传入

SpringServletContainerInitializer

先看一下这个实现类,从名字可以推断出它是Spring对ServletContainerInitializer的实现,该类主要是用于实例化ServletContext并将其委派给用户自定义的WebApplicationInitializer实现类,然后由WebApplicationInitializer来负责执行对Servlet的初始化操作

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {

	@Override
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

}

onStartup方法执行逻辑分析

  • 当容器启动时首先会找到@HandlesTypes注解中指定类的子类,然后将其子类作为第一个参数传入onStartup方法,然后执行onStartup方法
  • 判断传入的webAppInitializerClasses参数是否为空,如果为空则直接返回
  • 不为空则进入循环,这里会检测收集到的类是否合法,主要是检查是否是WebApplicationInitializer的实现类,因为@HandlesTypes的参数可以是一个数组,检测的项有,
    • 该类不该是一个接口
    • 该类不该是一个抽象类
    • 该类必须能够转换为WebApplicationInitializer,也就是必须是WebApplicationInitializer的实现类
  • 如果满足条件,则将其添加到局部变量initializers中,这是一个元素为WebApplicationInitializer的list
  • 检查initializers是否为空,如果为空,直接返回,不做任何操作
  • 如果不为空,则对initializers进行排序,排序主要是为了让这些类按顺序被实例化
  • 接着遍历initializers,调用其onStartup方法

这个是使用外部Servlet容器时的逻辑,实际上我们编写SpringBoot应用时都是用的SpringBoot内嵌的Servlet容器,因此如果我们以java -jar的方式或者以IDEA的方式来启动一个SpringBoot项目,都不会调用SpringServletContainerInitializer#onStartup

image.png
在这里打了断点后启动应用,如上面的分析,断点在预料之中没有停在这里,而是直接启动了应用

WebApplicationInitializer

一个SPI接口,用于以code-based的方式来配置Servlet,该接口的实现类在Servlet容器启动时会自动被SpringServletContainerIntializer探测到并调用其#onStartup

xml-based

配置Servlet,配置启动参数

<servlet>
  <servlet-name>dispatcher</servlet-name>
  <servlet-class>
    org.springframework.web.servlet.DispatcherServlet
  </servlet-class>
  <init-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/spring/dispatcher-config.xml</param-value>
  </init-param>
  <load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
  <servlet-name>dispatcher</servlet-name>
  <url-pattern>/</url-pattern>
</servlet-mapping>

code-based

public class MyWebAppInitializer implements WebApplicationInitializer {
  
    @Override
    public void onStartup(ServletContext container) {
        XmlWebApplicationContext appContext = new XmlWebApplicationContext();
        appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml");

        ServletRegistration.Dynamic dispatcher =
            container.addServlet("dispatcher", new DispatcherServlet(appContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
    }

}

使用ServletRegistration.Dynamic,我们可以动态注册Servlet到Servlet容器中

ServletRegistration
public interface ServletRegistration extends Registration {

    public Set<String> addMapping(String... urlPatterns);

    public Collection<String> getMappings();

    public String getRunAsRole();

    public static interface Dynamic
    extends ServletRegistration, Registration.Dynamic {
        public void setLoadOnStartup(int loadOnStartup);
        public Set<String> setServletSecurity(ServletSecurityElement constraint);
        public void setMultipartConfig(MultipartConfigElement multipartConfig);
        public void setRunAsRole(String roleName);
    }
}

一个用于注册Servlet的接口,继承自Registration接口

Registration

该接口是一个通用接口,用于注册Filter和Servlet

public interface Registration {

    public String getName();

    public String getClassName();

    /**
     * 添加初始化参数
     */
    public boolean setInitParameter(String name, String value);

    /**
     * 获取初始化参数的值
     */
    public String getInitParameter(String name);

    /**
     * 批量添加初始化参数
     */
    public Set<String> setInitParameters(Map<String,String> initParameters);

    /**
     * 批量获取初始化参数的值
     */
    public Map<String, String> getInitParameters();

    public interface Dynamic extends Registration {

        /**
         * 让Servlet和Filter拥有异步的能力
         */
        public void setAsyncSupported(boolean isAsyncSupported);
    }
}
FilterRegistration

用于注册Filter

除了实现WebApplicationInitializer接口,还可以继承AbstractAnnotationConfigDispatcherServletInitializer类