欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

SpringBoot核心机制五、EnableAutoConfiguration属性自动装配

程序员文章站 2022-04-19 22:49:41
...

所有调试基于spring-boot-2.4.5版本

--- 楼兰
​ 现在终于到了SpringBoot最为核心的自动装配功能,这是SpringBoot最为重要的一个功能机制,也是网上文章讨论最为泛滥的功能点。但是在这个系列文章中,大家一起来看看以spring.factories为核心来理解自动装配会不会有什么不一样的感觉。

​ 关于什么是自动装配,应该是使用SpringBoot开发过程中接触最多的功能。最常见的使用方式是在配置文件里配置了spring.datasource相关的几个属性后,就直接可以在SpringBoot应用中使用Datasource实例,以及使用JdbcTemplate来操作JDBC了。大部分的第三方框架与SpringBoot的整合也都是通过这个自动装配机制,所以他的重要性是不言而喻的。

一、SpringBoot自动装配的两架马车

​ 关于SpringBoot的自动装配机制,这是让整个Java领域都亮瞎眼的划时代的一个重要机制。虽然他的底层是非常巧妙的,但是到了上层应用,却又非常轻巧。这也正是他的魅力所在。

​ 关于这个自动装配机制的解读也是非常多的,但是我觉得都过于零散。要想对整个机制形成大概的把握,以下这两个Spring的核心机制是必须要清楚的,要不然,就算你以为自己看到了自动装配的机制源码,其实也形成不了体系。

1、@Import注解

​ 关于@Import注解,其实是Spring底层的一个重要机制,但是对于理解SpringBoot的自动装配机制是非常重要的,所以这里也有必要单独提出来总结一下。

​ Spring中对于@Imoport注解的处理逻辑在 AbstractApplicationContext -> refresh() -> invokeBeanFactoryPostProcessors(beanFactory) -> ConfigurationClassPostProcessor -> postProcessBeanDefinitionRegistry()方法中。在这个方法中由一个ConfigurationClassParser最终来处理所有的配置对象。

		// Parse each @Configuration class
		ConfigurationClassParser parser = new ConfigurationClassParser(
				this.metadataReaderFactory, this.problemReporter, this.environment,
				this.resourceLoader, this.componentScanBeanNameGenerator, registry);

最终跟踪parser的parse方法,就跟踪到了这个processImports方法。

class ConfigurationClassParser {
    ...
    // Process any @Import annotations
    private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
			Collection<SourceClass> importCandidates, Predicate<String> exclusionFilter,
			boolean checkForCircularImports) {

		if (importCandidates.isEmpty()) {
			return;
		}

		if (checkForCircularImports && isChainedImportOnStack(configClass)) {
			this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
		}
		else {
			this.importStack.push(configClass);
			try {
				for (SourceClass candidate : importCandidates) {
					if (candidate.isAssignable(ImportSelector.class)) { //<====分析重点
						// Candidate class is an ImportSelector -> delegate to it to determine imports
						Class<?> candidateClass = candidate.loadClass();
						ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
								this.environment, this.resourceLoader, this.registry);
						Predicate<String> selectorFilter = selector.getExclusionFilter();
						if (selectorFilter != null) {
							exclusionFilter = exclusionFilter.or(selectorFilter);
						}
						if (selector instanceof DeferredImportSelector) {
							this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);//<===接下来会贴出DeferedImportSelector接口的处理逻辑
						}
						else {
							String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
							Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames, exclusionFilter);
							processImports(configClass, currentSourceClass, importSourceClasses, exclusionFilter, false);
						}
					}
					else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
						// Candidate class is an ImportBeanDefinitionRegistrar ->
						// delegate to it to register additional bean definitions
						Class<?> candidateClass = candidate.loadClass();
						ImportBeanDefinitionRegistrar registrar =
								ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
										this.environment, this.resourceLoader, this.registry);
						configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
					}
					else {
						// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
						processConfigurationClass(candidate.asConfigClass(configClass), exclusionFilter);
					}
				}
			}
			catch (BeanDefinitionStoreException ex) {
				throw ex;
			}
			catch (Throwable ex) {
				throw new BeanDefinitionStoreException(
						"Failed to process import candidates for configuration class [" +
						configClass.getMetadata().getClassName() + "]", ex);
			}
			finally {
				this.importStack.pop();
			}
		}
	}
}

