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.port
、logging.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容器中,实现自动配置功能!
3、@Conditional
派生注解
SpringBoot在启动的时候为我们加载了这么多组件,我们不可能全部用得上,那如果用不上的还注册进容器,岂不是耗费资源。其实底层使用了条件装配@Conditional,在我们需要的情况下才会注册对应的组件。
比如,SpringBoot中对AOP
的自动配置如下所示:
AOP的自动配置中使用了非常多的条件注解:
@ConditionalOnClass({EnableAspectJAutoProxy.class, Aspect.class, Advice.class, AnnotatedElement.class})
表示当 类路径上存在EnableAspectJAutoProxy
、Aspect
、Advice
、AnnotatedElement
时才会自动配置@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环境 |
@ConditionalOnJndi | JNDI存在指定项 |
Q:如何知道哪些自动配置类生效了,哪些配置类没有生效?
A: 我们可以通过在主配置文件中配置 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪些自动配置类生效。如下:
