上一篇文章的@AutoConfigurationPackage注解的解析其實是我在複習自動裝配原理的時候突然間注意到的,然後就淺淺的研究了一下,今天的才是主角。
說到Spring自動裝配原理,應該是面試的時候的一道常見的面試題。我入職現在這家公司的時候就被問到了自動裝配原理,當時隻是應付面試而背了面試題,并不知道實際的加載過程,今天正好有時間,我們來好好研究一下,話不多說,開幹!
SPI機制
在說自動裝配之前, 先說一個前置的知識點,就是Spring的SPI(Service Provider Interface)機制,其實這并不是Spring特有的,在很多架構中都有SPI機制。SPI說白了就是一種擴充機制,我們在相應配置檔案中定義好某個接口的實作類,然後再根據這個接口去這個配置檔案中加載這個執行個體類并執行個體化。有了SPI機制,那麼就為一些架構的靈活擴充提供了可能,而不必将架構的一些實作類寫死在代碼裡面。
SPI機制的主要目的:
- 為了解耦,将接口和具體實作分離開來。
- 提高架構的擴充性。以前寫程式的時候,接口和實作都寫在一起,調用方在使用的時候依賴接口來進行調用,無權選擇使用具體的實作類。
在Spring中,SPI機制的實作主要工具是SpringFactoriesLoader類,我們來看一下這個類中加載的方法:
//類中的靜态常量
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
//傳入類名和類加載器
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
//擷取類的全限定類名,以EnableAutoConfiguration為例
//全限定類名:org.springframework.boot.autoconfigure.EnableAutoConfiguration
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
//
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
...
try {
//讀取META-INF檔案夾下的spring.factories檔案的url
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檔案
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
//擷取每一個鍵值對:key=value
for (Map.Entry<?, ?> entry : properties.entrySet()) {
//然後把value值按,切割,放入到result集合中并傳回
List<String> factoryClassNames = Arrays.asList(
StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
result.addAll((String) entry.getKey(), factoryClassNames);
}
}
...
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
說白了就是讀取指定目錄下的檔案,然後根據傳入的key,去将對應的value擷取并執行個體化。
我們找到spring-boot-autoconfigure-jar包下的spring.factories檔案看看,其實可以發現,不隻是自動裝配用到了SPI機制,這種機制在Spring中随處可見。
圖1:spring.factories
BeanDefinition加載的核心:ConfigurationClassPostProcessor
說到BeanDefinition,這裡簡單說一下,BeanDefinition和Bean的關系就是類與對象的關系,在Spring中,是先掃描所有的類然後把每個類封裝成BeanDefinition,BeanDefinition中有類的各種資訊,然後到最後會将BeanDefinition執行個體化成Bean對象。
然後我們來看ConfigurationClassPostProcessor的執行時機,我們自動裝配的代碼原理也是在這個類裡
ConfigurationClassPostProcessor類實作了BeanDefinitionRegistryPostProcessor接口,這是BeanDefinition注冊的後置處理接口,在AbstractApplicationContext#refresh#invokeBeanFactoryPostProcessors方法中被執行,我們一點點來看
我們直接點進SpringApplication的run方法
public ConfigurableApplicationContext run(String... args) {
ConfigurableApplicationContext context = null;
...
try {
...
//ConfigurationClassPostProcessor被注冊為BeanDefinition是在建立上下文時
//圖2
context = createApplicationContext();
...
//invokeBeanFactoryPostProcessors方法在這裡執行,然後ConfigurationClassPostProcessor後置處理器執行
//圖3、圖4
refreshContext(context);
...
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
圖2:注冊ConfigurationClassPostProcessor
圖3:開始執行BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry方法
圖4:執行後置處理方法
當執行ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry方法時,最終會調用到processConfigBeanDefinitions方法中,然後執行parse方法,開始處理我們main方法所在的主類,并開始處理main方法上所有的注解。
圖5:開始執行parse方法
自動裝配原了解析
在我們在main方法上的@SpringBootApplication注解中,有一個@EnableAutoConfiguration注解,在這個注解上,有一個@Import(AutoConfigurationImportSelector.class),而AutoConfigurationImportSelector這個類便是我們自動裝配的核心類,這個類中的selectImports方法中使用了我們的SPI機制,他将EnableAutoConfiguration.class注解類作為key,去我們的META-INF/spring.factories檔案下去尋找key=org.springframework.boot.autoconfigure.EnableAutoConfiguration的鍵值對,并擷取對應的value,然後将value按,進行切割,擷取每一個全限定類名并進行封裝。
這裡會被圖11-2所調用
接上圖
擷取全限定類型
服務啟動後,當執行到ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry方法後便開啟了注解解析之路,該方法會調用ConfigurationClassParser#parse方法,而parse方法中分為兩步(圖6所示):
圖6:兩步如圖
圖6-1:實際調用的是processConfigurationClass
1、通過doProcessConfigurationClass核心方法會解析main方法啟動類上的所有注解(processConfigurationClass會調用doProcessConfigurationClass方法,圖12),在doProcessConfigurationClass中的processImports方法會處理經過parse方法處理擷取的@Import注解中的類(圖7)。而processImports中将@Import注解中的類分為三種(圖8、圖9):
圖7:doProcessConfigurationClass中處理import注解
圖8:處理@Import中的三種類型
圖9:這也是個遞歸方法
我們的AutoConfigurationImportSelector類便是實作了ImportSelector接口,是以加入到deferredImportSelectors集合中(圖10)。
圖10:AutoConfigurationImportSelector被掃描
2、上面parse方法執行完後,通過processDeferredImportSelectors方法處理後的@Import方法中的類,即AutoConfigurationImportSelector類。上面我們也說了該類的作用,是以這裡可以調用AutoConfigurationImportSelector的process方法拿到所有的自動裝配類(圖11-1、圖11-2),然後循環再次調用processImports類(圖11),此時将進入到圖8的最下面的紅色标記處,調用processConfigurationClass類去處理(圖12),是以形成了遞歸(圖13)
圖11:擷取自動裝配類循環調用processImports
圖11-1:查詢所有的自動裝配類,調用AutoConfigurationImportSelector的process方法
圖11-2:AutoConfigurationImportSelector的process方法
圖12:遞歸調用processConfigurationClass
圖13:自動裝配類調用
我們可以看一下doProcessConfigurationClass方法中部分代碼:
protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
throws IOException {
// 解析 @PropertySource annotations
for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), PropertySources.class,
org.springframework.context.annotation.PropertySource.class)) {
if (this.environment instanceof ConfigurableEnvironment) {
processPropertySource(propertySource);
}
else {
logger.warn("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
"]. Reason: Environment must implement ConfigurableEnvironment");
}
}
// 解析 @ComponentScan annotations,這裡會掃描到我們指定的包,然後将掃描到的類都封裝成BeanDefinition
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
for (AnnotationAttributes componentScan : componentScans) {
// The config class is annotated with @ComponentScan -> perform the scan immediately
Set<BeanDefinitionHolder> scannedBeanDefinitions =
this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
// Check the set of scanned definitions for any further config classes and parse recursively if needed
for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
if (bdCand == null) {
bdCand = holder.getBeanDefinition();
}
if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
parse(bdCand.getBeanClassName(), holder.getBeanName());
}
}
}
}
// Process any @Import annotations
//這裡會解析到所有的@Import注解
processImports(configClass, sourceClass, getImports(sourceClass), true);
...
// No superclass -> processing is complete
return null;
}
Spring自動裝配實踐
通過上面的分析,我們知道了自動裝配的原理。那麼接下來,我們自己動手實踐一下模拟建立一個starter
建立一個模闆,假設叫common-starter子產品,裡面我們建立一個接口
@RequestMapping("/abs/health")
public class HealthCheckController {
@GetMapping(value = "/check")
public BaseRspsMsg check() {
log.debug(" ==========>健康檢查");
BaseRspsMsg baseRspsMsg = null;
try {
baseRspsMsg = BaseRspsMsg.ok("check success");
} catch (Exception e) {
baseRspsMsg = BaseRspsMsg.build(BaseRspsMsg.BIZ_CODE_00001_FAILED, "check fail");
}
return baseRspsMsg;
}
}
建立一個自動裝配類
@Configuration
//掃描指定的類
@ComponentScan(basePackageClasses = HealthCheckController.class)
public class DemoAutoConfiguration {
//這裡我們建立一個bean
@Bean
public AgentInfosRsp agentInfosRsp(){
AgentInfosRsp agentInfosRsp = new AgentInfosRsp();
agentInfosRsp.setAgentID("777");
agentInfosRsp.setAgentName("777");
agentInfosRsp.setLocation("河北邯鄲");
return agentInfosRsp;
}
}
建立spring.factories檔案
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.pub.config.DemoAutoConfiguration
現在我們在項目中引入這個starter的pom檔案,然後調用check接口
確定服務啟動類沒有掃到指定路徑:
啟動我們的服務并調用http://127.0.0.1:9361/abs/health/check
調用成功!
我們再通過SpringUtil擷取我們建立的bean
@RequestMapping("/api/prov")
@RestController
public class SendToPlatformController implements ApplicationContextAware {
ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext=applicationContext;
}
@GetMapping("/aaa")
public String aaa() {
return JSON.toJSONString(applicationContext.getBean(AgentInfosRsp.class));
}
}
擷取成功!
我們發現我們建立的bean成功被Spring掃描到并放入Spring的IOC容器中,說明我們的自動裝配是成功的。