SpringBoot自動裝配-原始碼分析

張鐵牛發表於2021-08-02

1. 簡介

通過原始碼探究SpringBoot的自動裝配功能。

2. 核心程式碼

2.1 啟動類

我們都知道SpringBoot專案建立好後,會自動生成一個當前模組的啟動類。如下:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

2.2 @SpringBootApplication

在啟動類中有個很重要的註解@SpringBootApplication,在該註解中除了元註解,就是@SpringBootConfiguration
@EnableAutoConfiguration@ComponentScan

  • @SpringBootConfiguration:標識了當前類為配置類
  • @ComponentScan:配置類的元件掃描
  • @EnableAutoConfiguration:啟用自動裝配
@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 {};
  ...
}

2.3 @EnableAutoConfiguration

這裡我們重點看@EnableAutoConfiguration註解。

在該註解中我們看到了熟悉的@Import註解,並且該註解指定匯入了AutoConfigurationImportSelector.class

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
  String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
  Class<?>[] exclude() default {};
  String[] excludeName() default {};
}

2.4 AutoConfigurationImportSelector

我們進入到AutoConfigurationImportSelector.class,看到當前類繼承自DeferredImportSelector介面,而通過檢視DeferredImportSelector原始碼 public interface DeferredImportSelector extends ImportSelector {}得知,DeferredImportSelector繼承自ImportSelector介面。因此我們大概得知SpringBoot預設裝載了ImportSelector::selectImports()方法返回的全限類名陣列。

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
    ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
  /**
   * 重寫ImportSelector介面中的selectImports方法
   * <p>
   *    該方法返回的陣列<全限類名> 都將被裝載到IOC容器
   */
	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    // 將符合注入IOC條件的Bean類資訊返回
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
  
  /**
   * 獲取自動配置的資訊 
   */
  protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
    // 獲取元註解屬性
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // ** 獲取候選的配置資訊
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    // 移除重複元素
		configurations = removeDuplicates(configurations);
    // 獲取任何限制候選配置的排除項
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // 判斷排除項是否存在
		checkExcludedClasses(configurations, exclusions);
    // 從候選配置集合中排除需要排除的項
		configurations.removeAll(exclusions);
    // 獲取在spring.factories中註冊的過濾器,並執行filter方法,返回符合註冊條件的元素
		configurations = getConfigurationClassFilter().filter(configurations);
    // 觸發自動配置匯入事件
		fireAutoConfigurationImportEvents(configurations, exclusions);
    // 返回自動配置和排除項資訊
		return new AutoConfigurationEntry(configurations, exclusions);
	}
  
  /**
   * 獲取屬性
   */
  protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
		String name = getAnnotationClass().getName();
		AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
		Assert.notNull(attributes, () -> "No auto-configuration attributes found. Is " + metadata.getClassName()
				+ " annotated with " + ClassUtils.getShortName(name) + "?");
		return attributes;
	}
  
  /**
   * 獲取候選的配置資訊
   */
  protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());
    // 這個就很重要了,從這裡大概可以判斷出 配置資訊是從META-INF/spring.factories這個檔案中獲取到的
		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;
	}
}

2.5 SpringFactoriesLoader

為了驗證配置資訊是不是從META-INF/spring.factories獲取的,我們繼續跟蹤原始碼SpringFactoriesLoader::loadFactoryNames()

public final class SpringFactoriesLoader {

	/**
	 * 工廠資源位置 
	 *
	 * <p>
	 *     可以存在於多個Jar檔案中
	 */
	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

	static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();

	/**
	 * 載入工廠名稱
	 * 
	 */
	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
		}
    // 當前上下文中 factoryTypeName = EnableAutoConfiguration註解的全限類名
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
	}

  /**
   * 載入spring工廠
   */
	private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
		Map<String, List<String>> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		result = new HashMap<>();
		try {
      // 獲取 META-INF/spring.factories 列舉資訊
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
			while (urls.hasMoreElements()) {
        // spring.factories 檔案地址
				URL url = urls.nextElement();
        // 獲取resource資訊
				UrlResource resource = new UrlResource(url);
        // 載入配置檔案中的配置資訊
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        // 遍歷配置資訊放入全域性的Map快取中
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
								.add(factoryImplementationName.trim());
					}
				}
			}

			// Replace all lists with unmodifiable lists containing unique elements
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		return result;
	}
}

在這裡為了更方便的檢視loadSpringFactories中各步驟是用來幹嘛的,特意新增debug截圖如下:

2.6 spring.factories

spring-boot-autoconfigure下的META-INF/spring.factories檔案資訊

從上圖中我們能看出spring.factories 中指定了很多常用中介軟體的auto configure檔案資訊。

2.7 RedisAutoConfiguration

我們僅檢視我們比較熟悉的redis中介軟體的autoconfiguration檔案資訊

RedisAutoConfiguration原始碼中我們能看出在檔案中使用很多的@Conditional註解來實現注入符合條件的SpringBean

// 標識為配置類
@Configuration(proxyBeanMethods = false)
// 當存在RedisOperations.class時注入當前類
@ConditionalOnClass(RedisOperations.class)
// 啟用RedisProperties屬性檔案
@EnableConfigurationProperties(RedisProperties.class)
// 匯入客戶端配置類
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

  @Bean
  // 當 當前環境中沒有redisTemplate Bean時注入當前Bean
  @ConditionalOnMissingBean(name = "redisTemplate")
  /*
   * 當指定RedisConnectionFactory類已存在於 BeanFactory 中,並且可以確定單個候選項才會匹配成功。
   * 或者 BeanFactory 存在多個 RedisConnectionFactory 例項,但是有一個 primary 候選項被指定(通常在類上使用 @Primary 		 * 註解),也會匹配成功
   */ 
  @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
  public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
    RedisTemplate<Object, Object> template = new RedisTemplate<>();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }

  @Bean
  @ConditionalOnMissingBean
  @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
  public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
    StringRedisTemplate template = new StringRedisTemplate();
    template.setConnectionFactory(redisConnectionFactory);
    return template;
  }

}

3. 小結

至此我們大概瞭解了SpringBoot是如何實現自動裝配的。

  1. 專案啟動
  2. 通過啟動類上的@SpringBootApplication註解載入@EnableAutoConfiguration註解
  3. 通過@EnableAutoConfiguration載入@Import(AutoConfigurationImportSelector.class)執行AutoConfigurationImportSelector匯入選擇器
  4. AutoConfigurationImportSelector中執行selectImports()方法
  5. AutoConfigurationImportSelector::selectImports()通過載入ClassPath下的META-INF/spring.factories檔案來動態的注入*AutoConfiguration類
  6. *AutoConfiguration類中通過使用@Conditional註解及其派生註解實現了Bean的靈活裝載。

相關文章