SpringBoot(二)自動裝配正文 - @SpringBootApplication、@EnableAutoConfiguration

前言

        最近在學習Spring Boot相關的課程,過程中以筆記的形式記錄下來,方便以後回憶,同時也在這裏和大家探討探討,文章中有漏的或者有補充的、錯誤的都希望大家能夠及時提出來,本人在此先謝謝了!

開始之前呢,希望大家帶着幾個問題去學習:
1、Spring Boot 自動裝配是什麼?
2、這個功能在什麼時代背景下發明產生的?
3、這個功能有什麼用?
4、怎麼實現的?
5、優點和缺點是什麼?
6、這個功能能應用在工作中?
這是對自我的提問,我認爲帶着問題去學習,是一種更好的學習方式,有利於加深理解。好了,接下來進入主題。

(一)起源

        在上篇文章中我們講到 Spring 註解雖然可以代替以往XML的形式,幫助我們自動註冊Bean以及初始化組件,簡化我們的開發,但還是做不到真正意義上的自動裝配,今天我們就來講講 Spring Boot 是如何深度整合 Spring 註解編程模型、@Enable 模塊驅動及條件裝配等 Spring 原生特性來實現自動裝配的。

注:本篇文章所用到的 Spring Boot版本是 2.1.6.BUILD-SNAPSHOT

(二)Spring Boot 自動裝配實現

        我們都知道 Spring Boot 的啓動過程非常簡單,只需要啓動一個 main 方法,項目就可以運行,就算依賴了諸多外部模塊如:MVC、Redis等,也不需要我們進行過多的配置,那它的底層原理是什麼呢?接下來,我們就一起去看一看。

我們先來看一段 Spring Boot 的啓動類代碼:

@SpringBootApplication
public class LoongSpringBootApplication {
	public static void main(String[] args) {
		SpringApplication.run(LoongSpringBootApplication.class, args);
	}
}

我們需要關注的是 @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 {

	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};
	
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

}

我們來看一看它的組成部分:

  • @SpringBootConfiguration:它裏面標註了 @Configuration註解,上篇文章說過,表明這是個配置類,功能與 @Configuration無異。
  • @EnableAutoConfiguration:這個就是實現自動裝配的核心註解,是用來激活自動裝配的,其中默認路徑掃描以及組件裝配、排除等都通過它來實現。
  • @ComponentScan:上篇文章我們講過這是用來掃描被 @Component標註的類 ,只不過這裏是用來過濾 Bean 的,指定哪些類不進行掃描,而且用的是自定義規則。
  • Class<?>[] exclude():根據class來排除,排除指定的類加入spring容器,傳入的類型是class類型。且繼承自 @EnableAutoConfiguration中的屬性。
  • String[] excludeName():根據class name來排除,排除特定的類加入spring容器,參數類型是class的全類名字符串數組。同樣繼承自 @EnableAutoConfiguration
  • String[] scanBasePackages():可以指定多個包名進行掃描。繼承自 @ComponentScan
  • Class<?>[] scanBasePackageClasses():可以指定多個類或接口的class,然後掃描 class 所在包下的所有組件。同樣繼承自 @ComponentScan

1、@EnableAutoConfiguration 實現

        上面我們說到 @EnableAutoConfiguration是實現自動裝配的核心註解,是用來激活自動裝配的,看註解前綴我們應該知道是上篇文章中所講的 Spring @Enable 模塊驅動的設計模式,所以它必然會有 @Import導入的被 @Configuration標註的類或實現 ImportSelectorImportBeanDefinitionRegistrar接口的類。接着,我們來看看它的定義:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	...
}

可以看到它由兩部分組成:

  • @AutoConfigurationPackage:這是用來將啓動類所在包,以及下面所有子包裏面的所有組件掃描到Spring容器中,這裏的組件是指被 @Component或其派生註解標註的類。這也就是爲什麼不用標註@ComponentScan的原因。
  • @Import(AutoConfigurationImportSelector.class):這裏導入的是實現了 ImportSelector接口的類,組件自動裝配的邏輯均在重寫的 selectImports方法中實現。