对于DeferredImportSelector接口子类的实现逻辑如下:

//#org.springframework.context.annotation.ConfigurationClassParser#DeferredImportSelectorHandler
public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
			DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
			if (this.deferredImportSelectors == null) {
				DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
				handler.register(holder);
				handler.processGroupImports();
			}
			else {
				this.deferredImportSelectors.add(holder);
			}
		}
//#org.springframework.context.annotation.ConfigurationClassParser#DeferredImportSelectorGroupingHandler 
public void register(DeferredImportSelectorHolder deferredImport) {
			Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();
			DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent(
					(group != null ? group : deferredImport),
					key -> new DeferredImportSelectorGrouping(createGroup(group)));
			grouping.add(deferredImport); //<=== 分组
			this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
					deferredImport.getConfigurationClass());
		}

		public void processGroupImports() {
			for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {//<===按组加载
				Predicate<String> exclusionFilter = grouping.getCandidateFilter();
				grouping.getImports().forEach(entry -> {
					ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
					try {
						processImports(configurationClass, asSourceClass(configurationClass, exclusionFilter),
								Collections.singleton(asSourceClass(entry.getImportClassName(), exclusionFilter)),
								exclusionFilter, false);
					}
					catch (BeanDefinitionStoreException ex) {
						throw ex;
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to process import candidates for configuration class [" +
										configurationClass.getMetadata().getClassName() + "]", ex);
					}
				});
			}
		}

简单总结下这一段代码,Spring处理@Import注解的顺序

  1. 判断@Import引入的类是不是实现了ImportSelector接口。如果是,那在对这个类进行初始化的过程中,会调用他的selectImports方法去加载需要引入的configuration类。而这个selectImports方法,返回的是需要注入的所有类的全类名的一个字符串数组。这也是SpringBoot自动装配部分采用的实现方式。

    而对于实现了DeferredImportSelector子接口的类,则提供了一种按组加载的方式。而这种安组加载的特性更多的可能是体现在对@Order注解和Ordered接口的排序顺序的调整。由全体排序改为安组排序。

  2. 判断@Import引入的类是不是实现了ImportBeanDefinitionRegistrar接口。如果是,那在源码中有一行注释说明了他的处理方式。他提供了一种动态添加BeanDefinition的处理方式。

    // Candidate class is an ImportBeanDefinitionRegistrar ->
    // delegate to it to register additional bean definitions
    
  3. 最后如果引入的只是一个普通的类,那就直接作为一个正常的配置类来处理。加载其中的@Bean等注解。

​ 关于ImportSelector接口和ImportBeanDefinitionRegistrar接口的具体原理,会在后续专门抽出文章来解释,并会附上示例。但是,在这里,我们的重点是SpringBoot,所以只需要有个理解就可以了,因为这对于真正去理解SpringBoot的自动装配原理是非常有用的。

2、@Conditional注解

2.1 Spring中的@Conditional条件注入机制

​ Spring-context包中提供了根据@Conditional标签进行条件判断的机制。一般与@Bean等注解结合使用,提供了IOC的动态注入功能。就是会检查@Conditional中指定的Condition进行条件判断,满足条件就会注入IOC容器,不满足就不会注入。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

	/**
	 * All {@link Condition} classes that must {@linkplain Condition#matches match}
	 * in order for the component to be registered.
	 */
	Class<? extends Condition>[] value();

}

我们可以来一个简单的示例,来实现给不同的操作系统注入不同的Bean

先来实现两个条件判断类,从环境变量中来判断操作系统是windows还是linux。

public class WindowsCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //获取ioc使用的beanFactory
        ConfigurableListableBeanFactory beanFactory = conditionContext.getBeanFactory();
        //获取类加载器
        ClassLoader classLoader = conditionContext.getClassLoader();
        //获取当前环境信息
        Environment environment = conditionContext.getEnvironment();
        //获取bean定义的注册类
        BeanDefinitionRegistry registry = conditionContext.getRegistry();

        //获得当前系统名
        String property = environment.getProperty("os.name");
        //包含Windows则说明是windows系统,返回true
        if (null == property || property.contains("Windows")){
            return true;
        }
        return false;
    }
}

