Spring Boot @EnableAutoConfiguration解析

剛作後端開發的時候,最先接觸的是基礎的spring,爲了引用二方包提供bean,還須要在xml中增長對應的包<context:component-scan base-package="xxx" />或者增長註解@ComponentScan({ "xxx"})。當時以爲挺urgly的,但也沒有去研究有沒有更好的方式。java

直到接觸Spring Boot 後,發現其能夠自動引入二方包的bean。不過一直沒有看這塊的實現原理。直到最近面試的時候被問到。因此就看了下實現邏輯。git

更多文章見我的博客:github.com/farmerjohng…github

使用姿式

講原理前先說下使用姿式。面試

在project A中定義一個bean。spring

package com.wangzhi;

import org.springframework.stereotype.Service;

@Service
public class Dog {
}

複製代碼

並在該project的resources/META-INF/下建立一個叫spring.factories的文件,該文件內容以下後端

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.wangzhi.Dog
複製代碼

而後在project B中引用project A的jar包。springboot

projectA代碼以下:bash

package com.wangzhi.springbootdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@EnableAutoConfiguration
public class SpringBootDemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(SpringBootDemoApplication.class, args);
        System.out.println(context.getBean(com.wangzhi.Dog.class));
    }

}

複製代碼

打印結果:this

com.wangzhi.Dog@3148f668
複製代碼

原理解析

整體分爲兩個部分:一是收集全部spring.factoriesEnableAutoConfiguration相關bean的類,二是將獲得的類註冊到spring容器中。url

收集bean定義類

在spring容器啓動時,會調用到AutoConfigurationImportSelector#getAutoConfigurationEntry

protected AutoConfigurationEntry getAutoConfigurationEntry( AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // EnableAutoConfiguration註解的屬性:exclude,excludeName等
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 獲得全部的Configurations
    List<String> configurations = getCandidateConfigurations(annotationMetadata,
            attributes);
    // 去重
    configurations = removeDuplicates(configurations);
    // 刪除掉exclude中指定的類
    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會調用到方法loadFactoryNames

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        // factoryClassName爲org.springframework.boot.autoconfigure.EnableAutoConfiguration
		String factoryClassName = factoryClass.getName();
        // 該方法返回的是全部spring.factories文件中key爲org.springframework.boot.autoconfigure.EnableAutoConfiguration的類路徑
		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
	}


public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
		MultiValueMap<String, String> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}

		try {
            // 找到全部的"META-INF/spring.factories"
			Enumeration<URL> urls = (classLoader != null ?
					classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
					ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
			result = new LinkedMultiValueMap<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
                // 讀取文件內容,properties相似於HashMap,包含了屬性的key和value
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryClassName = ((String) entry.getKey()).trim();
                    // 屬性文件中能夠用','分割多個value
					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.factories中指定的bean的類路徑,在processGroupImports方法中會以處理@Import註解同樣的邏輯將其導入進容器。

public void processGroupImports() {
    for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
        // getImports即上面獲得的全部類路徑的封裝
        grouping.getImports().forEach(entry -> {
            ConfigurationClass configurationClass = this.configurationClasses.get(
                    entry.getMetadata());
            try {
                // 和處理@Import註解同樣
                processImports(configurationClass, asSourceClass(configurationClass),
                        asSourceClasses(entry.getImportClassName()), false);
            }
            catch (BeanDefinitionStoreException ex) {
                throw ex;
            }
            catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration class [" +
                                configurationClass.getMetadata().getClassName() + "]", ex);
            }
        });
    }
}

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
	...
    // 遍歷收集到的類路徑
    for (SourceClass candidate : importCandidates) {
       ...
        //若是candidate是ImportSelector或ImportBeanDefinitionRegistrar類型其處理邏輯會不同,這裏不關注
     	// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
						// process it as an @Configuration class
						this.importStack.registerImport(
								currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
		// 看成 @Configuration 處理 
        processConfigurationClass(candidate.asConfigClass(configClass));
   ...
}
            
    ...
}
複製代碼

能夠看到,在第一步收集的bean類定義,最終會被以Configuration同樣的處理方式註冊到容器中。

End

@EnableAutoConfiguration註解簡化了導入了二方包bean的成本。提供一個二方包給其餘應用使用,只須要在二方包裏將對外暴露的bean定義在spring.factories中就行了。對於不須要的bean,能夠在使用方用@EnableAutoConfigurationexclude屬性進行排除。

相關文章
相關標籤/搜索