接下來我們就來看看這兩者具體是怎麼實現的。

1.1、獲取默認包掃描路徑

我們先來看看 Spring Boot 是如何通過 @AutoConfigurationPackage註解獲取默認包掃描路徑的,進入它的實現:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

可以看到它是通過 @Import導入了 AutoConfigurationPackages.Registrar類,該類實現了 ImportBeanDefinitionRegistrar接口,所以按照上篇文章所講的,它是在重寫的方法中直接註冊相關組件。繼續往下:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

		@Override
		public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
			register(registry, new PackageImport(metadata).getPackageName());
		}

		....
	}
private static final class PackageImport {

		private final String packageName;

		PackageImport(AnnotationMetadata metadata) {
			this.packageName = ClassUtils.getPackageName(metadata.getClassName());
		}
		....
}

這裏主要是通過 metadata元數據信息構造 PackageImport類。先獲取啓動類的類名,再通過 ClassUtils.getPackageName獲取啓動類所在的包名。我們接着往下看:

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
		if (registry.containsBeanDefinition(BEAN)) {
			BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
			ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
			constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
		}
		else {
			GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
			beanDefinition.setBeanClass(BasePackages.class);
			beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
			beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
			registry.registerBeanDefinition(BEAN, beanDefinition);
		}
	}

最後就是將這個包名保存至 BasePackages類中,然後通過 BeanDefinitionRegistry將其註冊,進行後續處理,至此該流程結束。

1.2、獲取自動裝配的組件

該部分就是實現自動裝配的入口,從上面得知這裏也是通過 @Import來實現的,來看看導入的類:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {

    ....
    
	@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());
	}
	
	....
}

主要關注重寫的 selectImports方法,其中 AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);是加載自動裝配的元信息。而AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata)該方法返回的就是自動裝配的組件,我們進去看看:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
			AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	
	// 獲取 @EnableAutoConfigoration 標註類的元信息,也就是獲取該註解 exclude 和 excludeName 屬性值
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	
	// 該方法就是獲取自動裝配的類名集合
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	
	// 去除重複的自動裝配組件,就是將List轉爲Set進行去重
	configurations = removeDuplicates(configurations);
	
	// 這部分就是根據上面獲取的 exclude 及 excludeName 屬性值,排除指定的類
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	
	// 這裏是過濾那些依賴不滿足的自動裝配 Class
	configurations = filter(configurations, autoConfigurationMetadata);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	
	// 返回的就是經過一系列去重、排除、過濾等操作後的自動裝配組件
	return new AutoConfigurationEntry(configurations, exclusions);
}

該方法中就是先獲取待自動裝配組件的類名集合,然後通過一些列的去重、排除、過濾,最終返回自動裝配的類名集合。主要關注 getCandidateConfigurations(annotationMetadata, attributes)這個方法,裏面是如何獲取自動裝配的類名集合:

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;
}

其中getSpringFactoriesLoaderFactoryClass()返回的是EnableAutoConfiguration.class
繼續往下,執行的是 SpringFactoriesLoader#loadFactoryNames方法:

public final class SpringFactoriesLoader {

    ...
    
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    
    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    
        // 前面可以看到,這裏的 factoryClass 是 EnableAutoConfiguration.class
		String factoryClassName = factoryClass.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, 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 {
			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 factoryClassName = ((String) entry.getKey()).trim();
					for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryClassName, factoryName.trim());
					}
				}
			}
			cache.put(classLoader, result);
			return result;
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
	}
	...
}

最終的實現邏輯都在這裏,主要過程如下:

(1)搜索classpath路徑下以及所有外部jar包下的META-INF文件夾中的spring.factories文件。主要是spring-boot-autoconfigur包下的

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

...

可以看到其中內容,存儲的是key-value格式的數據,且key是一個類的全路徑名稱,value是多個類的全路徑名稱,且以逗號分割。

(2)將所有的spring.factories文件轉成Properties格式,將裏面key-value格式的數據轉成Map,該Map的value是一個List,之後將相同Key的value合併到List中,將該Map作爲方法返回值返回。

