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(...) // ----------------------------------------------------------------------------------------------- privateCollection getSpringFactoriesInstances(Class type) { return getSpringFactoriesInstances(type, new Class [] {}); } private Collection getSpringFactoriesInstances(Class type, Class [] parameterTypes, Object... args) { ClassLoader classLoader = getClassLoader(); // Use names and ensure unique to protect against duplicates Set names = new LinkedHashSet<>( SpringFactoriesLoader.loadFactoryNames(type, classLoader)); List 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 ListloadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); } private static Map > loadSpringFactories(@Nullable ClassLoader classLoader) { try { Enumeration 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
文件中的内容。
这是非常关键的一点,自动配置便从这里开始了。
Spring Boot AutoConfigure
Spring Boot 中的自动配置很大一部分是通过 spring-boot-autoconfigure
完成的,SpringApplication 启动时会解析它的 META-INF/spring.factories
文件:
# Initializersorg.springframework.context.ApplicationContextInitializer=\org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener# Application Listenersorg.springframework.context.ApplicationListener=\org.springframework.boot.autoconfigure.BackgroundPreinitializer# Auto Configuration Import Listenersorg.springframework.boot.autoconfigure.AutoConfigurationImportListener=\org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener# Auto Configuration Import Filtersorg.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\org.springframework.boot.autoconfigure.condition.OnBeanCondition,\org.springframework.boot.autoconfigure.condition.OnClassCondition,\org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition# Auto Configureorg.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
方法 ClassLoader.getResources
会获取所有 类路径 下的指定的资源文件