public class LinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        Environment environment = conditionContext.getEnvironment();

        String property = environment.getProperty("os.name");
        if (null != property && property.contains("Linux")){
            return true;
        }
        return false;
    }
}
//根据操作系统判断输入到IOC容器中的systemType是哪种实现。
@Configuration
public class ConditionConfig {

    @Bean(name = "systemType")
    @Conditional(WindowsCondition.class)
    public String helloWindows(){
        return "windows";
    }

    @Bean(name = "systemType")
    @Conditional(LinuxCondition.class)
    public String helloLinx(){
        return "linux";
    }
}

然后就可以从容器中获取这个名为systemType的Bean,看看他是哪种类型。

@SpringBootApplication
public class P1Application implements CommandLineRunner {

    public static void main(String[] args) {
        final SpringApplication application = new SpringApplication(P1Application.class);
//        application.addInitializers(new MyApplicationContextInitializer());
//        application.addListeners(new MyApplicationListener());
        application.run(args);
    }

    @Autowired
    private ApplicationContext applicationContext;


    @Override
    public void run(String... args) throws Exception {
		//条件注入
        System.out.println("current os.name = "+applicationContext.getBean("systemType"));
    }
}

当环境变量os.name为空或者os.name=windows时,就会打印出windows。而如果os.name=linux,就会打印出linux。这样就完成了选择注入。

2.2 SpringBoot提供的增强版条件注入机制

​ spring-boot-autoconfigure模块在Spring的选择注入机制的基础上,提供了一系列的扩展实现。

这里所说的@Conditional是指一系列以Conditional开头的按条件注入Bean的注解,这是spring-boot-autoconfigure提供的一个非常重要的加载机制。几乎所有基于SpringBoot的扩展应用都会使用这一机制,来进行按需加载。这也是理解SpringBoot自动装配核心机制的另一个重要组成部分。

注解 说明
@ConditionalOnSingleCandidate 当给定类型的bean存在并且指定为Primary的给定类型存在时,返回true
@ConditionalOnMissingBean 当给定的类型、类名、注解、昵称在beanFactory中不存在时返回true.各类型间是or的关系
@ConditionalOnBean 与上面相反,要求bean存在
@ConditionalOnMissingClass 当给定的类名在类路径上不存在时返回true,各类型间是and的关系
@ConditionalOnClass 与上面相反,要求类存在
@ConditionalOnCloudPlatform 当所配置的CloudPlatform为**时返回true
@ConditionalOnExpression spel表达式执行为true
@ConditionalOnJava 运行时的java版本号是否包含给定的版本号.如果包含,返回匹配,否则,返回不匹配
@ConditionalOnProperty 要求配置属性匹配条件
@ConditionalOnJndi 给定的jndi的Location 必须存在一个.否则,返回不匹配
@ConditionalOnNotWebApplication web环境不存在时
@ConditionalOnWebApplication web环境存在时
@ConditionalOnResource 要求制定的资源存在

这部分的源码还是比较容易理解的。 相关的各种条件配置的入口都在org.springframework.boot.autoconfigure.condition包下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TrRs37u9-1619776780261)(3.png)]

二、SpringBoot自动装配核心机制解读

对于SpringBoot的自动装配机制,核心还是从@SpringBootApplication这个注解开始。先来看下@SpringBootApplication的注解层次结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WN16aOh5-1619776780264)(4.png)]

可以看到@SpringBootApplication注解最终通过@EnableAutoConfiguration注解引入了两个配置类。AutoConfigrationPackage.Registrar.class和AutoConfigurationImportSelector.class。这其中,

  • AutoConfigurationPackages.Registrar实现了ImportBeanDefinitionRegistrar接口。主要功能是实现对启动类所在包及子包的扫描。也就是说,SpringBoot在启动时只会扫描启动类所在的包及其子包的对象扫描,如果要添加与启动类不在同一个包下的其他类,那就需要另外使用@ComponentScan注解来指定。当然,如果熟悉@Import的机制,自行扩展扫描范围也是可以的。
  • AutoConfigurationImportSelector实现了DeferredImportSelector接口。而这个接口,就是实现SpringBoot自动装配的核心。–你可能会发现,在基于SpringBoot的很多开源组件中,很多功能的核心配置项都是通过@Enablexxx这种格式的注解来加载的,而在这些注解的背后,都会通过@Import直接加载相应的配置。这也是SpringBoot中不言而喻的默契。

