我們每創建一個springboot應用就會發現,其目錄結構中都會有一個以應用名爲首的Application類(下文中都直接稱爲Application類),而其他包都是在這個類的同級或子級下面,結構如圖:
Application類作爲應用的啓動類,位於項目源碼的根目錄中,至於爲什麼結構會這麼安排,我們下面會說。
如上圖所示,我們可以看到,Application最關鍵的地方有兩個:
- @SpringBootApplication註解
- SpringApplication.run()方法
1.1@SpringBootApplication註解
打開註解的源碼我們可以看到,主要由以下幾個註解組成:
- @SpringBootConfiguration
- @EnableAutoConfiguration
- @ComponentScan
- @SpringBootConfiguration
@SpringBootConfiguration註解是由@Configuration來註解的,因此也就表示Application類本身就是一個bean。雖然@SpringBootConfiguration註釋中說該註解一個應用中只能用一次,但是配置多個也不會報錯,只是建議在一個應用中只用一次,並且該註解也可以與@Configuration互換。
- @ComponentScan
@ComponentScan註解的功能與我們之前在xml文件配置的的功能是一樣的,而上面也提到Application類放在源碼的根目錄下,其實就是與這個註解有關。@ComponentScan在沒有指明basePackages懺屬性的時候,默認會掃描該註解所在的類的包及其子包下的所有@Component註解過的類,包括@Controller,@Service,@Configuration這些註解。這也就是爲什麼我們不用做任何配置,就可以將springboot應用中的類掃描爲bean。
- @EnableAutoConfiguration
@EnableAutoConfiguration註解,從名字我們也可以看出,是開啓自配置配置的。從源碼中我們可以看到,該註解中有一個@Import(AutoConfigurationImportSelector.class),而其中發揮自動配置作用就是AutoConfigurationImportSelector類。
@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 {
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}
}
從AutoConfigurationImportSelector的源碼中可以知道,該類是通過SpringFactoriesLoader來加載自動配置類的定義,從而進一步通過這些自動配置的類來完成默認配置。從而這也就解決了,爲什麼我們使用springboot的時候壓根就不需要配置太多,原因就是因爲SpringBoot通過自動配置將已經封裝在jar包中的自動配置類加載進來生成了bean。而對於SpringFactoriesLoader的原理我們下面會說。
1.2SpringApplication類
Application類是通過SpringApplication類的靜態run方法來啓動應用的。打開這個靜態方法,該表態方法真正執行的是兩部分:
- new SpringApplication()
- 執行對象run()方法
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 = deduceWebApplicationType();
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
SpringApplication構造方法
在SpringApplication的構造方法中,我們可以看到有如下幾個步驟:
- 推斷當前的環境是否是web環境,其中springboot5.0中又添加了reactive環境。
- 初始化ApplicationContextInitilizer,其中的原理也是通過SpringFactoriesLoader來實現的。
- 初始化ApplicationListener,原理同上。
- 推斷當前啓動的main方法所在的類。
- 推斷當前應用環境
springboot在啓動時需要推斷當前的應用環境,springboot5.0當中一共定義了三種環境:none, servlet, reactive。none表示當前的應用即不是一個web應用也不是一個reactive應用,是一個純後臺的應用。servlet表示當前應用是一個標準的web應用。reactive是spring5當中的新特性,表示是一個響應式的web應用。而判斷的依據就是根據Classloader中加載的類。如果是servlet,則表示是web,如果是DispatcherHandler,則表示是一個reactive應用,如果兩者都不存在,則表示是一個非web環境的應用。
- 初始化ApplicationContextInitializer
在介紹初始化之前,先介紹一下SpringFactoiesLoader的原理。進入到getSpringFactoriesInstances
這個方法中:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// 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;
}
進入到SpringFactoriesLoader.loadFactoryNames(type,classLoader):
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
if (result != null) {
return result;
} else {
try {
//獲取所有jar的spring.factories的文件路徑
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
LinkedMultiValueMap result = new LinkedMultiValueMap();
while(urls.hasMoreElements()) {
//加載其中的一個spring.factories文件
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
//該spring.factories文件中的所有鍵值對
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
//其中一個接口的實現類的集合
Entry<?, ?> entry = (Entry)var6.next();
List<String> factoryClassNames = Arrays.asList(StringUtils.commaDelimitedListToStringArray((String)entry.getValue()));
result.addAll((String)entry.getKey(), factoryClassNames);
}
}
cache.put(classLoader, result);
return result;
} catch (IOException var9) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var9);
}
}
}
SpringFactoiesLoader會掃描所有jar包中的META-INF/spring.factoies文件,該文件的格式都是key=value的格式。key是一個接口的名字,value是實現類的名字,如果value有多個值,則用逗號分隔。而我們上面提到的@EnableAutoConfiguration也是利用這個原理去掃描所有的自動配置類,以該註解的全限定類名作爲key,所有的默認配置類爲值。SpringFactoiesLoader內部有一個靜態的雙層ConcurrentMap cache,用來存儲這些key-value,第一層map的key是classloader,springboot默認的classloader是appClassLoader,value則是一個MultiValueMap,是spring自己實現的一個hashmap。而這個MultiValueMap的key就是spring.factoies文件中的key,value是一個list,即spring.factoies文件中的value。SpringFactoiesLoader利用緩存機制,只在第一次掃描所有的META-INF/spring.factoies文件時,就把所有的key-value都加載到這個map中,後面再進從中獲取值時,直接從map中取就可以了,就不需要再重新掃描了。
把ApplicationContextInitializer都加載了之後,還要進行一項工作就是會對所有的ApplicationContextInitializer實現類生成對象,SpringApplication中有一個屬性,List類型的initializers,用來存儲這些實例化後的對象,這些對象存儲之後,會在後面的啓動過程中初始化ApplicationContext。
- 初始化ApplicationListener
ApplicationListener的過程與ApplicationContextInitializer是一樣的,不過因爲SpringFactoiesLoader已經掃描過一次了,所以這次執行的時候,就會直接從SpringFactoiesLoader中的靜態map中取出值即可。同樣的,也需要將所有的實現類生成對象,並保存在SpringApplication對象的listener list屬性中。
注:springboot初始化過程中主要是掃描兩個spring.factoies文件,其中定義的key-value中的類,是整個啓動過程中要用到的。一個是spring-boot-2.0.3.RELEASE中的,一個是spring-boot-autoconfigure包中的。這兩個jar包中的文件定義整個啓動流程中要執行的所有默認配置好的類。
- 推斷整個應用的main方法所在的類
其實我們已經從main方法中啓動了,爲什麼後面還要再推斷一下呢?其實這樣做的目的,主要是爲了將該類的對象存儲在SpringApplication對象中,創建日誌Logger和打印日誌用的。我們在啓動時會看到主類的類名以及其他的打印信息,都是通過該對象來創建logger和打印日誌的。
總結:從上面幾個步驟中我們可以看出,前期的new SpringApplication()方法中主要是起到了一個預加載的功能,將前期的環境判斷,後面要用到的對象都準備好,到run執行的時候就直接拿出來用就好了,也是大大方便了後面run方法執行的過程。
run方法
進入到run方法裏面:
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
//獲取並啓動SpringApplicationRunListeners監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
//這兩句用於屬性的配置,執行完後,外部參數和application.properties中的參數都被加載進來了,並且發佈了應用環境配置事件。
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
- stopWatch.start();
StopWatch做一個應用啓動的簡單監控,監控應用啓動花費的時間;
- SpringApplicationRunListeners listeners = getRunListeners(args);
獲取SpringApplicationRunListener,該類唯一的實現是EventPublishingRunListener,它用於在上下文啓動過程做一些事件發佈,以通知監聽器應用啓動運行到哪一步了。它包含了下面幾個方法,每個方法都使用SimpleApplicationEventMulticaster.multicastEvent(ApplicationEvent)來發布事件。
starting()//run方法執行的時候立馬執行;對應事件的類型是ApplicationStartedEvent
environmentPrepared(ConfigurableEnvironment environment) //ApplicationContext創建之前並且環境信息準備好的時候調用;對應事件的類型是ApplicationEnvironmentPreparedEvent
contextPrepared(ConfigurableApplicationContext context)// ApplicationContext創建好並且在source加載之前調用一次;沒有具體的對應事件
contextLoaded(ConfigurableApplicationContext context)//AplicationContext創建並加載之後並在refresh之前調用;對應事件的類型是ApplicationPreparedEvent
started(ConfigurableApplicationContext context) //run方法結束之前調用;對應事件的類型是ApplicationReadyEvent或ApplicationFailedEven
-
getOrCreateEnvironment() ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
進入到方法裏面,可以發現這一步就是在:
- getOrCreateEnvironment()中加載servlet配置、servlet上下文配置、虛擬機配置、操作系統環境配置、jndi配置,
- 然後在prepareEnvironment()加載命令行變量配置(放在最前面),並通知ConfigFileApplicationListener.onApplicationEvent()(這個監聽器是在SpringApplication構造函數執行時加載的)去加載Random配置和"classpath:/,classpath:/config/,file:./,file:./config/"四個地方的application.properties、application.xml、application.yml和application.yaml中的配置。後面spring容器初始化時會有spring.factories中的AutoConfiguration類執行的默認配置。
protected void configurePropertySources(ConfigurableEnvironment environment,
String[] args) {
MutablePropertySources sources = environment.getPropertySources();
if (this.defaultProperties != null && !this.defaultProperties.isEmpty()) {
sources.addLast(
new MapPropertySource("defaultProperties", this.defaultProperties));
}
if (this.addCommandLineProperties && args.length > 0) {
String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
if (sources.contains(name)) {
PropertySource<?> source = sources.get(name);
CompositePropertySource composite = new CompositePropertySource(name);
composite.addPropertySource(new SimpleCommandLinePropertySource(
"springApplicationCommandLineArgs", args));
composite.addPropertySource(source);
sources.replace(name, composite);
}
else {
sources.addFirst(new SimpleCommandLinePropertySource(args));
}
}
}
- context = createApplicationContext();
創建應用上下文,依據前面SpringApplication構造階段推斷出的應用類型創建應用上下文對象。
- prepareContext(context, environment, listeners, applicationArguments,printedBanner);
初始化應用上下文,這裏就是用前面SpringApplication構造階段加載的所有應用上下文初始化器的initialize()方法初始化應用上下文對象。然後將自己的XxxSpringApplication類的定義加載到SpringApplication對象中,以便於打印日誌。
- refreshContext(context);
加載Bean,就是Spring加載Bean定義和創建單例Bean的過程,後面我會寫一篇Spring源碼解讀來單獨介紹。
- afterRefresh(context, applicationArguments);
在springboot啓動過程中的run方法的最後,有一句·afterRefresh(context, applicationArguments)。主要是執行ApplicationRunner和CommandLineRunner兩個接口的實現類,這兩個類都是在springboot啓動完成後執行的一點代碼,類似於普通bean中的init方法,開機自啓動,用於做一些初始化的工作。 兩者唯一不同是獲取參數方式不同。
所以,總結run方法,主要做了以下幾點操作:
- 啓動簡單監聽器;
- 獲取並啓動SpringApplicationRunListener;
- 默認、外部配置屬性加載;
- 創建上下文;
- 初始化上下文,調用初始化器的initialize();
- 初始化容器,加載Bean;
- 應用啓動後處理;
- 啓動完成。
1.3@EnableAutoConfiguration
參考來源:
這一節我們詳細介紹EnableAutoConfiguration。我們前面講了,@EnableAutoConfiguration其實就是通過AutoConfigurationImportSelector來處理的,AutoConfigurationImportSelector中的SpringFactoriesLoader會找到所有spring.factories
文件,然後查詢屬性org.springframework.boot.autoconfigure.EnbleAutoConfiguration
的值,將這些值存儲到cache中,後面再在spring啓動過程中的refresh方法中進行真正的配置類bean加載。而EnbleAutoConfiguration的值其實就是一些starter的AutoConfiguration類。
下面我們就進入到其中一個AutoConfiguration類中看看,它是怎樣加載默認配置和創建該組件所需bean的。
以Redis的自動配置類RedisAutoConfiguration爲例,如下所示。
@Configuration
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
public RedisAutoConfiguration() {
}
@Bean
@ConditionalOnMissingBean(
name = {"redisTemplate"}
)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
該配置類頂部有一個@EnableConfigurationProperties({RedisProperties.class}),這個註解中指定的RedisProperties就是Redis的默認配置屬性的POJO了,通過將默認屬性放到POJO類中,我們在Springboot應用中配置該屬性時可以產生類型提示並校驗提供的值:
@ConfigurationProperties(
prefix = "spring.redis"
)
public class RedisProperties {
private int database = 0;
private String url;
private String host = "localhost";
private String password;
private int port = 6379;
private boolean ssl;
private Duration timeout;
private RedisProperties.Sentinel sentinel;
private RedisProperties.Cluster cluster;
private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
//一系列的setter/getter方法
}
因此,當我們自己想創建一個starter時,只需要提供下面幾個類即可:
- XxxProperties
- XxxAutoConfiguration:並在類的頂部使用EnableConfigurationProperties({XxxProperties.class})
- 在META-INF/spring.factories中增加下面的內容:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
XxxAutoConfiguration
- 然後打包即可。
我們回到RedisAutoConfiguration,看到該類中有幾個方法,方法上面有一些註解。其中@Bean是告訴spring該方法返回一個Bean交給Spring容器管理,而@ConditionalXxx註解則是執行下面方法的條件,滿足條件才執行。
而@ConditionalXxx的原理就是,通過實現Conditional接口的matchOutcome方法,然後在@ConditionalXxx上面加一個@Conditional註解,並給出處理類--Conditional的實現類即可。
通過AutoConfiguration中有如下幾個重要的@Conditional:
@ConditionalOnBean:當容器裏有指定Bean的條件下
@ConditionalOnClass:當類路徑下有指定類的條件下
@ConditionalOnExpression:基於SpEL表達式作爲判斷條件
@ConditionalOnJava:基於JV版本作爲判斷條件
@ConditionalOnJndi:在JNDI存在的條件下差在指定的位置
@ConditionalOnMissingBean:當容器裏沒有指定Bean的情況下
@ConditionalOnMissingClass:當類路徑下沒有指定類的條件下
@ConditionalOnNotWebApplication:當前項目不是Web項目的條件下
@ConditionalOnProperty:指定的屬性是否有指定的值
@ConditionalOnResource:類路徑是否有指定的值
@ConditionalOnSingleCandidate:當指定Bean在容器中只有一個,或者雖然有多個但是指定首選Bean
@ConditionalOnWebApplication:當前項目是Web項目的條件下。