Table of Contents
前言
在学习了 Spring Framework 一段时间后我便开始了 Spring Boot 的学习,在写下一段简单的代码后,我的程序成功的运行了起来。
但随之而来的不是其简便带来的喜悦,而是是对其原理的困惑与慌张,此时,Spring Boot 在手里就像是一个脱离了自己控制的工具,它存在了一部分在你控制之外的行为,而且你还不知道这样的行为是如何发生的。
我不喜欢这样的感觉,于是我便去查询了相关的资料,解答了自己的疑问。这样一来,脱离了自己控制的那一部分,仿佛又回到了自己的控制之中。
SpringApplication
通常情况下,我们的应用程序可以通过 SpringApplication.run(Application.class)
启动,所以,我们可以来看一下这个方法到底干了什么:
public class SpringApplication {
// -----------------------------------------------------------------------------------------------
// SpringApplication.run(...)
// -----------------------------------------------------------------------------------------------
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
// -----------------------------------------------------------------------------------------------
// SpringApplication(...)
// -----------------------------------------------------------------------------------------------
public SpringApplication(Class<?>... primarySources) {
this(null, primarySources);
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
// -----------------------------------------------------------------------------------------------
// SpringApplication.getSpringFactoriesInstances(...)
// -----------------------------------------------------------------------------------------------
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
}
复制代码
上面的代码是和 Spring Boot 自动配置密切相关的部分代码,我们可以看到,当我们调用 SpringApplication.run(...)
方法后会构造一个 SpringApplication
实例,而在构造这个实例的过程中,我们会调用 getSpringFactoriesInstances
方法。
这个方法是实现自动配置过程中非常重要的一个方法,这个方法内部调用了 SpringFactoriesLoader.loadFactoryNames
方法,我们可以看一下 SpringFactoriesLoader
做了什么:
public final class SpringFactoriesLoader {
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
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);
}
}
}
复制代码
SpringFactoriesLoader
是 Spring Framework 提供的一个工具类,调用这个类的 loadFactoryNames
方法后,会调用 loadSpringFactories
方法,这个方法内部会通过 ClassLoader.getResources
方法获取所有的 META-INF/spring.factories
文件中的内容1。
这是非常关键的一点,自动配置便从这里开始了。
Spring Boot AutoConfigure
Spring Boot 中的自动配置很大一部分是通过 spring-boot-autoconfigure
完成的,SpringApplication 启动时会解析它的 META-INF/spring.factories
文件:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
......
复制代码
可以看到,这个文件中声明了很多的类,也就是说,自动配置就是通过这些类来完成的。我们可以来看一下这些类做了什么:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";
@Configuration
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties({ HttpProperties.class, WebMvcProperties.class })
protected static class DispatcherServletConfiguration {
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet() {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
return dispatcherServlet;
}
}
}
复制代码
上面这个类是经过极大简化的 DispatcherServletAutoConfiguration
类,可以看到,这个类内部声明配置了 DispatcherServlet
, 通常情况下,在使用 Spring MVC 的过程中,这是由我们自己完成的,但是在这里,自动配置帮助我们完成了这一步骤。
除了 DispatcherServletAutoConfiguration 以外,还有很多类似的配置类,这些类利用了一些很有用的 Spring 特性来完成整个配置过程。
条件注解
Spring Boot 的自动配置类使用了大量的条件注解,通过这些条件注解,Spring Boot 很好的完成了配置工作,同时,因为存在条件注解的原因,我们可以覆盖 Spring Boot 的默认配置。
条件注解有很多,这篇博客也不是专门介绍这方面的博客,因此这里简单列举三个在自动配置中大量使用的注解:
条件注解 | 作用 |
---|---|
@ConditionalOnClass | 仅在存在某个 Class 的情况下生效 |
@ConditionalOnBean | 仅在存在某个 Bean 的情况下生效 |
@ConditionalOnMissingBean | 仅在不存在每个 Bean 的情况下生效 |
其中,通过 @ConditionalOnClass 注解,我们便可以通过引入 不同依赖 的方式应用不同的配置,比如,当类路径下存在 jackson
的情况下,Spring 可以自动完成 jackson
的相关配置。
同时,通过 @ConditionalOnBean 可以保证配置依赖的 Bean 存在时才进行下一步的配置,而 @ConditionalOnMissingBean
正好与之相反,它要求相应的 Bean 不存在才执行下一步的配置。
这意味着,我们可以自己配置某种 Bean,由于 Spring Boot 会先执行我们的配置,因此,当我们配置的 Bean 存在后,相应的默认配置就不会再执行。而且,还不会影响后序的配置过程。
于是,我们便可以通过定义自己的 Bean 的方式覆盖默认的配置。
WebApplicationType
Spring Boot 自动配置中还存在非常重要的一环,那就是 WebApplicationType 的判断,这通过 WebApplicationType.deduceFromClasspath()
完成:
public enum WebApplicationType {
NONE, SERVLET, REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
"org.springframework.web.context.ConfigurableWebApplicationContext" };
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 JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";
private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";
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;
}
}
复制代码
这是该枚举的部分代码,可以看到,它是通过判断某几个类是否存在来判断 WebApplicationType 的。
一开始对于 WebApplicationType 我是不太在意的,后来在使用 Spring Boot 的过程中发现自己的程序启动后瞬间就退出才开始关注这一问题,我发现原因是 tomcat
的依赖被设置为了 provided
, 这使得程序直接编译运行后在类路径下找不到相关的类,因此 Spring Boot 就将 WebApplicationType 判断为了 NONE,于是便没有启动 tomcat server
.
在解决这个问题的过程中,发现虽然自动配置主要是由 spring-boot-autoconfigure 完成的,但还是有一部分配置在其他的 Spring Boot 组件中。
@SpringBootApplication
这里主要是想介绍一下 @SpringBootApplication 包含的 @ComponentScan 注解。
由于我学习 Spring 是通过《Spring 实战》这本书来学习的,它并没有详细介绍 @ComponentScan 的作用,在尝试后我才发现:
- @ComponentScan 默认会自动扫描当前包及当前包的所有子包中的 Bean
- @ComponentScan 如果扫描到 @Configuration,那么就会自动导入相关的配置
这也许就是为什么 Spring Boot 应用程序往往将 Application.java 放在各个子包外面的原因,通过这种方式,就可以自动完成所有子包中的配置和 Bean 的扫描了。
简单总结
到了这里就可以简单总结一下 Spring Boot 的自动配置原理了:
- SpringApplication 启动时会检测当前的运行环境,得到 WebApplicationType
- SpringApplication 启动时会解析文件
spring-boot-autoconfigure/META-INF/spring.factories
中的内容,得到相关的自动配置实现类 - SpringApplication 会先通过组件扫描导入你自定义的配置,这些配置可以覆盖默认配置
- SpringApplication 会通过条件注解应用相关自动配置实现类中定义的配置
也就是说,Spring Boot 中的自动配置更像是由 Spring Boot 开发人员预定义好的配置,我们也可以借助相应的方式实现自己的自动配置。
结语
在某种程度上,我是被 自动配置 这个名字给唬住了,在了解其原理后,发现它的原理并没有那么复杂,某种程度上来说,还很朴素 @_@
当然了,自动配置的详细流程肯定比这篇博客中的流程复杂的多,但是,在掌握了基本的原理以后,我们便可以干预这个流程(虽然不掌握也同样可以),从而再次将脱离了自己控制的那一部分代码,再次掌握在自己手里。
Footnotes
1 方法 ClassLoader.getResources
会获取所有 类路径 下的指定的资源文件