记得在Spring+SpringMVC的日子里,创建一个spring的web项目,必不可少的配置就是在WEB-INF目录下添加web.xml。为什么在SpringBoot项目中,web.xml消失的无影无踪呢?

我的观点是:如果使用外部的tomcat,SpringBoot会通过ServletContainerInitializerApplicationContext应用上下文创建之前就生成ServletContext,但是servlet配置的注册是由DispatcherServletRegistrationBean来完成的。而如果使用的内嵌tomcat,就跟ServletContainerInitializer没有任何关系了。

网络上很多的观点是ServletContainerInitializer让SpringBoot成功初始化,我觉得他们可能粗略地跟着其他人的解析粗略看了一下源码,就草草地得了一个结论。

源码 SpringBoot的版本是 2.2.2.RELEASE 、Spring版本是5.2.2.RELEASE 。

如果观点有误,请指正!!!

了解web.xml

web.xml是什么

Servlet规范要求Web容器支持部署描述文件(web.xml)。Web容器使用部署描述文件(Deployment Descriptor,DD)初始化Web应用程序的组件。

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.4" 
    xmlns="http://java.sun.com/xml/ns/j2ee" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 
    http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
  <display-name>javaWeb</display-name> 
  <filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>web.filter.CharacterEncodingFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
  <servlet>
    <servlet-name>UserServlet</servlet-name>
    <servlet-class>cn.uhfun.demo.UserServlet</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>UserServlet</servlet-name>
    <url-pattern>/user/userServlet</url-pattern>
  </servlet-mapping>
  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
  ...
</web-app>

例如上面的配置,制定了web应用的名称、过滤器(和过滤器class关联,以及指定过滤url)、servlet(和实现类关联,servlet映射)、欢迎页等。

Tomcat容器在部署web应用时,会在初始化阶段加载web.xml文件,从而加载servlet及其映射,最终能够对外提供服务。

SpringMVC中的web.xml

在普通的Java web应用中,我们需要开发很多的Servlet来处理不同的请求。而SpringMVC的核心思想是设计一个功能强大的Servlet(聚合了Spring容器的各种能力)作为前端控制器,协调组织不同的组件,完成请求处理和返回响应。

DispatcherServlet是一个继承自FrameworkServlet的servlet,在源码中它是这么描述的

/*
* Central dispatcher for HTTP request handlers/controllers, e.g. for web UI controllers
* or HTTP-based remote service exporters. Dispatches to registered handlers for processing
* a web request, providing convenient mapping and exception handling facilities.
 
* 用于HTTP请求处理程序/控制器的中央调度器,例如用于Web UI控制器或基于HTTP的远程服务导出器。
* 调度到注册的处理程序以处理Web请求,从而提供方便的映射和异常处理工具。
*/

简单理解就是,它可以让我们以更加灵活便捷的方式来编写请求处理器,例如我们常见的通过注解的方式来编写处理器@controller、@RequestMapping等

因此它的web.xml中就出现了DispatcherServlet的字样,并且你不再需要其他的Servlet(关于DispatcherServlet的实现细节我会放在后面的文章中重点介绍):

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns="http://java.sun.com/xml/ns/javaee"
        xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
              http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    id="WebApp_ID" version="3.0">
    <display-name>Spring MVC App</display-name>
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>characterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <servlet>
        <servlet-name>MainDispatcherServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>MainDispatcherServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

SpringBoot中的DispatcherServlet

既然没有了web.xml,那SpringBoot中的DispatcherServlet是如何被加载的呢?

首先,SpringBoot提供了两种启动的方式,这两种方式在注册serlvet的配置时有所不同

SpringBoot启动方式

  1. 使用内嵌web容器,这是比较常见的一种方式

    @SpringBootApplication
    public class DemoSpringmvcApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoSpringmvcApplication.class, args);
        }
    }
    
  2. 使用外部web容器

    @SpringBootApplication
    public class DemoSpringmvcApplication extends SpringBootServletInitializer {
        @Override
        protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
            return builder.sources(DemoSpringmvcApplication.class);
        }
    }
    

SpringBoot自动加载配置原理简析

我们需要先来简单了解一下SpringBoot的加载原理

SpringBoot的启动

通常SpringBoot项目都有一个启动类

@SpringBootApplication
public class DemoSpringmvcApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoSpringmvcApplication.class, args);
    }
}

SpringApplication的静态方法run会将启动类的class作为参数,构建一个SpringApplication的实例,然后调用实例的run方法

创建上下文

public class SpringApplication {
  ...
  public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args);
  } 
  ...
  public ConfigurableApplicationContext run(String... args) {
    ...
		try {
			...
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			...
			}
			...
		}
		...
	}
  ...
  private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);
		...
	}
  ...
  protected void refresh(ApplicationContext applicationContext) {
		Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
		((AbstractApplicationContext) applicationContext).refresh();
	}
}

