Spring Boot 是如何自动集成 Web 环境的

858 阅读8分钟

使用 Spring Boot 集成 Web 环境是很方便的,只要在依赖中加入如下依赖,就默认使用 Tomcat 作为 servlet 容器,不需要额外的代码。这个特性极大的方便了 Web 项目的开发。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

那 Spring Boot 是如何自动完成 servlet 容器初始化的呢?这个问题的关键点在于两点,第一点是 servlet 容器是怎么初始化的,第二点是 Spring Boot 是如何自动完成这个过程的。特别是第二点,涉及到 Spring Boot 的自动装配机制。

Servlet 容器初始化流程

Spring Boot 应用在启动的时候,会判断当前应用类型。有 NONE,SERVLET 和 REACTIVE 三种。NONE 表示普通的 JAVA 程序,不涉及 web 容器。SERVLET 表示使用 servlet 作为 web 容器,我们一般使用的就是这种类型。REACTIVE 是在 Spring 5 之后新增的一种应用类型,也是一种 web 框架,但并不是基于 servlet,而是基于 reactive。

如何判断当前应用是哪种类型呢?不同的应用类型引入的 jar 包是不一样的,SERVLET 引入的是 spring-boot-starter-web,REACTIVE 引入的是 spring-boot-starter-webflux。 Spring Boot 通过判断当前的应用中是否存在特定的类来确定应用类型

deduceFromClasspath 方法用于判断应用类型。如下代码列出几个特定的类,比如使用 webflux 的时候,一定会存在 org.springframework.web.reactive.DispatcherHandler,一定不存在 org.springframework.web.servlet.DispatcherServlet。

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
    "org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

static WebApplicationType deduceFromClasspath() {
  if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
      && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
    return WebApplicationType.REACTIVE;
  }
  for (String className : SERVLET_INDICATOR_CLASSES) {
    if (!ClassUtils.isPresent(className, null)) {
      return WebApplicationType.NONE;
    }
  }
  return WebApplicationType.SERVLET;
}

方法中使用 ClassUtils.isPresent() 方法来判断是否存在某个类,该方法内部最终调用的是 Class.forName() 方法。lass.forName(xxx.xx.xx) 的作用就是要求 JVM 查找并加载指定的类。

有了应用类型后,就能根据应用类型生成特定的 ApplicationContext,这些 ApplicationContext 的不同之处体现在执行上下文刷新时候操作不同。我们以 SERVLET 类型的应用为例,该类型的应用使用 AnnotationConfigServletWebServerApplicationContext 作为应用上下文。

ApplicationContextFactory DEFAULT = (webApplicationType) -> {
  try {
    switch (webApplicationType) {
    case SERVLET:
      return new AnnotationConfigServletWebServerApplicationContext();
    case REACTIVE:
      return new AnnotationConfigReactiveWebServerApplicationContext();
    default:
      return new AnnotationConfigApplicationContext();
    }
  }
};

ApplicationContext 初始化完毕后,会刷新上下文,也就是执行最关键的 refresh() 方法。这个阶段 Spring 会获得 BeanFactory,由 BeanFactory 生成 Bean,我们忽略中间复杂的流程,refresh() 方法中有一个方法叫做 onRefresh(),这个方法就是不同类型 ApplicationContext 实现个性化的扩展的地方。如下是 ServletWebServerApplicationContext(AnnotationConfigServletWebServerApplicationContext 继承了这个类) 的 onRefresh 方法。

可以看出,onRefresh 中方法最重要的就是创建服务器。

@Override
protected void onRefresh() {
  // super 并没有做任何事
  super.onRefresh();
  // 创建 web server
  try {
    createWebServer();
  }
  catch (Throwable ex) {
    throw new ApplicationContextException("Unable to start web server", ex);
  }
}

创建服务器的流程是这样的:WebServerFactory -> WebServer -> initPropertySources。这也是 Spring 一贯的套路,首先获得工厂类,然后通过工厂类来完成 Bean 的创建。

private void createWebServer() {
  ...
  if (webServer == null && servletContext == null) {
    ...
    // 创建 WebServerFactory
    ServletWebServerFactory factory = getWebServerFactory();

    // 通过 WebServerFactory 创建 WebServer
    this.webServer = factory.getWebServer(getSelfInitializer());
    createWebServer.end();
    ...
  }
  ...
  // WebServer 初始化配置
  initPropertySources();
}

