前言:前面和大家一起學習了springboot啓動流程源碼中如何從springboot過度到spring以及springboot如何內置tomcat,如何還不瞭解的童鞋可以去看一下之前的文章(springboot啓動流程源碼分析(1))。今天和大家一起學習的是springboot如何加載第三方的starter,只有熟悉了這個原理我們纔會發現自定義springboot的starter也是非常容易。
一、引入思考的問題
1、springboot未出現之前,我們在在spring項目中如果要使用數據源(比如我們使用druid),需要做哪些事情呢?
(1)引入druid的jar包
(2)配置數據源的參數
(2)在xml文件中配置數據源的bean,或者使用註解的方式@bean注入到spring容器中
2、當我們使用springboot項目時,需要做的事情有哪些?
(1)引入相應的druid的starter的包
(2)在yml或者properties中配置數據源參數
對比上面兩個步驟,我們發現springboot中,我們並沒有顯示的向spring容器中注入相應的datasource的bean,但是我們爲什麼能夠直接使用數據源呢(比如使用事務的時候)
今天咱們要學習的內容,就是解釋下上面的問題,如果你還不瞭解上述的原理,那咱們開始吧。在學習之前如果大家瞭解SPI(service provider interface)那就更好了,因爲這裏面其實就是用到了SPI的機制,SPI引用還是非常廣泛的,比如spring、dubbo中都有廣泛使用
二、springboot啓動加載starter
我們還是從啓動類開始分析
@SpringBootApplication
public class HellobootApplication {
public static void main(String[] args) {
SpringApplication.run(HellobootApplication.class, args);
}
}
我們進入@SpingBootApplication的註解
@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 {....}
只需要繼續看上面的註解就行,這次我們關注@EnableAutoConfiguration這個註解類,繼續跟
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
//關注這裏
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {...}
我們關注這一行註解@Import(AutoConfigurationImportSelector.class)這裏需要spring的知識,如果想要了解這一行註解的原理,可以自行在網上查取(其實原理就是ConfigurationClassPostProcessor這個處理器起的作用),暫時不瞭解也沒關係,在這裏先解釋一下,他會注入AutoConfigurationImportSelector的bean到spring容器,然而這個類又實現了ImportSelector,所以會調用selectImports,並且該方法返回的String[]的內容全部會注入到spring容器中。我們看下這個類的selectImports()方法
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
//重點看這裏
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
到了這裏,我們差不多已經見到了勝利的曙光了,springboot啓動流程的原理還是相對比較容易的,當然前提是你需要對spring的知識有一定的瞭解。上述方法很簡單,我們只要關注它需要向spring容器裏面注入哪些bean(不要跑偏了,我們之前帶着問題,就是爲什麼我們沒有顯示向spring容器中注入datasource,但是在springboot中我們能直接拿到),所以我們接着看getAutoConfigurationEntry()會給我們返回哪些string
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//拿到所有候選的需要注入到spring容器的bean的全路徑
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//刪除重複,其實就是用set轉存了一下
configurations = removeDuplicates(configurations);
//排除一些我們不需要的...
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
上面這個方法也挺好理解的,從每一行代碼的名字,我們大概就知道是做了哪些事情,爲了簡單梳理主要的流程,我這裏只看getCandidateConfigurations(),看看springboot如何幫我們找到需要注入到spring容器中的對象
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
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;
}
你如果留意的話,會看到一個校驗的信息"…META-INF/spring.factories…",暫時先不說,這個方法很簡單,我們直接看第一行代碼跟下一個方法SpringFactoriesLoader.loadFactoryNames()
String factoryTypeName = factoryType.getName();
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 {
//這裏就是通過類加載器去掃描所有的"META-INF/spring.factories"文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
//下面就不用了,掃描了很多spring.factories,需要一個個處理
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);
}
}
這裏我將兩個方法寫一起了,第一個方法太簡單了,我們直接看第二個吧,第二個方法的主要的核心代碼已經註釋了,下面總結一下:
(1)首先從判斷緩存裏面拿,拿不到就掃描
(2)掃描所有jar包中的META-INF/spring.factories文件,並處理裏面的內容(SPI)
(3)加入緩存,方便下次掃描
也就是說,前面跟了那麼多步驟,其實springboot要做的事情就是獲取每個jar包中的META-INF/spring.factories文件,然後將裏面的內容通過反射的方式創建對象,放入到spring容器中管理
那我們看看這些META-INF/spring.factories文件的內容
(1)springboot自帶的
(2)druid-start裏面的
所以通過上面的分析,我們的spring容器會自動註冊"com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure"這個bean,看下這個類吧
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class,
DruidStatViewServletConfiguration.class,
DruidWebStatFilterConfiguration.class,
DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
@Bean(initMethod = "init")
@ConditionalOnMissingBean
public DataSource dataSource() {
LOGGER.info("Init DruidDataSource");
return new DruidDataSourceWrapper();
}
}
到這裏就很明顯了,我們看到這個類用@Configuration註釋了,並且有@Bean註釋了datasource,所以相當在druid裏面裏經幫我們自動注入了datasource(當然這裏有一些springboot的註解,比如@COnditionOnclass等條件注入,大家可以自己網上查找資料或研究),另外大家可以自行研究druid如何將yml或者properties文件中的配置信息注入到datasource中,我就不跟大家一起了。分析到這裏,相信大家對springboot加載流程有一個整理的理解,同時也可以自定義starter啓動器,如果還是有一些問題,可以自己再跟一遍源碼,並且學習他人的自定義starter。