调用内部的protected方法 createApplicationContext(),因为webApplicationTypeSERVLET,通过反射创建了org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext的实例

protected ConfigurableApplicationContext createApplicationContext() {
   Class<?> contextClass = this.applicationContextClass;
   if (contextClass == null) {
      try {
         switch (this.webApplicationType) {
         case SERVLET:
            contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
            break;
         case REACTIVE:
            contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
            break;
         default:
            contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
         }
      }
      ...
   }
   return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}

注意!!!在构建实例时,对其中的reader和scanner进行了初始化

public AnnotationConfigServletWebServerApplicationContext() {
   this.reader = new AnnotatedBeanDefinitionReader(this);
   this.scanner = new ClassPathBeanDefinitionScanner(this);
}
注册ConfigurationClassPostProcessor的BeanDefinition到容器中

在AnnotatedBeanDefinitionReader构造方法中,会调用AnnotationConfigUtils的方法将ConfigurationClassPostProcessor处理器的beanDefinition添加到容器中

public AnnotatedBeanDefinitionReader(BeanDefinitionRegistry registry, Environment environment) {
   ...
   AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
}
public abstract class AnnotationConfigUtils {
  public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
        BeanDefinitionRegistry registry, @Nullable Object source) {
     ...
     Set<BeanDefinitionHolder> beanDefs = new LinkedHashSet<>(8);
     if (!registry.containsBeanDefinition(CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME)) {
        RootBeanDefinition def = new RootBeanDefinition(ConfigurationClassPostProcessor.class);
        def.setSource(source);
        beanDefs.add(registerPostProcessor(registry, def, CONFIGURATION_ANNOTATION_PROCESSOR_BEAN_NAME));
     }
     ...
	}
}

刷新应用上下文,invokeBeanFactoryPostProcessors

创建完应用上下文,这时我们再回到SpringApplication的非静态run方法中的refreshContext(context);

它进入AbstractApplicationContextrefresh方法,接下里会调用invokeBeanFactoryPostProcessors方法,执行BeanFactoryPostProcessor中的处理方法,因为ConfigurationClassParser的beanDefinition已经在容器中了,所以会被调用

public abstract class AbstractApplicationContext extends DefaultResourceLoader
		implements ConfigurableApplicationContext {
		@Override
    public void refresh() throws BeansException, IllegalStateException {
       synchronized (this.startupShutdownMonitor) {
          // 为刷新做准备,例如
          // 初始化上下文环境中的所有占位符属性源;验证是否所有标记为必需的属性都是可解析的
          prepareRefresh();
          // 获得子类中刷新的Bean工厂
          ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
          // 准备好在此上下文中使用的bean工厂
          prepareBeanFactory(beanFactory);
          try {
             // 允许在上下文子类中对bean工厂进行后处理
             postProcessBeanFactory(beanFactory);
            // 调用上下文中注册为bean的工厂处理器
            invokeBeanFactoryPostProcessors(beanFactory);
             ...
          }
          ...
       }
    }		
    protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
      PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
      ...
    }
}

Import AutoConfigurationImportSelector

解析启动类上的@SpringBootApplication、@EnableAutoConfiguration间接引入的@Import(AutoConfigurationImportSelector.class)

public Iterable<Group.Entry> getImports() {
  for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
    this.group.process(deferredImport.getConfigurationClass().getMetadata(),
                       deferredImport.getImportSelector());
  }
  return this.group.selectImports();
}

AutoConfigurationImportSelector找到其他Configuration并自动加载

AutoConfigurationImportSelector通过调用SpringFactoriesLoader的loadFactoryNames方法,找到候选的其他配置类

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
   ...
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
   configurations = removeDuplicates(configurations);
   ...
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         getBeanClassLoader());
  ...
}

loadFactoryNames方法会扫描所有jar包下的META-INF/spring.factories的配置

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryTypeName, factoryImplementationName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}

DispatcherServletAutoConfiguration生效

在spring-boot-autoconfigure-2.2.2.RELEASE.jar的META-INF/spring.factories中存在关于DispatcherServlet的自动配置

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
...
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
...

DispatcherServletAutoConfiguration配置类生效

DispatcherServletAutoConfiguration中有两个配置类

  • DispatcherServletConfiguration

    通过@Bean注册DispatcherServlet的bean对象t到spring容器中

  • DispatcherServletRegistrationConfiguration

    通过@Bean注册DispatcherServletRegistrationBean`的bean对象到spring容器中

DispatcherServletRegistrationBean是什么?

它实现了ServletContextInitializer,下面是ServletContextInitializer源码的描述

/* 
* Interface used to configure a Servlet 3.0+ {@link ServletContext context}
* programmatically. Unlike {@link WebApplicationInitializer}, classes that implement this
* interface (and do not implement {@link WebApplicationInitializer}) will <b>not</b> be
* detected by {@link SpringServletContainerInitializer} and hence will not be
* automatically bootstrapped by the Servlet container.
* <p>
* This interface is designed to act in a similar way to
* {@link ServletContainerInitializer}, but have a lifecycle that's managed by Spring and
* not the Servlet container.

* 用于以编程方式配置Servlet 3.0+{@link ServletContext Context}的接口。
* {@link WebApplicationInitializer}不同,
* 实现此接口(且不实现{@link WebApplicationInitializer})的类不会被{@link SpringServletContainerInitializer}检测到,
* 因此不会由Servlet容器自动引导。此接口的设计方式与{@link ServletContainerInitializer}类似,但其生命周期由Spring管理,而不是Servlet容器

注册servlet的配置

applicationContextonRefresh阶段会执行ServletContextInitializer.onStartup

SpringBoot根据内嵌还是外部的web容器有不同的操作

如果webServer == null && servletContext == nulltrue则使用内嵌的tomcat

使用外部web容器时直接调用 ServletContextInitializer的onStartup方法,最终调用的其实就是前面注册到容器中的DispatcherServletRegistrationBean

而使用内嵌容器,ServletContextInitializer会在获取内嵌web容器,然后容器启动后被启动(调用onStarup方法)

public class ServletWebServerApplicationContext extends GenericWebApplicationContext
		implements ConfigurableWebServerApplicationContext {
		...
		@Override
    protected void onRefresh() {
       super.onRefresh();
       try {
          createWebServer();
       }
       catch (Throwable ex) {
          throw new ApplicationContextException("Unable to start web server", ex);
       }
    }		
    ...
    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();
    }
		...
}

为什么外部web容器启动时servletContext不为null

首先这里需要先提到一个接口ServletContainerInitializer

public interface ServletContainerInitializer {  
    void onStartup(Set<Class<?>> c, ServletContext ctx) throws ServletException;
}

Servlet3.0提供了这个接口,在web容器启动时可以让第三方组件机做一些初始化的工作

ServletContainerInitializer(SCI)通过文件META-INF/services/javax.servlet.ServletContainerInitializer中的条目注册,该条目必须包含在包含SCI实现的JAR文件中。

SCI类中的{@link javax.servlet.nortation.HandlesTypes}注解可以作为参数

可以查看依赖 Maven:org.springframework:spring-web:5.2.2.RELEASE 的META-INF/services 中有一个javax.servlet.ServletContainerInitializer文件,它指定了一个ServletContainerInitializer实现类

org.springframework.web.SpringServletContainerInitializer

查看SpringServletContainerInitializer源码可以发现,web容器启动后,所有继承自WebApplicationInitializer的类都会启动

@HandlesTypes(WebApplicationInitializer.class)
public class SpringServletContainerInitializer implements ServletContainerInitializer {
	public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
      throws ServletException {
     List<WebApplicationInitializer> initializers = new LinkedList<>();
     if (webAppInitializerClasses != null) {
        for (Class<?> waiClass : webAppInitializerClasses) {
           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);
              }
           }
        }
     }
     ...
     for (WebApplicationInitializer initializer : initializers) {
        initializer.onStartup(servletContext);
     }
  }
}

因为我们自定义启动类 DemoSpringmvcApplication 继承自SpringBootServletInitializer(SpringBootServletInitializer继承自WebApplicationInitializer)所以会启动

SpringBootServletInitializer启动后,SpringApplicationBuilder设置了initializers,new了一个ServletContextApplicationContextInitializer,里面包含了外部web容器的servletContext。

protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
   SpringApplicationBuilder builder = createSpringApplicationBuilder();
	 ...
   builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
   ...
   return run(application);
}

在前面提到的springApplication.run方法中的prepareContext(context, environment, listeners, applicationArguments, printedBanner);

当运行到这里时,会执行初始化器的应用,应用后servletContext就被集成到了applicationContext

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
      SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
   context.setEnvironment(environment);
   postProcessApplicationContext(context);
   applyInitializers(context);
   ...
}

所以可以通过在applicationContext onRefresh阶段servletContext是否为空来判断是何种方式启动的

总结

SpringBoot有一个关于DispatcherServlet的配置类,DispatcherServlet和DispatcherServletRegistrationBean作为bean注册到了容器中,而servlet配置注册的关键是DispatcherServletRegistrationBean(实现了ServletContextInitializer接口)调用onStarup方法。