spring boot starter機制

        spring boot是一款非常優秀的微服務開發框架,具有開箱即用、簡化配置、內置tomcat等等一系列優點。這一切都離不開spring boot的starter機制,starter機制也可以理解爲spring boot所實現的一種SPI機制。本文主要講解spring boot starter機制的使用方式,以及實現原理。

1. 使用示例

1.1 概述

        對於starter的使用,首先我們需要注意的是,它一般有四部分:

  • application.properties中聲明的屬性配置;

  • 用於存儲application.properties中的屬性的property model,這個property model可以用@ConfigurationProperties進行標註,從而自動讀取application.properties配置文件中的屬性;

  • 使用@Configuration註解標註的XXXAutoConfiguration類,然後注入第二部分中記錄屬性的model,從而自動創建某些bean;

  • META-INF/文件夾下創建spring.factories文件,其中添加一條配置如下:

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.xxx.XXXAutoConfiguration
    

    這裏的key是@EnableAutoConfiguration註解類的全路徑名,值則是我們需要自動裝配的類。

1.2 裝載流程

       上面概述的四個部分,對於spring boot而言,其裝載流程如下:

       這裏需要說明的一個點在於,spring boot既然已經使用@Configuration註解標註了XXXAutoConfiguration類,爲什麼還需要在spring.factories文件中對其進行聲明。這主要是因爲,spring boot默認會掃描主類所在的目錄以及子目錄下的類,而XXXAutoConfiguration類一般是第三方jar包,默認是不會被掃描到的。通過spring.factories進行聲明,spring boot就會對其進行加載,然後通過過濾條件對不需要的bean進行過濾,從而實現自動裝配的目的。這裏所說的過濾,一般都是通過查看配置文件中是否配置了相關屬性,或者classpath中是否存在某個類來進行的,這樣我們在使用的時候,就只需要在配置文件中進行配置,即可直接使用相關的bean,而無需顯示聲明。

1.3 示例

       這裏我們以汽車爲例,自動裝載一個Car對象,而裝配這個Car對象需要很多的屬性,我們使用CarProperties對象來存儲這些屬性,而這些屬性則是通過application.properties文件聲明的,因而需要使用@ConfigurationPropertiesCarProperties進行標註,從而實現自動讀取配置屬性,如下是CarProperties的聲明:

@ConfigurationProperties(prefix = "car")
public class CarProperties {
  private String name;
  private String price;

  // getters and setters
}

        如此,CarProperties就可以自動讀取application.properties文件中的car.namecar.price屬性,並且設置到CarProperties對象中。讀取了配置文件的屬性之後,我們使用CarAutoConfiguration來構建對這些屬性的使用方式,這裏我們就是通過這些屬性創建一個Car對象。如下是CarAutoConfiguration的實現:

@Configuration
@EnableConfigurationProperties(CarProperties.class)
public class CarAutoConfiguration {

  @Bean
  @ConditionalOnProperty(value = "car.enabled", havingValue = "true")
  public Car car(CarProperties properties) {
    Car car = new Car();
    car.setName(properties.getName());
    car.setPrice(properties.getPrice());
    return car;
  }
}

        關於CarAutoConfiguration有三點需要說明:

  • CarAutoConfiguration需要使用@Configuration註解進行標註,否則其不會生效;
  • 需要使用@EnableConfigurationProperties引入CarProperties屬性model,否則其不會自動讀取application.properties中的屬性;
  • Car這個目標對象進行裝載的方法上,一般都需要加一定的限制條件,比如這裏是指明application.properties配置中必須顯示聲明car.enabled屬性值爲true,否則就不會調用car()方法裝載Car對象,這麼做的目的主要目的是隻有在用戶需要該對象的時候,才進行裝載。

       最後就是需要在META-INF/目錄下創建spring.factories文件,並且指定需要自動裝載CarAutoConfiguration,如下是spring.factories的聲明:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.car.starter.CarAutoConfiguration

        上面這些配置一般是在一個單獨的工程中進行的,當某個使用方需要自動裝配Car對象時,只需要引入該工程的pom聲明,然後在自身的application.properties文件中聲明裝配Car對象所需要的屬性,即可自動創建Car對象,如下是一個配置示例:

car.enabled=true
car.name=bentian
car.price=200000

       配置完成之後,即可自動注入Car對象了,而不需要做額外的聲明,如下是一個使用示例:

@RestController
@RequestMapping("/home")
public class IndexController {

  @Autowired
  private Car car;

  @GetMapping("/index")
  public String index() {
    return "hello, spring boot autoconfigure: " + car.getName() + ", " + car.getPrice();
  }
}

        在瀏覽器訪問http://localhost:8080/home/index,即可獲取一段文本:hello, spring boot autoconfigure: bentian, 200000,說明我們成功進行了自動裝配。

2. 實現原理

        關於Spring boot的SPI實現原理,其本質就是對@Configuraion註解的解析過程,當spring boot通過spring.factories文件讀取到要自動裝配的類的時候,就會對其進行解析,由於其使用了@Configuraion註解進行標註,因而只需要將其標註了@Bean註解的方法封裝爲一個個BeanDefinition即可,然後由spring boot實例化。至於對這些bean的過濾,則根據所定義的配置進行過濾即可。這裏spring.factories文件的讀取,其也是在@Configuraion註解的解析過程中進行的,具體的則是讀取該類上標註的@Import註解來進行的。

2.1 SpringBootApplication註解

       在spring boot啓動類上,我們一般會使用@SpringBootApplication註解進行標註,然後就可以完成自動裝配的功能。這裏@SpringBootApplication註解是由多個註解組合而成的,其聲明如下:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
		@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
	// methods...
}

@Configuration
public @interface SpringBootConfiguration {
}

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
  // methods...
}

        @SpringBootApplication是由@SpringBootConfiguraion@EnableAutoConfiguraion兩個註解組合而成的,而@SpringBootConfiguraion則是由@Configuraion註解標註的,@EnableAutoConfiguraion註解則是由@AutoConfigurationPackage@Import兩個註解組合而成的,關於這幾個註解的作用如下:

  • @Configuraion:自動裝配的核心類,其是通過ConfiguraionClassPostProcessor進行解析的;
  • @AutoConfiguraionPackage:需要自動掃描的類,默認情況下,其會掃描其所標註的類所在的包及其子包下的所有對象,註冊成spring boot的bean;
  • @Import:用於讀取spring.factories的類,這裏是AutoConfigurationImportSelector,其只會讀取spring.factories文件中key爲org.springframework.boot.autoconfigure.EnableAutoConfiguration的所有屬性;

2.2 @Configuraion註解的解析

        @Configuraion註解的解析是通過ConfiguraionClassPostProcessor來進行的,ConfiguraionClassPostProcessor實現了BeanDefinitionRegistryPostProcessor接口,其會在spring boot解析完所有的BeanDefinition,並且調用BeanFactoryPostProcessor.postProcessBeanFactory()方法之前進行調用。ConfiguraionClassPostProcessor的實現代碼如下:

public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
		PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
      @Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
		processConfigBeanDefinitions(registry);
	}
      
  public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
		List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
		String[] candidateNames = registry.getBeanDefinitionNames();

    // 獲取BeanDefinitionRegistry中所有使用@Configuraion註解進行了標註的bean
		for (String beanName : candidateNames) {
			BeanDefinition beanDef = registry.getBeanDefinition(beanName);
			if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) ||
					ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
				if (logger.isDebugEnabled()) {
					logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
				}
			}
			else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
				configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
			}
		}

    // 判斷如果沒有使用@Configuration標註的bean則退出
		if (configCandidates.isEmpty()) {
			return;
		}

		// 對使用@Configuraion進行標註的bean進行排序
		configCandidates.sort((bd1, bd2) -> {
			int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
			int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
			return Integer.compare(i1, i2);
		});

		// 設置BeanNameGenerator
		SingletonBeanRegistry sbr = null;
		if (registry instanceof SingletonBeanRegistry) {
			sbr = (SingletonBeanRegistry) registry;
			if (!this.localBeanNameGeneratorSet) {
				BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
				if (generator != null) {
					this.componentScanBeanNameGenerator = generator;
					this.importBeanNameGenerator = generator;
				}
			}
		}

    // 初始化Environment
		if (this.environment == null) {
			this.environment = new StandardEnvironment();
		}

		// 這個ConfigurationClassParser是解析@Configuration註解關鍵的一個類,每個使用@Configuraion註解
    // 進行標註的類都會初始化一個ConfigurationClassParser進行解析
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

		Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
		Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
		do {
      // 對使用@Configuration註解標註的類進行解析
			parser.parse(candidates);
      // 對解析得到的@Configuration類定義進行校驗
			parser.validate();

			Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
			configClasses.removeAll(alreadyParsed);

			if (this.reader == null) {
				this.reader = new ConfigurationClassBeanDefinitionReader(
						registry, this.sourceExtractor, this.resourceLoader, this.environment,
						this.importBeanNameGenerator, parser.getImportRegistry());
			}
      
      // 讀取@Configuraion標註的類新創建的類定義,比如其使用@Bean進行標註的方法,
      // 將其封裝爲一個個BeanDefinition,然後註冊到BeanDefinitionRegistry中
			this.reader.loadBeanDefinitions(configClasses);
			alreadyParsed.addAll(configClasses);

			// 略...
	}
}

        上面的流程其實比較簡單,主要是查找BeanDefinitionRegistry中註冊的所有使用@Configuration註解進行標註的類,然後將每一個類都交由一個ConfigurationClassParser進行解析。這裏需要說明的一點是,在初始狀態,BeanDefinitionRegistry中只有啓動類使用@Configuration進行了標註,ConfigurationParser會對其解析,然後根據其註解配置,會繼續掃描得到的BeanDefinition,並且掃描其是否包含@Configuration註解,從而遞歸的進行解析。

2.3 ConfiguraionClassParser解析

        ConfiguraionClassParser.parse()的解析過程最終是交由doProcessConfigurationClass()方法進行的,其調用過程中有如下代碼:

// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);

        這個方法就是讀取spring.factories,並且解析其需要自動裝配的類的入口。我們首先來看getImports()方法,其是用於讀取spring.factories的:

private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
  Set<SourceClass> imports = new LinkedHashSet<>();
  Set<SourceClass> visited = new LinkedHashSet<>();
  collectImports(sourceClass, imports, visited);
  return imports;
}

// 遞歸的解析sourceClass上標註的註解,找到使用@Import註解聲明的類,
// 對於@SpringBootApplication註解就是AutoConfigurationImportSelector
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
  throws IOException {

  if (visited.add(sourceClass)) {
    for (SourceClass annotation : sourceClass.getAnnotations()) {
      String annName = annotation.getMetadata().getClassName();
      if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
        collectImports(annotation, imports, visited);
      }
    }
    imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
  }
}

        上面的getImports()方法就是獲取當前類上使用@Import註解聲明的類,然後將其交由processImports()方法進行處理,如下是該方法的源碼:

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
    Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
  for (SourceClass candidate : importCandidates) {
    if (candidate.isAssignable(ImportSelector.class)) {
      // 判斷當前使用@Import註解聲明的類是否實現了ImportSelector接口,如果實現了,則交由
      Class<?> candidateClass = candidate.loadClass();
      ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
      ParserStrategyUtils.invokeAwareMethods(
        selector, this.environment, this.resourceLoader, this.registry);
      // 由於AutoConfigurationImportSelector不僅實現了ImportSelector接口,也實現了DeferredImportSelector
      // 接口,因而最終是交由其進行處理
      if (selector instanceof DeferredImportSelector) {
        this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
      } else {
        String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
        Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
        processImports(configClass, currentSourceClass, importSourceClasses, false);
      }
    }
}

        這裏的讀取過程最終還是交由ImportSelector進行,在讀取完成之後,由於該類使用了@Configuration註解進行標註,因而最終還是會遞歸的交由上面的doProcessConfigurationClass()方法進行處理,最終實現bean的自動裝配。

3. 小結

        本文首先以一個示例講解了spring boot自動裝配機制的使用方式,然後講解了其實現原理,主要是通過@Configuration註解的解析過程來實現的。

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