SpringBoot从入门到精通—自动配置原理(深入源码)

1、引言

不论在工作中,亦或是求职面试,Spring Boot已经成为我们必知必会的技能项。除了某些老旧的政府项目或金融项目持有观望态度外,如今的各行各业都在飞速的拥抱这个已经不是很新的Spring启动框架。

当然,作为Spring Boot的精髓,自动配置原理的工作过程往往只有在“面试”的时候才能用得上,但是如果在工作中你能够深入的理解Spring Boot的自动配置原理,将无往不利。

Spring Boot的出现,得益于“约定优于配置”的理念,没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成),这是基于Spring 4.x提供的按条件配置Bean的能力。

2、 Spring Boot自动配置原理

2.1 配置文件到底能写什么?怎么写?

我们一接触Spring Boot的时候就了解到:Spring Boot有一个全局配置文件application.properties或application.yml。我们的各种属性都可以在这个文件中进行配置,最常配置的比如:server.portlogging.level.*数据库配置Redis配置 等等,然而我们实际用到的往往只是很少的一部分,而且可以配置的属性都可以在官方文档中查找到。

2.2 自动配置原理?

(1)@SpringBootApplication

每个SpringBoot应用都有一个启动类,所有的SpringBoot项目必须从启动类启动(一般情况下SpringBoot启动类类名就是项目名+Application),启动类上必须使用@SpringBootApplication注解标注而且主程序类必须位于项目基包的根目录。这个注解是Spring Boot的核心注解,标有这个注解的应用就是基于SpringBoot的应用,我们的自动配置原理和这个注解还有着千丝万缕的联系!我们可以点看进去看看。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited  //上面都是Java原生的注解,不清楚意思的请自行百度或Google
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
   //省略
}

@SpringBootApplication注解是一个复合注解,这里有三个重要注解:

  • @SpringBootConfiguration:这也是一个复合注解,底层使用的是Spring的基础注解**@Configuration**,它的作用就是让SpringBoot引用支持JavaConfig的配置方式进行配置
  • @EnableAutoConfiguration:开启自动配置,后面着重说明它的原理。
  • @ComponentScan:开启包扫描,也是Spring的基础注解。默认扫描的是当前类下的包。这就是要把启动类放置到基包根目录下的原因,这样启动类就可以在启动的时候把包下的@Controller/@Service/@Component/@Repository 等注解类加载到IOC容器中

(2)@SpringBootConfiguration注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    //省略
}

@SpringBootConfiguratuon注解的作用是用来告诉SpringBoot这是一个SpringBoot的配置类,从他的源码可以看出它是@Configuration注解的派生注解,对于这个注解我们应该不会陌生,这个类是Spring的核心注解之一。因此可以知道的是这两个注解的功能大致是一样的,只不过一个是专门用于SpringBoot的配置类,另一个则在Spinrg体系中是通用的。

(3)@EnableAutoConfiguration注解

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

我们点进去看一下,发现有两个比较重要的注解:

  • @AutoConfigurationPackage:自动配置包
  • @Import:给IOC容器导入组件

(3.1)@AutoConfigurationPackage注解

这个注解的作用即使自动配置包,查看源码我们可以发现,依靠的还是**@Import**注解,它使用@Import注解导入再点进去查看,我们发现重要的就是以下的代码:

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

在默认的情况下就是将主配置类(@SpringBootApplication)的所在包及其子包里边的组件扫描到Spring容器中。

看完这句话,会不会觉得,这不就是@ComponentScan注解的功能吗?这俩不就重复了吗?

我开始也有这个疑问,直到我看到文档的这句话:

