Springboot源碼解析

我們每創建一個springboot應用就會發現,其目錄結構中都會有一個以應用名爲首的Application類(下文中都直接稱爲Application類),而其他包都是在這個類的同級或子級下面,結構如圖:

Application類作爲應用的啓動類,位於項目源碼的根目錄中,至於爲什麼結構會這麼安排,我們下面會說。

如上圖所示,我們可以看到,Application最關鍵的地方有兩個:

  • @SpringBootApplication註解
  • SpringApplication.run()方法

1.1@SpringBootApplication註解

打開註解的源碼我們可以看到,主要由以下幾個註解組成:

  1. @SpringBootConfiguration
  2. @EnableAutoConfiguration
  3. @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的構造方法中,我們可以看到有如下幾個步驟:

  1. 推斷當前的環境是否是web環境,其中springboot5.0中又添加了reactive環境。
  2. 初始化ApplicationContextInitilizer,其中的原理也是通過SpringFactoriesLoader來實現的。
  3. 初始化ApplicationListener,原理同上。
  4. 推斷當前啓動的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包中的文件定義整個啓動流程中要執行的所有默認配置好的類。

bede228d46b03a7cf360aeb5d3bc9e77.png

  • 推斷整個應用的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);

進入到方法裏面,可以發現這一步就是在:

  1. getOrCreateEnvironment()中加載servlet配置、servlet上下文配置、虛擬機配置、操作系統環境配置、jndi配置,
  2. 然後在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方法,主要做了以下幾點操作:

  1. 啓動簡單監聽器;
  2. 獲取並啓動SpringApplicationRunListener;
  3. 默認、外部配置屬性加載;
  4. 創建上下文;
  5. 初始化上下文,調用初始化器的initialize();
  6. 初始化容器,加載Bean;
  7. 應用啓動後處理;
  8. 啓動完成。

 

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項目的條件下。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章