SpringBoot源码解析(一)
不知道大家在第一次搭建SpringBoot项目时是否有注意到,没有繁琐的配置文件,引入依赖时也不需要指定版本。整合第三方框架,如果需要开启相应的功能,只需要在启动类上添加@Enablexxxx注解即可实现效果。项目启动更是简单,只需运行启动类中的main方法,我们的SpringBoot项目就跑起来了。
版本依赖
先回答第一个问题,SpringBoot是如何做到不指定版本也能找到合适的版本号。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.2.RELEASE</version> <relativePath/> </parent>123456复制代码类型:[java]
打开pom文件,我们可以看到有个parent标签,这时候我们按住ctrl+鼠标左键点击spring-boot-starter-parent,会看到以下内容。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.2.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath> </parent>123456复制代码类型:[java]
我们发现,该文件中并没有关于版本号的踪迹。莫慌,它还有个parent,继续按住ctrl+鼠标左键点击spring-boot-dependencies进入,找到properties标签,这里定义了一系列的版本。
<properties> <activemq.version>5.15.11</activemq.version> <antlr2.version>2.7.7</antlr2.version> <appengine-sdk.version>1.9.77</appengine-sdk.version> <artemis.version>2.10.1</artemis.version> // 此处省略若干版本号... <wsdl4j.version>1.6.3</wsdl4j.version> <xml-maven-plugin.version>1.0.2</xml-maven-plugin.version> <xmlunit2.version>2.6.3</xmlunit2.version> </properties>12345678910复制代码类型:[java]
至此,第一个问题我们就解答完毕。
自动配置原理
package com.javafamily.familydemo;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class FamilyDemoApplication { public static void main(String[] args) { SpringApplication.run(FamilyDemoApplication.class, args); } }12345678910111213复制代码类型:[java]
一个最简单的SpringBoot项目其实只有一个Application启动类。我们想要研究自动配置,能够点击的也就只有@SpringBootApplication注解和run方法了。
今天咱们先看@SpringBootApplication注解,话不多说,直接开干。
点击@SpringBootApplication注解将看到以下内容:
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })public @interface SpringBootApplication { // 内容省略...}12345678910111213复制代码类型:[java]
我们发现,@SpringBootApplication是一个组合注解。
前四个是专门(即只能)用于对注解进行注解的,称为元注解。
@Target
用来定义注解标记的位置(如类、方法、属性等)
@Retention
注解的声明周期(源文件、编译期、运行时保留)
@Documented
用于标记在生成javadoc时是否将注解包含进去
@Inherited
子类继承父类的注解(FamilyDemoApplication的子类将自动继承@SpringBootApplication注解)
@SpringBootConfiguration:
进入@SpringBootConfiguration,我们发现它被@Configuration标记,也是一个组合注解。
@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Configurationpublic @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods() default true; }12345678910复制代码类型:[java]
继续点击@Configuration,又看到一个熟悉的注解@Component。
@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Componentpublic @interface Configuration { @AliasFor( annotation = Component.class ) String value() default ""; boolean proxyBeanMethods() default true; }123456789101112复制代码类型:[java]
至此,我们可以得出两个结论:
该注解与@Configuration注解功能相同,仅表示当前类为一个JavaConfig类,区别是前者为SpringBoot的注解,后者是SpringFramework的注解。
@Configuration本质上还是@Component,@ComponentScan可以处理@Configuration注解的类。
@ComponentScan:
默认扫描当前类所在的包以及子孙包。需要注意的是配置扫描工作并不是由该注解完整,真正的工作者还是@EnableAutoConfiguration。
// 扫描指定的包ComponentScan.Filter[] includeFilters() default {};// 排除不需要扫描的包ComponentScan.Filter[] excludeFilters() default {};12345复制代码类型:[java]
@EnableAutoConfiguration:
该注解用于开启自动配置,也是一个组合注解。所谓自动配置指的是会自动找到所需的类(SpringBoot&开发人员定义),然后交给Spring容器完成这些类的装配。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)public @interface EnableAutoConfiguration { // 内容省略...}1234567891011复制代码类型:[java]
@AutoConfigurationPackage:
将@SpringBootApplication标记的类所在包及其子孙包中所有组件都扫描到Spring容器里。
@Import:
用来导入所需配置类或需要前置加载的类。
到这里,我们就只剩下AutoConfigurationImportSelector还没看。该类主要关注以下两个方法。
/** * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata} * of the importing {@link Configuration @Configuration} class. * @param autoConfigurationMetadata the auto-configuration metadata * @param annotationMetadata the annotation metadata of the configuration class * @return the auto-configurations that should be imported */protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { // 判断自动配置是否开启 if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } // 获取注解的属性 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 通过SPI机制从META‐INF/spring.factories中获得需要自动配置的类 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); // 依靠LinkedHashSe机制移除重复配置类 configurations = removeDuplicates(configurations); // 根据@EnableAutoConfiguration注解中的属性,获取不需要加载的配置类集合 Set<String> exclusions = getExclusions(annotationMetadata, attributes); // 校验不需要加载的配置类是否合法 checkExcludedClasses(configurations, exclusions); // 从configurations移除不需要加载的类 configurations.removeAll(exclusions); // 过滤不符合条件的配置类 configurations = filter(configurations, autoConfigurationMetadata); // 触发自动配置导入时间 fireAutoConfigurationImportEvents(configurations, exclusions); // 创建AutoConfigurationEntry对象 return new AutoConfigurationEntry(configurations, exclusions); }1234567891011121314151617181920212223242526272829303132复制代码类型:[java]
/** * Return the auto-configuration class names that should be considered. By default * this method will load candidates using {@link SpringFactoriesLoader} with * {@link #getSpringFactoriesLoaderFactoryClass()}. * @param metadata the source metadata * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation * attributes} * @return a list of candidate configurations */protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 通过SPI机制从META-INF/spring.factories加载类型为EnableAutoConfiguration类 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you " + "are using a custom packaging, make sure that file is correct."); return configurations; }1234567891011121314151617复制代码类型:[java]
getCandidateConfigurations方法中其实就一行核心代码,我们继续跟进。
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(); // 调用loadSpringFactories方法 return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); }private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { // classLoader加载的FACTORIES_RESOURCE_LOCATION已经在类中定义了 值为META-INF/spring.factories 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); } }123456789101112131415161718192021222324252627282930313233343536373839复制代码类型:[java]
loadFactoryNames方法会调用loadSpringFactories方法,而在loadSpringFactories方法中会读取本类中FACTORIES_RESOURCE_LOCATION变量。这个变量的值正是META-INF/spring.factories。
那么META-INF/spring.factories文件在哪找呢?
举个例子:项目中引入了MyBatisPlus依赖,按照下图指示即可找到相应的配置文件。其它依赖也是如此。
最后,附上注解依赖图结束本次的源码探险之旅,我们下次再见。