it will be used when scanning for code @Entity classes.It is generally recommended that you place EnableAutoConfiguration (if you're not using @SpringBootApplication) in a root package so that all sub-packages and classes can be searched.

比如说,你用了Spring Data JPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。

因此这二者扫描的对象是不一样的。

(3.2)@Import注解

SpringBoot自动配置功能就是由Import注解后面那个类AutoConfigurationImportSelector实现的(1.5版本以前使用EnableAutoConfigurationImportSelector类,1.5以后这个类废弃了使用的是AutoConfigurationImportSelector类)。这个类会调用SpringFactoriesLoader.loadFactoryNames()方法来扫描定义在MEAT-INF/spring.factories文件中的jar包

AutoConfigurationImportSelector实现了ImportSelector接口,这个接口中只有一个方法selectImports(),它的作用是控制哪些类自动的实例加入到容器中,在Spring Boot 2.1.7版本中得源码如下:

@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() 方法的源码中使用了getAutoConfigurationEntry() 这个方法,其中configurations存放的数据就是加入容器的自动配置类的完整包路径,继续追踪源码,进入getAutoConfigurationEntry()源码:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
		AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	
	//扫描具有META-INF/spring.factories文件的jar包
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	//去掉重复的配置
	configurations = removeDuplicates(configurations);
	//删除需要排除的类
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	configurations = filter(configurations, autoConfigurationMetadata);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

从getCandidateConfigurations()方法的实现中,我们发现他调用SpringFactoriesLoader.loadFactoryNames反返回了List集合,返回的东西是啥呢?我们直接追踪源码:

通过DEBUG可以看到,返回对List中全部是一些类的全限定名。

如果继续往下追踪发现他最终在loadFactories方法中使用本类的一个私有静态方法loadSpringFactories加载了META-INF/spring.factories这个配置文件

public final class SpringFactoriesLoader {

	/**
	 * The location to look for factories.
	 * Spring启动的时候会扫描所有jar路径下的META-INF/spring.factories,将其文件包装成Properties对象
	 */
	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";


	public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
	    //获取加载spring.factories的类的类名,一般默认就是EnableAutoConfiguration
		String factoryClassName = factoryClass.getName();
		//这个方法中最终通过Properties类将配置文件加载到系统中
		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 {
		    //获取系统中所有的spring.factories文件的路径
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			//遍历路径集合,把文件中的类包装成一个个Properties
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				//Properties加载配置文件到系统中
				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);
		}
	}
}

查看spring-boot-autoconfigure包下META-INF/spring.factories:

看到的非常多的xxxxAutoConfiguration类,这些类都是容器中的一个组件,加入到容器中,用他们做自动配置。

2.2 Spring Boot自动配置原理总结

@SpringBootApplication等同于下面三个注解:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

其中@EnableAutoConfiguration是关键(启用自动配置),内部实际上就去加载META-INF/spring.factories文件的信息,然后筛选出以EnableAutoConfiguration为key的数据,加载到IOC容器中,实现自动配置功能!

官网参考文档:https://docs.spring.io/spring-boot/docs/2.2.0.BUILD-SNAPSHOT/reference/html/using-spring-boot.html#using-boot-structuring-your-code

3、@Conditional 派生注解

SpringBoot在启动的时候为我们加载了这么多组件,我们不可能全部用得上,那如果用不上的还注册进容器,岂不是耗费资源。其实底层使用了条件装配@Conditional,在我们需要的情况下才会注册对应的组件。

比如,SpringBoot中对AOP的自动配置如下所示:

AOP的自动配置中使用了非常多的条件注解:

  • @ConditionalOnClass({EnableAspectJAutoProxy.class, Aspect.class, Advice.class, AnnotatedElement.class}) 表示当 类路径上存在EnableAspectJAutoProxyAspectAdviceAnnotatedElement时才会自动配置
  • @ConditionalOnProperty(prefix = "spring.aop", name = {"auto"},havingValue = "true",matchIfMissing = true) 表示当配置文件中存在配置项spring.aop.auto=true 时会自动配置,不过这里默认值就是true,所以默认会配置
  • @ConditionalOnProperty( prefix = "spring.aop",name = {"proxy-target-class"}, havingValue = "true/false",matchIfMissing = true/false ) 表示当配置文件中存在配置项 spring.aop.proxy-target-class并且当它的值为true的时候会使用CGLib代理,否则默认会使用JDK动态代理

在SporingBoot源码中下方这些Condition注解比较常见:

Conditional扩展注解作用(判断是否满足当前指定条件)
@ConditionalOnJava系统的java版本是否符合要求
@ConditionalOnBean容器中存在指定Bean;
@ConditionalOnMissingBean容器中不存在指定Bean;
@ConditionalOnExpression满足SpEL表达式指定
@ConditionalOnClass类路径上有指定的类
@ConditionalOnMissingClass系统中没有指定的类
@ConditionalOnSingleCandidate容器中只有一个指定的Bean,或者这个Bean是首选Bean
@ConditionalOnProperty系统中指定的属性是否有指定的值
@ConditionalOnResource类路径下是否存在指定资源文件
@ConditionalOnWebApplication当前是web环境
@ConditionalOnNotWebApplication当前不是web环境
@ConditionalOnJndiJNDI存在指定项

Q如何知道哪些自动配置类生效了,哪些配置类没有生效?

A: 我们可以通过在主配置文件中配置 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效。如下:

留言区

还能输入500个字符