获得工厂类的方式很简单,直接从当前 Spring 容器中获得类型为 ServletWebServerFactory 的 Bean 名称。这里有一个问题就是,为什么 Spring 容器中会有类型为 ServletWebServerFactory 的 Bean,先放一放,后续详细分析。

总之,这里会从 Spring 容器中获得一个 TomcatServletWebServerFactory 的对象。这个对象不仅包含了创建 TomcatSercer 的方法,还包含了用户的一些配置参数。有了这些参数才能正确的创建 Web 服务。

protected ServletWebServerFactory getWebServerFactory() {
  // Use bean names so that we don't consider the hierarchy
  String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
  ...
  return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}

调用该对象的 getWebServer 方法。该方法中创建了一个 Tomcat 对象。并启动该 Tomcat 实例。

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
  if (this.disableMBeanRegistry) {
    Registry.disableRegistry();
  }
  // 创建 Tomcat 对象
  Tomcat tomcat = new Tomcat();
  // 省略对 Tomcat 属性赋默认值的代码
  ...

  // 启动 Tomcat,返回一个 TomcatWebServer
  return getTomcatWebServer(tomcat);
}

Spring Boot 中内置了 Tomcat 的代码,其实不止 Tomcat,它还内置了 jetty,netty 和 Undertow 的代码。只不过默认使用 tomcat。

总结一下 Web 环境启动过程,Spring Boot 根据当前引入的 jar 包(特定类)判断应用类型,根据应用类型创建不同的 ApplicationContext,应用刷新上下文时,不同的 ApplicationContext 会有不同的行为,比如 SERVLET 类型的应用就是创建一个 WebServer。Spring Boot 会直接从当前容器中获得类型为 ServletWebServerFactory 的 Bean,作为 WebServer 的工厂类,然后调用该类生成一个 WebServer,最后启动这个 Server。

Spring Boot 自动装配特性

现在还有一个问题就是,ServletWebServerFactory 的实现类是如何加入容器的?为什么会是 TomcatServletWebServerFactory?

这就涉及到 Spring Boot 的自动装配特性。也就是 @EnableAutoConfiguration 注解。该注解集成在了 @SpringBootApplication 注解中,每个 Spring Boot 应用都会申明。

该注解中需要关注 @Import(AutoConfigurationImportSelector.class)。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
  ...
}

我们之前在Spring Boot 是如何解析配置类的一文中详细的分析了配置类的执行阶段,以及解析流程。@Import 注解就是在解析配置类的过程中生效的。

之前讲过,@Import 有三种方式,其中有一种就是 @Import 一个 ImportSelector。其实 ImportSelector 还有一个子类是 DeferredImportSelector,实现了这个类表示这些导入的 Bean 需要延后处理,本质上是执行的时机不同。

AutoConfigurationImportSelector 就是一个 DeferredImportSelector。忽略该类的调用流程,直接看 AutoConfigurationImportSelector 最终为容器中加入了怎样的类。如下是核心代码。

@Override
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 使用 SPI 的方式获得 EnableAutoConfiguration 实现类
  List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  // 去重
  configurations = removeDuplicates(configurations);
  // 从注解的 exclude/excludeName 属性中获取排除项
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  ...
  return new AutoConfigurationEntry(configurations, exclusions);
}

Spring Boot 使用 SPI 的方式获得所有包下的 META-INF/spring.factories 文件,并读取其中的 EnableAutoConfiguration 实现类。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
      getBeanClassLoader());
  return configurations;
}

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
  return EnableAutoConfiguration.class;
}

在 spring-boot-autoconfigure jar 包下,就定义了 129 个实现类(自动配置类基本都在这里),我随便列举几个。其它的包下还有一些调试会用到的,不关键,就不列举了。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
...

这写类并不会全部加载到容器中,除了需要去重,根据注解传参排除一些外,还会在加载之前进行一次过滤,负责过滤的类也是通过 SPI 的方式加载到 Spring 中的。这部分就不赘述了。总之,此时我们获得了很多 EnableAutoConfiguration 的实现类,这些类会被 Import 到 Spring 容器中,它们就是实现自动装配的而基础。

