spring boot starter机制

原创
2021/05/01 23:11
阅读数 1.2K

        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注解的解析过程来实现的。

展开阅读全文
加载中

作者的其它热门文章

打赏
2
0 收藏
分享
打赏
0 评论
0 收藏
2
分享
返回顶部
顶部