所以,你看,如果不对@Impert机制有深入了解的话,对这两个重要的接口很难产生让人眼前一亮的熟悉感觉。那接下来,我们就重点来解析跟SpringBoot自动装配相关的AutoConfigurationImportSelector类。

打开AutoConfigurationImportSelector这个类后,可以看到他有个selectImports方法

	@Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

网上看到很多文章也就信誓旦旦的从这个方法开始解释自动装配的原理了。但是如果打个断点调试一下就会发现,虽然最终确实是通过getAutoConfigurationEntry方法来加载所有spring.factories文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration配置。但是,其实启动过程中并不会进入这个selectImports方法。究其原因,就是因为大部分的开发者对@Import组件的机制并不是很明白。

​ 像上面介绍的,对于DeferredImportSelector接口,Spring的@Import会以一种按组加载的方式来处理,所以,Spring在加载AutoConfigurationImportSelector时,是从这个不起眼的地方进入的。

	@Override
	public Class<? extends Group> getImportGroup() {
		return AutoConfigurationGroup.class;
	}

这个AutoConfigurationGroup是在当前类下定义的一个分组加载的静态子类。而我们要关注的,是这两个方法:

	private static class AutoConfigurationGroup
			implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
		...
		@Override
		public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
			Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
					() -> String.format("Only %s implementations are supported, got %s",
							AutoConfigurationImportSelector.class.getSimpleName(),
							deferredImportSelector.getClass().getName()));
			AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
					.getAutoConfigurationEntry(annotationMetadata); // <====加载自动配置类
			this.autoConfigurationEntries.add(autoConfigurationEntry);
			for (String importClassName : autoConfigurationEntry.getConfigurations()) {
				this.entries.putIfAbsent(importClassName, annotationMetadata);
			}
		}

		@Override
		public Iterable<Entry> selectImports() {
			if (this.autoConfigurationEntries.isEmpty()) {
				return Collections.emptyList();
			}
			Set<String> allExclusions = this.autoConfigurationEntries.stream()
					.map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
			Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
					.map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
					.collect(Collectors.toCollection(LinkedHashSet::new));
			processedConfigurations.removeAll(allExclusions); //<=====去掉需要排除的类

			return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
					.map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
					.collect(Collectors.toList()); //<====组内排序后返回。
		}
    }

这其中 process方法就会完成所有spring.factories文件下org.springframework.boot.autoconfigure.EnableAutoConfiguration的扫描。而selectImports就是将process方法扫描到的结果进行过滤与排序。

而process方法中的getAutoConfigurationEntry就是加载自动配置项的核心所在。

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);
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
				getBeanClassLoader());//<===扫描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;
	}

如果你是从这个系列文章的开头一直看下来的,会不会对这个SpringFactoriesLoader.loadFactoryNames方法产生一种特殊的熟悉感?闭着眼睛一猜就能猜到getSpringFactoriesLoaderFactoryClass方法返回的就是一个干净利落的EnableAutoConfiguration.class。

而在完成了selectImports方法后,就返回到了之前在介绍@Import机制时提到的ConfigurationClassParser,继续进行后续的@Import机制处理。至此,SpringBoot和Spring成为了两个泾渭分明,相辅相成的两个功能模块,而不再是混到一起的模糊概念。而对于我来说,在进行这一番梳理之前,自动装配机制大致也懂,但是却只是一个模糊的整体。

三、SpringBoot当中的默认配置类

把SpringBoot的自动装配机制整个梳理清楚后,再来看spring-boot-autoconfigure中的每个具体的配置类,就容易很多了。

1、spring-boot-autoconfigure中关于数据库的几个配置:

org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration

数据库是我们在开发过程中最为常用的组件了。使用SpringBoot进行数据库操作时,只需要在配置文件中配置spring.datasource相关的几个属性,指定数据库的driverClass、url、usename、password等几个属性,就可以使用IOC容器中的jdbcTemplate来进行JDBC操作了。这种SpringBoot最简单的用法相信大家都是会的,但是要如何去了解背后的实现机制呢?从这些配置类入手,就是最为简单的方式。

首先看这个DataSourceAutoConfiguration,从名字就能理解他是用来注入DataSource的。这个代码就不全部贴了,我们来关注下他庞大的头部注解:

@SuppressWarnings("deprecation")
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class) //<====注入DataSource到IOC容器
@Import({ DataSourcePoolMetadataProvidersConfiguration.class,
		DataSourceInitializationConfiguration.InitializationSpecificCredentialsDataSourceInitializationConfiguration.class,
		DataSourceInitializationConfiguration.SharedCredentialsDataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {

注解中引入的DataSourceProperties类,就提供了DataSource的注入实现。这个@ConfigurationProperties注解就会去解析SpringBoot配置文件当中以prefix(spring.datasource)开头的各种配置,并通过其中属性的setter方法,将配置信息关联到对象实例当中。例如spring.datasource.url配置就会被关联到IOC容器中DataSourceProperties实例的url属性中。

@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
...
}

而接下来,在JdbcTemplateAutoConfiguration这个配置类中,就会通过IOC容器中的DataBase实例,来实例化一个JdbcTemplate。

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {

    //实例化JdbcTemplate
	@Bean
	@Primary
	JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		JdbcProperties.Template template = properties.getTemplate();
		jdbcTemplate.setFetchSize(template.getFetchSize());
		jdbcTemplate.setMaxRows(template.getMaxRows());
		if (template.getQueryTimeout() != null) {
			jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
		}
		return jdbcTemplate;
	}
}

而如果你真的仔细去瞟了一眼源码,你会发现DataSourceAutoConfiguration中注入的DataSource属性中有很多属性是过时了的,也就是不建议用的。而如果没有在IOC中注入DataSource数据源,那在XADataSourceAutoConfiguration中会注入一个支持XA分布式事务的数据源DataSource。而如果要深入理解XA事务处理方式,这也是个不错的入口。

2、SpringBoot中关于GitProperties的配置

接下来我们来玩一个比较冷门的配置:

org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration

我们可以在项目classpath下增加一个git.properties文件。在文件中写入以下内容:

git.branch=1
git.commit.id=2

然后我们就可以在SpringBoot启动后引入一个GtiProperties对象。

@SpringBootApplication
public class P1Application implements CommandLineRunner {

    public static void main(String[] args) {
        final SpringApplication application = new SpringApplication(P1Application.class);
        application.run(args);
    }

    @Autowired
    private GitProperties gitProperties;


    @Override
    public void run(String... args) throws Exception {
        System.out.println("git BranchId="+gitProperties.getBranch()+";git commitId = "+ gitProperties.getCommitId());
    }
}

这样就能打印出结果git BranchId=1 git commitId = 2。

具体的实现机制可以去看下ProjectInfoAutoConfiguration的源码

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ProjectInfoProperties.class)
public class ProjectInfoAutoConfiguration {
	...
    //只要找到了git.properties文件,就会往IOC中注入一个GitProperties对象。
	@Conditional(GitResourceAvailableCondition.class)
	@ConditionalOnMissingBean
	@Bean
	public GitProperties gitProperties() throws Exception {
		return new GitProperties(
				loadFrom(this.properties.getGit().getLocation(), "git", this.properties.getGit().getEncoding()));
	}
 ...   
}

static class GitResourceAvailableCondition extends SpringBootCondition {

		@Override
		public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
			ResourceLoader loader = context.getResourceLoader();
			Environment environment = context.getEnvironment();
            //配置git.properties文件的位置。默认就是classpath:
			String location = environment.getProperty("spring.info.git.location");
			if (location == null) {
				location = "classpath:git.properties";
			}
			ConditionMessage.Builder message = ConditionMessage.forCondition("GitResource");
			if (loader.getResource(location).exists()) {
				return ConditionOutcome.match(message.found("git info at").items(location));
			}
			return ConditionOutcome.noMatch(message.didNotFind("git info at").items(location));
		}

	}

关于这个git.properties文件,是可以由Git仓库生成的。网上查到一个maven插件可以帮我们自动生成这个git.properties文件。

<plugins>
  <plugin>
    <groupId>pl.project13.maven</groupId>
    <artifactId>git-commit-id-plugin</artifactId>
  </plugin>
</plugins>

这个插件我也没用过,有兴趣的可以自己去查查体验一下。

spring-boot-autoconfigure的spring.factories中一共配置了50个自动装配的属性,这里就不再多列举了,有兴趣的话也建议你们自己去看看。当然,有可能很多复杂的功能衍生出来的扩展配置会比SpringBoot的这个自动装配机制还要复杂也说不定,但是这也是SpringBoot的魅力不是吗?