前言
在使用Spring-Cloud微服務框架的時候,對於@Import和@ImportResource這兩個註解想必大家並不陌生。我們會經常用@Import來匯入配置類或者匯入一個帶有@Component等註解要放入Spring容器中的類;用@ImportResource來匯入一個傳統的xml配置檔案。另外,在啟用很多元件時,我們會用到一個形如@EnableXXX的註解,比如@EnableAsync、@EnableHystrix、@EnableApollo等,點開這些註解往裡追溯,你也會發現@Import的身影。如此看來,這兩個註解與我們平時的開發關係密切,但大家知道它們是如何發揮作用的嗎?下面就一起探索一下。
正文
首先看這兩個註解的路徑,它們都位於org.springframework.context.annotation包下,可以說是根正苗紅的Spring註解,所以對這兩個註解的處理,更多的也是在原有的Spring框架中進行的。在Spring-Cloud啟動類的run方法中,通過簡單的追溯我們可以定位到這個run方法(僅部分程式碼):
1 public ConfigurableApplicationContext run(String... args) { 2 StopWatch stopWatch = new StopWatch(); 3 stopWatch.start(); 4 ConfigurableApplicationContext context = null; 5 Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList(); 6 this.configureHeadlessProperty(); 7 SpringApplicationRunListeners listeners = this.getRunListeners(args); 8 listeners.starting(); 9 10 Collection exceptionReporters; 11 try { 12 ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); 13 ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments); 14 this.configureIgnoreBeanInfo(environment); 15 Banner printedBanner = this.printBanner(environment); 16 context = this.createApplicationContext(); 17 exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context); 18 this.prepareContext(context, environment, listeners, applicationArguments, printedBanner); 19 this.refreshContext(context); 20 this.afterRefresh(context, applicationArguments); 21 stopWatch.stop();
可以看到,在19行的位置,呼叫 refreshContext方法,看到這裡,想必都會想到Spring中大名鼎鼎的refresh方法,確實如此,正是在這個方法裡面完成了對refresh方法的呼叫。對這兩個註解的處理,應該還是落在refresh方法中。
這時就需要參考之前一篇博文中的內容了(地址https://www.cnblogs.com/zzq6032010/p/11031214.html)。我們知道在初始化ApplicationContext容器的時候,會初始化AnnotationBeanDefinitionReader類,在初始化此類的時候Spring會通過硬編碼的形式強行給容器中注入一個元處理器類ConfigurationClassPostProcessor。而Spring Cloud中是在哪裡注入的此元處理器類?回到上面的run方法中,點開第16行的程式碼就會發現如下程式碼:
1 protected ConfigurableApplicationContext createApplicationContext() { 2 Class<?> contextClass = this.applicationContextClass; 3 if (contextClass == null) { 4 try { 5 switch(this.webApplicationType) { 6 case SERVLET: 7 contextClass = Class.forName("org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext"); 8 break; 9 case REACTIVE: 10 contextClass = Class.forName("org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext"); 11 break; 12 default: 13 contextClass = Class.forName("org.springframework.context.annotation.AnnotationConfigApplicationContext"); 14 } 15 } catch (ClassNotFoundException var3) { 16 throw new IllegalStateException("Unable create a default ApplicationContext, please specify an ApplicationContextClass", var3); 17 } 18 } 19 20 return (ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass); 21 }
可以看到這個方法可能會建立三種ApplicationContext,而分別對這三種容器的構造方法進行檢視,發現每個構造方法中都初始化了一個AnnotationBeanDefinitionReader,所以元處理器類ConfigurationClassPostProcessor就是這樣載入到容器中的。
同樣通過那篇博文我們知道,是在refresh方法中的第五個方法invokeBeanFactoryPostProcessors(beanFactory)完成了對類ConfigurationClassPostProcessor中postProcessBeanDefinitionRegistry方法的呼叫。我們重點關注對parse.parse()方法的呼叫,如下圖所示:
1 // 初始化解析器 2 ConfigurationClassParser parser = new ConfigurationClassParser( 3 this.metadataReaderFactory, this.problemReporter, this.environment, 4 this.resourceLoader, this.componentScanBeanNameGenerator, registry); 5 6 Set<BeanDefinitionHolder> candidates = new LinkedHashSet<BeanDefinitionHolder>(configCandidates); 7 Set<ConfigurationClass> alreadyParsed = new HashSet<ConfigurationClass>(configCandidates.size()); 8 do { 9 // 解析,此方法是這個後置處理方法的核心 經過了漫長的解析 複雜的一批 10 parser.parse(candidates);
此方法異常複雜,但是這不能阻擋我們前進的腳步,繼續檢視之。發現後面調到了org.springframework.context.annotation.ConfigurationClassParser#doProcessConfigurationClass方法,此方法內容量較大,分別對@PropertySource、@Import、@ImportSource、@Bean進行了處理,我們就以@ImportResource為例追溯,因為@Import相比@ImportResource只是少了一步解析Xml檔案。
定位到處理@ImportResource的地方:
1 // 將解析結果新增到ConfigurationClass的importedResources中 2 if (sourceClass.getMetadata().isAnnotated(ImportResource.class.getName())) { 3 AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); 4 String[] resources = importResource.getAliasedStringArray("locations", ImportResource.class, sourceClass); 5 Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader"); 6 for (String resource : resources) { 7 String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); 8 configClass.addImportedResource(resolvedResource, readerClass); 9 } 10 }
可以知道,此處是將@ImportResource中每一個xml資源配置項提取出來,跟reader一起放入了configClass的一個map中。有放入就有取出,取出的地方在parse方法的下面,如下所示:
1 parser.parse(candidates); 2 parser.validate(); 3 4 Set<ConfigurationClass> configClasses = new LinkedHashSet<ConfigurationClass>(parser.getConfigurationClasses()); 5 configClasses.removeAll(alreadyParsed); 6 7 // Read the model and create bean definitions based on its content 8 if (this.reader == null) { 9 this.reader = new ConfigurationClassBeanDefinitionReader( 10 registry, this.sourceExtractor, this.resourceLoader, this.environment, 11 this.importBeanNameGenerator, parser.getImportRegistry()); 12 } 13 // 將BeanDefinition載入進容器中 14 this.reader.loadBeanDefinitions(configClasses);
第14行程式碼點進去追溯,就會發現下面的方法:
1 private void loadBeanDefinitionsForConfigurationClass(ConfigurationClass configClass, 2 TrackedConditionEvaluator trackedConditionEvaluator) { 3 4 if (trackedConditionEvaluator.shouldSkip(configClass)) { 5 String beanName = configClass.getBeanName(); 6 if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) { 7 this.registry.removeBeanDefinition(beanName); 8 } 9 this.importRegistry.removeImportingClassFor(configClass.getMetadata().getClassName()); 10 return; 11 } 12 13 if (configClass.isImported()) { 14 registerBeanDefinitionForImportedConfigurationClass(configClass); 15 } 16 for (BeanMethod beanMethod : configClass.getBeanMethods()) { 17 loadBeanDefinitionsForBeanMethod(beanMethod); 18 } 19 loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); 20 loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); 21 }
第19行程式碼處就是取出了之前put進去的資料,呼叫XmlBeanDefinitionReader的loadBeanDefinitions方法進行載入處理。而且此處還可以看到對@Import的處理,對ImportBeanDefinitionRegistrars的處理。
到這裡,Spring容器就完成了對@Import、@ImportResource註解的處理,將所有涉及到的類都存入了容器中。其中還有一點需要提一下,就是在對@Import註解處理的時候,使用了遞迴跟迴圈呼叫,因為@Import引入的類上可能還有@Import、@ImportResource等註解,這樣做就能保證不會漏掉。
好了,基本解讀就到這裡,如果其中有不準確之處,還請各位道友指正,我們下期再見?