现在SpringBoot项目已经普及,对于@SpringBootApplication注解肯定也不陌生。另外在SpringBoot项目下,我们也进行使用自动配置功能,也就是引入某个模块的jar包,SpringBoot就会自动配置对应功能,很容易就在SpringBoot项目下进行使用。本文主要就是分析SpringBoot自动配置的底层原理。不过还是要从@SpringBootApplication注解开始。
1. @SpringBootApplication
该注解包含了三个注解,@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan
- @SpringBootConfiguration注解内容会标记@Configuration注解,也就是会作为一个配置类,该类上面可以做一些Bean的配置,以及该类也是一个Bean会在Spring IOC容器里。
- @ComponentScan包的扫描路径,所以在模块情况下会扫描@SpringBootApplication所在的包路径。
- @EnableAutoConfiguration开启自动配置,也是本文重点要分析的内容。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
2. @EnableAutoConfiguration
Spring Boot实现自动装配,依赖@EnableAutoConfiguration注解背后的逻辑。在@SpringBootApplication注解下,会标记@EnableAutoConfiguration注解。
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
@EnableAutoConfiguration注解内部,通过@Import注解实现配置类的扫描,其中扫描的逻辑在AutoConfigurationImportSelector中,该注解还能通过执行字段排除不需要的加载的配置类。
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
2.1 AutoConfigurationImportSelector
AutoConfigurationImportSelector实现了ImportSelector接口,所以就会实现selectImports方法。
实际上AutoConfigurationImportSelector实现的是DeferredImportSelector接口,内部有个getImportGroup方法,SpringBoot真正执行的会是DeferredImportSelector$Group类。这里的逻辑比较复杂,先以selectImports方法串通逻辑,整体逻辑实际是类似的。
主要有三个逻辑:判断是否支持自动装配;获取元信息;获取配置类信息,内部会进行过滤筛选。
//AutoConfigurationImportSelector#selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//1.是否支持自动装配
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//2. 获取元信息
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
//3. 获取配置类信息
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
2.1.1 是否支持自动装配
首先判断当前的class是不是AutoConfigurationImportSelector,现在查看的就是AutoConfigurationImportSelector,所以条件成立。然后会判断环境变量中的spring.boot.enableautoconfiguration是否有配置,如果没有配置默认为true。也就是说,可以在applicaiton.yml中配置spring.boot.enableautoconfiguration为false,关闭自动装配。
//AutoConfigurationImportSelector#isEnabled
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}
2.1.2 获取元信息
逻辑就是从META-INF/spring-autoconfigure-metadata.properties文件中,读取数据,封装成PropertiesAutoConfigurationMetadata,而该文件可能有多个,也就是说每个jar包内部可能都会包含该文件。
//AutoConfigurationMetadataLoader
protected static final String PATH = "META-INF/spring-autoconfigure-metadata.properties";
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader) {
return loadMetadata(classLoader, PATH);
}
static AutoConfigurationMetadata loadMetadata(ClassLoader classLoader, String path) {
try {
Enumeration<URL> urls = (classLoader != null) ? classLoader.getResources(path)
: ClassLoader.getSystemResources(path);
Properties properties = new Properties();
while (urls.hasMoreElements()) {
properties.putAll(PropertiesLoaderUtils.loadProperties(new UrlResource(urls.nextElement())));
}
return loadMetadata(properties);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load @ConditionalOnClass location [" + path + "]", ex);
}
}
static AutoConfigurationMetadata loadMetadata(Properties properties) {
return new PropertiesAutoConfigurationMetadata(properties);
}
那么spring-autoconfigure-metadata.properties怎么来的?内容是什么?以及作用是什么?
首先该文件可以手动填写(待验证),但是Spring Boot也提供了工具,也就是spring-boot-autoconfigure-processor模块,该模块会在编译期间就会根据注解信息,生成spring-autoconfigure-metadata.properties文件。spring-boot-autoconfigure模块,内部就依赖了该模块,所以从源码中看不到spring-autoconfigure-metadata.properties文件,在编译后才能看到。
以spring-boot-autoconfigure为例,查看编译后的spring-autoconfigure-metadata.properties文件,可以发现对于WebMvcAutoConfiguration配置类中的@ConditionalOnClass条件注解需要Servlet、DispatcherServlet、WebMvcConfigurer,这个文件中已经把类路径给解析出来了。那么作用也很清楚了,就是通过编译期间的扫描,减少运行时Conditional注解解析,加快启动时间。
...
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.ConditionalOnClass=javax.servlet.Servlet,org.springframework.web.servlet.config.annotation.WebMvcConfigurer,org.springframework.web.servlet.DispatcherServlet
2.1.3 获取配置类信息
//AutoConfigurationImportSelector#getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
//判断是否自动装配,因为是个通用接口,所以这里又判断了一次
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
//1. 获取注解属性
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//2. 获取配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//3. 去重
configurations = removeDuplicates(configurations);
//4. 排除配置类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
//5. 过滤
configurations = filter(configurations, autoConfigurationMetadata);
//6. 通知
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
获取注解属性,就会从会@EnableAutoConfiguration注解中获取属性,也就是exclude、excludeName这两个参数。
//AutoConfigurationImportSelector#getAttributes
protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
String name = getAnnotationClass().getName();
AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
return attributes;
}
protected Class<?> getAnnotationClass() {
return EnableAutoConfiguration.class;
}
获取配置类,就是会利用SpringFactoriesLoader类进行获取配置信息,SpringFactoriesLoader内部实际会从META-INF/spring.factories文件读取所需的类,
这里的类就是EnableAutoConfiguration。记住该类的使用,后面的场景还会用到该类获取其他信息。
SpringFactoriesLoader类是由Spring-Core提供的类
//AutoConfigurationImportSelector#getCandidateConfigurations
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
return configurations;
}
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
以spring-boot-autoconfigure模块为例,resources下的spring.factories文件如下,其中可以看到EnableAutoConfiguration相关的配置类信息。
...
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...
因为每个依赖下都可能又spring.factories文件,所以需要对获取的配置类进行去重。去重原理也很简单,就是利用Set结构。
//AutoConfigurationImportSelector#removeDuplicates
protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
获取排除信息就会从注解的exclude和excludeName中获取,同时还会从环境变量spring.autoconfigure.exclude中获取信息。
//AutoConfigurationImportSelector#getExclusions
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
紧接着,会把排除信息进行校验,是否合法,如果不合法则会抛出异常(在handleInvalidExcludes方法内)。
//AutoConfigurationImportSelector#checkExcludedClasses
private void checkExcludedClasses(List<String> configurations, Set<String> exclusions) {
List<String> invalidExcludes = new ArrayList<>(exclusions.size());
for (String exclusion : exclusions) {
if (ClassUtils.isPresent(exclusion, getClass().getClassLoader()) && !configurations.contains(exclusion)) {
invalidExcludes.add(exclusion);
}
}
if (!invalidExcludes.isEmpty()) {
handleInvalidExcludes(invalidExcludes);
}
}
校验完后,会把配置类信息根据排除类,进行移除。
//AutoConfigurationImportSelector#getAutoConfigurationEntry
configurations.removeAll(exclusions);
然后会根据条件进行过滤,因为并不是所有的xxxAutoConfiguration都是需要进行加载的。getAutoConfigurationImportFilters()方法内,就会通过SpringFactoriesLoader获取AutoConfigurationImportFilter类,也就是会在spring.factories文件中获取该类。
//AutoConfigurationImportSelector#filter
private List<String> filter(List<String> configurations, AutoConfigurationMetadata autoConfigurationMetadata) {
long startTime = System.nanoTime();
String[] candidates = StringUtils.toStringArray(configurations);
boolean[] skip = new boolean[candidates.length];
boolean skipped = false;
for (AutoConfigurationImportFilter filter : getAutoConfigurationImportFilters()) {
invokeAwareMethods(filter);
boolean[] match = filter.match(candidates, autoConfigurationMetadata);
for (int i = 0; i < match.length; i++) {
if (!match[i]) {
skip[i] = true;
candidates[i] = null;
skipped = true;
}
}
}
if (!skipped) {
return configurations;
}
List<String> result = new ArrayList<>(candidates.length);
for (int i = 0; i < candidates.length; i++) {
if (!skip[i]) {
result.add(candidates[i]);
}
}
if (logger.isTraceEnabled()) {
int numberFiltered = configurations.size() - result.size();
logger.trace("Filtered " + numberFiltered + " auto configuration class in "
+ TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime) + " ms");
}
return new ArrayList<>(result);
}
在spring-boot-autoconfigure模块中,AutoConfigurationImportFilter的配置为OnBeanCondition、OnClassCondition、OnWebApplicationCondition。
- OnBeanCondition类处理@ConditionalOnBean、@ConditionalOnMissingBean、@ConditionalOnSingleCandidate注解。
- OnClassCondition类处理@ConditionalOnClass、@ConditionalOnMissingClass注解
- OnWebApplicationCondition类处理@ConditionalOnWebApplication、@ConditionalOnNotWebApplication注解。
至于其他注解,则需要在配置类在Spring中解析的时候,进行判断是否要配置,如:@ConditionalOnProperty注解。
实际代码处理在ConfigurationClassParser类中的processConfigurationClass方法,内部会通过ConditionEvaluator判断是否跳过。
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
过滤的逻辑比较复杂,核心会通过类是否存在进行判断,后文会做简要分析。
最后会从SpringFactoriesLoader获取AutoConfigurationImportListener类,然后会触发onAutoConfigurationImportEvent方法。业务上可以根据需要,实现自己的AutoConfigurationImportListener,在spring.factories上进行配置。
//AutoConfigurationImportSelector#fireAutoConfigurationImportEvents
private void fireAutoConfigurationImportEvents(List<String> configurations, Set<String> exclusions) {
List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
if (!listeners.isEmpty()) {
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this, configurations, exclusions);
for (AutoConfigurationImportListener listener : listeners) {
invokeAwareMethods(listener);
listener.onAutoConfigurationImportEvent(event);
}
}
}
2.2 条件判断
以OnClassCondition为例,内部继承了FilteringSpringBootCondition类和SpringBootCondition类,SpringBootCondition类实现了Condition接口,提供了抽象方法getMatchOutcome(),所以看OnClassCondition中实现的getMatchOutcome()逻辑,就是条件判断逻辑。
前面提到,OnClassCondition类处理@ConditionalOnClass、@ConditionalOnMissingClass这两注解,逻辑都类似,这里主要分析@ConditionalOnClass注解的处理。
主要有两个逻辑,首先从@ConditionalOnClass注解中,找出属性值,内部会把class也转成String的方式。其次就是会过滤,找过不存在的class,方便提示。
//OnClassCondition#getMatchOutcome
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
//从注解中找出属性值,class也会转成string
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {
//过滤是否有未找到的class,这样做是为了做提示
List<String> missing = filter(onClasses, ClassNameFilter.MISSING, classLoader);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes").items(Style.QUOTE, missing));
}
matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
.found("required class", "required classes")
.items(Style.QUOTE, filter(onClasses, ClassNameFilter.PRESENT, classLoader));
}
//...
return ConditionOutcome.match(matchMessage);
}
过滤的逻辑主要是调用ClassNameFilter.matches方法,根据传参,这里是ClassNameFilter.MISSING这个枚举类。
//OnClassCondition#filter
protected final List<String> filter(Collection<String> classNames, ClassNameFilter classNameFilter,
ClassLoader classLoader) {
if (CollectionUtils.isEmpty(classNames)) {
return Collections.emptyList();
}
List<String> matches = new ArrayList<>(classNames.size());
for (String candidate : classNames) {
if (classNameFilter.matches(candidate, classLoader)) {
matches.add(candidate);
}
}
return matches;
}
ClassNameFilter.MISSING内的逻辑,就是判断类不存在,就是尝试用类加载器加载类,如果加载不了就说明不存在。
//FilteringSpringBootCondition$ClassNameFilter
protected enum ClassNameFilter {
MISSING {
@Override
public boolean matches(String className, ClassLoader classLoader) {
return !isPresent(className, classLoader);
}
};
abstract boolean matches(String className, ClassLoader classLoader);
static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
resolve(className, classLoader);
return true;
}
catch (Throwable ex) {
return false;
}
}
}
//FilteringSpringBootCondition#resolve
protected static Class<?> resolve(String className, ClassLoader classLoader) throws ClassNotFoundException {
if (classLoader != null) {
return classLoader.loadClass(className);
}
return Class.forName(className);
}
综上,可以做个小结
- OnClassCondition内部过滤的逻辑就是判断对应的class是否存在,底层实现就是通过类加载尝试加载判断,没有抛异常则说明存在。
- OnBeanCondition内部过滤的逻辑就是判断对应的bean是否存在,底层就是通过BeanFactory查找对应的类型/名称进行判断。
- OnWebApplicationCondition内部过滤的逻辑就是判断当前的web类型是否符合,底层实现就是对应web类型的类存在,以及对应的web配置信息进行判断。
3. 自定义Starter
从上面自动配置的逻辑,我们可以看出,主要的流程,再结合官方的一些说明,可以整理出自定义Starter需要的步骤:
- 首先需要设置配置类,一般命名为xxxAutoConfiguration,然后该类上标记@Configuration注解,根据需要可以标记@ConditionalOnClass等注解,当类不存在时不会进行装配。内部的Bean也可以定义成@ConditionalOnMissingBean之类的,便于把实现类进行替换。(因为其他Bean的加载,在自动装配之前,所以可以判断是否有定义其他类)
- 在resource下的
META-INF/spring.factories文件下配置org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx.xxxx.xxxAutoConfiguration。 - (可选)最好能依赖spring-boot-autoconfigure-processor模块,这样在编译的时候就会生成
spring-autoconfigure-metadata.properties文件,便于自动配置时进行过滤。不过一般都会依赖到spring-boot-autoconfigure模块,该模块内部依赖spring-boot-autoconfigure-processor。(不确定是否具有传递性)
3.1 SpringBoot官方starter
Spring Boot内部有很多定义好的的Starter,比如spring-boot-starter-web,但是它们并没有定义resource,这是因为SpringBoot把这些配置全部都配置在了spring-boot-autoconfigure模块,该模块内部的META-INF/spring.factories就包括了所有SpringBoot官方提供的AutoConfiguration。
3.2 dubbo的starter
我们可以看下dubbo是怎么配置自定的spring boot starter。dubbo-spring-boot-starter模块依赖了spring-boot-starter、dubbo-spring-boot-autoconfigure模块,自动配置在dubbo-spring-boot-autoconfigure模块中。
dubbo-spring-boot-autoconfigure的META-INF/spring.factories文件内容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration
DubboRelaxedBinding2AutoConfiguration配置类如下,和前面描述的逻辑类似。
@Configuration
@ConditionalOnProperty(prefix = DUBBO_PREFIX, name = "enabled", matchIfMissing = true)
@ConditionalOnClass(name = "org.springframework.boot.context.properties.bind.Binder")
@AutoConfigureBefore(DubboRelaxedBindingAutoConfiguration.class)
public class DubboRelaxedBinding2AutoConfiguration {}
3.3 FAQ
-
为什么有需要有@Configuration注解,还需要把xxxAutoConfiguration添加到spring.factories文件中?虽然有@Configuration注解,但是xxxxxxAutoConfiguration所在路径不一定能被扫描到。
-
为什么需要设置@Configuration注解?Spring底层解析需要,没有该注解无法解析配置类。
4. 参考资料
-
SpringBoot源码分支2.2.x