EnableAutoConfiguration 的实现中有一个是 ServletWebServerFactoryAutoConfiguration,是负责集成 Web 环境的,这个类也是一个配置类,这种情况下会进行递归 Import。

ServletWebServerFactoryAutoConfiguration 中再次使用 @Import 导入一些类。我们关注 EmbeddedTomcat,EmbeddedJetty 和 EmbeddedUndertow。这三个配置类会分别检测 classpath 上存在的类,从而判断当前应用使用的 WebServer 的类型,从而决定加载哪一个 WebServer 的工厂类。

@Configuration(proxyBeanMethods = false)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
// ServletRequest 存在时才生效
@ConditionalOnClass(ServletRequest.class)
// 应用类型是 SERVLET 时才生效
@ConditionalOnWebApplication(type = Type.SERVLET)
// 前缀为 server 的配置参数加载到 bean ServerProperties
// 确保 server.port 等配置正确 
@EnableConfigurationProperties(ServerProperties.class)
// 判断 WebServer 类型,定义加载 Web 服务器的工厂 Bean。
@Import({ ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,
		ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,
		ServletWebServerFactoryConfiguration.EmbeddedJetty.class,
		ServletWebServerFactoryConfiguration.EmbeddedUndertow.class })
public class ServletWebServerFactoryAutoConfiguration {
  ...
}

以 EmbeddedTomcat 为例,它是 ServletWebServerFactoryConfiguration 的内部类,这里规定了 EmbeddedTomcat 存在的一些条件,以及如果 EmbeddedTomcat 符合条件,那么工厂类就是 TomcatServletWebServerFactory。

@Configuration(proxyBeanMethods = false)
class ServletWebServerFactoryConfiguration {

	@Configuration(proxyBeanMethods = false)
	@ConditionalOnClass({ Servlet.class, Tomcat.class, UpgradeProtocol.class })
	@ConditionalOnMissingBean(value = ServletWebServerFactory.class, search = SearchStrategy.CURRENT)
	static class EmbeddedTomcat {

		@Bean
		TomcatServletWebServerFactory tomcatServletWebServerFactory(
				ObjectProvider<TomcatConnectorCustomizer> connectorCustomizers,
				ObjectProvider<TomcatContextCustomizer> contextCustomizers,
				ObjectProvider<TomcatProtocolHandlerCustomizer<?>> protocolHandlerCustomizers) {
			...
			return factory;
		}

	}
  ...
}

饶了这么大一大圈,TomcatServletWebServerFactory 终于加载到容器中了,后续的流程正好就接上了第一部分的内容。以点见面,Spring Boot 自动装配的底层原理其实就是判断当前 classpath 是否存在特定类。我们只是替换了一个 jar 包,Spring 帮我们干了很多的脏活累活:根据条件加载不同的配置类,工厂类,生成最终的 Bean,将配置属性赋给 Bean 等等。少让用户写代码是 Spring Boot 能火起来的一个重要原因。

总结

  1. Spring Boot 根据当前引入的 jar 包(特定类)判断应用类型,根据应用类型创建不同的 ApplicationContext,应用刷新上下文时,Spring Boot 会直接从当前容器中获得类型为 ServletWebServerFactory 的 Bean,作为 WebServer 的工厂类,然后调用该类生成一个 WebServer,最后启动这个 Server。

  2. Spring Boot 自动装配特性极大的减少了配置量。自动装配的原理是:Spring Boot 通过 SPI 的方式加载 EnableAutoConfiguration 的实现类,这些类也是一些配置类,它们会通过判断当前 classpath 是否存在特定类,在容器中加载不同的 Bean。除了集成 Web 环境外,该特性还可以用于集成 elasticsearch,mongo, neo4j,oauth2 等等。

  3. Spring Boot 自定集成 Web 环境践行了 Spring Boot 约定大于配置 的理念。Spring Boot 通过对加载路径,加载方式的约定,实现了对 Spring 容器的灵活控制。也实现了很多高级功能,比如自动装配。我们在阅读 Spring Boot 源码的过程中,可以不看细节,但是要理解 Spring Boot 到底是如何约定的,以及实现了怎样的功能,这样才能对整体框架有一个清晰的认识。

如果您觉得有所收获,就请点个赞吧!