(3)返回到 loadFactoryNames方法,通過上面得知factoryClassName的值爲EnableAutoConfiguration,所以通過 getOrDefault(factoryClassName, Collections.emptyList())方法,獲取 key 爲EnableAutoConfiguration的類名集合。

ps:getOrDefault第一個入參是key的name,如果key不存在,則直接返回第二個參數值

至此,流程結束,最後返回的就是自動裝配的組件,可以看到一個特點,這些自動裝配的組件都是以 AutoConfiguration結尾。但該組件列表只是候選組件,因爲後面還有去重、排除、過濾等一系列操作,這裏就不再詳細述說。下面我們來看看自動裝配的組件內部是怎麼樣的。

2、自動裝配的組件內部實現

就拿比較熟悉的 Web MVC 來看,看看是如何實現 Web MVC 自動裝配的。先來代碼組成部分:

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    ...
    @Configuration
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ResourceLoaderAware {
        ...
        @Bean
        @ConditionalOnBean(View.class)
        @ConditionalOnMissingBean
        public BeanNameViewResolver beanNameViewResolver() {
            ...
        }
        ...
    }

    @Configuration
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {

        @Bean
        @Override
        public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
            ...
        }

        @Bean
        @Primary
        @Override
        public RequestMappingHandlerMapping requestMappingHandlerMapping() {
            ...
        }
    }
    ...
}
  • 註解部分:

    • @Configuration:這個大家都比較熟悉,標識該類是一個配置類
    • @ConditionalXXX:這是上篇文章所講的 Spring 條件裝配,只不過經由 Spring Boot 擴展形成了自己的條件化自動裝配,且都是@Conditional的派生註解。
      • @ConditionalOnWebApplication:參數值是 Type 類型的枚舉,當前項目類型是任意、Web、Reactive其中之一則實例化該 Bean。這裏指定如果爲 Web 項目才滿足條件。
      • @ConditionalOnClass:參數是 Class 數組,當給定的類名在類路徑上存在,則實例化當前Bean。這裏當Servlet.classDispatcherServlet.classWebMvcConfigurer.class存在才滿足條件。
      • @ConditionalOnMissingBean:參數是也是 Class 數組,當給定的類沒有實例化時,則實例化當前Bean。這裏指定當 WebMvcConfigurationSupport該類沒有實例化時,才滿足條件。
    • 裝配順序
      • @AutoConfigureOrder:參數是int類型的數值,數越小越先初始化。
      • @AutoConfigureAfter:參數是 Class 數組,在指定的配置類初始化後再加載。
      • @AutoConfigureBefore:參數同樣是 Class 數組,在指定的配置類初始化前加載。
  • 代碼部分:

    • 這部分就比較直接了,實例化了和 Web MVC 相關的Bean,如 HandlerAdapterHandlerMappingViewResolver等。其中,出現了 DelegatingWebMvcConfiguration類,這是上篇文章所講的 @EnableWebMvc@Import導入的配置類。

可以看到,在Spring Boot自動裝配的類中,經過了一系列的 @Conditional條件判斷,然後實例化某個模塊需要的Bean,且無需我們配置任何東西,當然,這都是默認實現,當這些不滿足我們的要求時,我們還得手動操作。

(三)總結

        關於Spring boot自動裝配的內容就告一段落,不難看出Spring Boot自動裝配所依賴的註解驅動、@Enable模塊驅動、條件裝配等特性均來自 Spring Framework。而自動裝配的配置類均來源於spring.factories文件中。核心則是基於“約定大於配置”理念,通俗的說,就是Spring boot爲我們提供了一套默認的配置,只有當默認的配置不滿足我們的需求時,我們再去修改默認配置。當然它也存在缺點就是組件的高度集成,使用的時候很難知道底層實現,加深了理解難度。

以上就是本章的內容,如過文章中有錯誤或者需要補充的請及時提出,本人感激不盡。



參考:

《Spring Boot 編程思想》

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