Spring Boot 原理解析—啓動類包掃描原理

爲了何更好的理解該篇內容,請先閱讀Spring Boot 原理解析—入口SpringApplication

我們知道在使用Spring Boot時,Spring會自動加載Spring Boot中啓動類包下以及其子包下的帶註解的類,本篇不會講述是如何加載註解類的,因爲這是屬於Spring的內容,我們只講述爲什麼會根據啓動類加載子包下的帶註解的類。在講解Spring Boot源碼之前我們先看一下Spring中包的掃描方式一種是@ComponentScan("cn.org.microservice.spring.ioc.annotation")註解,另一種則是以XML的方式配置:

<context:component-scan base-package="cn.org.microservice.spring.ioc.annotation"/>

如果我們不使用XMl而使用@ComponentScan,但是在註解中不配置然任何包,也就是說直接在@Configuration註解的淚傷註解@ComponentScan,代碼如下所示:

package cn.org.microservice.spring.ioc.annotation;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan
public class ConfigrationBean {
}

然後在其子包下創建幾個Bean,如下所示,我們在其子包下創建了三個Bean類,爲了方便我們只創建類,沒有創建接口。

package cn.org.microservice.spring.ioc.annotation.bean.service;
import org.springframework.stereotype.Service;
@Service
public class ServiceBean {
}
package cn.org.microservice.spring.ioc.annotation.bean;
import org.springframework.stereotype.Component;
@Component
public class BeanA {
}
package cn.org.microservice.spring.ioc.annotation.bean1;
import org.springframework.stereotype.Component;
@Component
public class Bean2 {
}

然後我們使用測試類測試,然後輸出容器中註冊的Bean的名稱。

public class AnnotationTest {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(ConfigrationBean.class);
	for(String beanName : applicationContext.getBeanDefinitionNames()) {
	System.out.println(beanName);
	}
    }
}
//一下爲輸出內容
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
configrationBean
beanA
serviceBean
bean2

通過輸出我們看到容器中居然註冊其子包下的註解類,但是實際上上我們並沒有告訴容器去註冊cn.org.microservice.spring.ioc.annotation子包,但是Spring還是掃描了cn.org.microservice.spring.ioc.annotation子包。SpringBoot用的就是這個原理。通過Spring Boot 原理解析—入口SpringApplication一篇我們知道Spring Boot啓動類上包含Configuration註解和@ComponentScan,我們運行main方法的時候只需要將主類註冊到Spring容器中,後續就會掃描主類包下的子包。下面我們就來看看其中的源碼解析,先看Spring Boot註冊主類的邏輯是在run方法中調用的prepareContext方法:

prepareContext(context, environment, listeners, applicationArguments,printedBanner);

然後我們進入prepareContext方法內部:

private void prepareContext(ConfigurableApplicationContext context,ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner){
    ......
    ......
    //前面一系列準備操作省略,可以自己查看源碼
    //獲取資源
    Set<Object> sources = getAllSources();
    Assert.notEmpty(sources, "Sources must not be empty");
    //加載資源 sources.toArray(new Object[0])就是主類的Class實例
    load(context, sources.toArray(new Object[0]));
    listeners.contextLoaded(context);
}
//接着我們再看load方法
protected void load(ApplicationContext context, Object[] sources) {
		...
    //創建BeanDefinitionLoader實例,由BeanDefinitionLoader 加載資源
    BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
    ......
    //BeanDefinitionLoader 實例加載資源
    loader.load();
}

然後我們只需要關注BeanDefinitionLoader的load方法即可:

public int load() {
    int count = 0;
	//這裏我們傳入的sources爲主類的Class實例:Application.class
    for (Object source : this.sources) {
	count += load(source);
    }
    return count;
}
private int load(Object source) {
    Assert.notNull(source, "Source must not be null");
    //我們傳入的是Class類型的實例,因此走該分支
    if (source instanceof Class<?>) {
		return load((Class<?>) source);
    }
    //下面是其他分支,不作介紹
    ......
    throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
private int load(Class<?> source) {
    if (isGroovyPresent()&& GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
        //非Groovy不作介紹
	....
    }
    //我們知道@Configuration的元註解@Component,因此走該分支
    if (isComponent(source)) {
        //使用BeanDefinitionRegistry實例註冊Bean
	this.annotatedReader.register(source);
	return 1;
    }
    return 0;
}

下面我們看將主類註冊到Spring容器中的邏輯:

public void register(Class<?>... annotatedClasses) {
    for (Class<?> annotatedClass : annotatedClasses) {
	//調用registerBean
	registerBean(annotatedClass);
    }
}
public void registerBean(Class<?> annotatedClass) {
    //doRegisterBean纔是註冊Bean的邏輯
    doRegisterBean(annotatedClass, null, null, null);
}
<T> void doRegisterBean(Class<T> annotatedClass, @Nullable Supplier<T> instanceSupplier, @Nullable String name,@Nullable Class<? extends Annotation>[] qualifiers, BeanDefinitionCustomizer... definitionCustomizers) {
    AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(annotatedClass);
    if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
	return;
    }
    //一下爲對BeanDefinition實例的處理
    ......
    //創建BeanDefinitionHolder實例
    BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
    definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
    //將BeanDefinition註冊到容器,調用完該方法 Spring Boot的主類已經被註冊到Spring 容器中了。
    //registerBeanDefinition方法就是將BeanDefinition註冊到Spring 容器中,這裏不在展示
    BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
}

至此,我們已經將主類註冊到Spring 容器中了,接着就是Spring 如何根據註冊到Spring中的主類掃描主類下以及其子包下的註解的類,即如何將其他Bean註冊到Spring容器中。同樣我們在Spring Boot 原理解析—從入口SpringApplication說起一篇中說過Bean的註冊最終是調用最終調用AbstractApplicationContext的refresh()方法。我們就看該方法中的內容,其中調用方法invokeBeanFactoryPostProcessors處理已經註冊到Spring容器中的Bean,也就是我們剛開始時注入到Spring中的主類。

public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
	ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
	try {
	    // Invoke factory processors registered as beans in the context.
	    //調用工廠處理已經註冊到容器中的Bean
	    invokeBeanFactoryPostProcessors(beanFactory);
        }
			......
    }
}

然後我們只需要關注invokeBeanFactoryPostProcessors即可,如下代碼爲其調用流程:

protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) {
    PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());
    ......
}
public static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) {
    ......
    //該方法處理已經註冊到容器中的Bean
    invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry);
    .....
}
private static void invokeBeanFactoryPostProcessors( Collection<? extends BeanFactoryPostProcessor> postProcessors, ConfigurableListableBeanFactory beanFactory) {
    //下面將邏輯交給BeanFactoryPostProcessor實現ConfigurationClassPostProcessor處理
    for (BeanFactoryPostProcessor postProcessor : postProcessors) {
        postProcessor.postProcessBeanFactory(beanFactory);
    }
}
@Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    ......
    //處理配置Bean定義
    processConfigBeanDefinitions(registry);
}
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    //獲取已經註冊到Spring容器中的Bean的名稱
    String[] candidateNames = registry.getBeanDefinitionNames();
    //下面的邏輯是將@Configuration註解的類加入到configCandidates中
    // ConfigurationClassParser解析每一個 @Configuration class
    ConfigurationClassParser parser = new ConfigurationClassParser(
    this.metadataReaderFactory, this.problemReporter, this.environment,this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
    Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
    do {
	//解析@Configuration註解的類
        parser.parse(candidates);
	}
		.....
}

真正解析@Configuration註解是ConfigurationClassParser類的parse方法,下面我們查看parse方法代碼:

public void parse(Set<BeanDefinitionHolder> configCandidates) {
    this.deferredImportSelectors = new LinkedList<>();
    for (BeanDefinitionHolder holder : configCandidates) {
	BeanDefinition bd = holder.getBeanDefinition();
	try {
	    //我們前面注入的主類是屬於AnnotatedBeanDefinition,走該分支,其他的代碼省略
	    if (bd instanceof AnnotatedBeanDefinition) {
                //這裏繼續調用了parse的重載方法
	        parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
	    }
            //下面的其他分支我們不關注
            ......
	}
	......
    }
}
protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
    //調用processConfigurationClass解析配置類
    //將Bean名稱和AnnotationMetadata 註解數據封裝爲ConfigurationClass
    processConfigurationClass(new ConfigurationClass(metadata, beanName));
}
	
protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
    //其他邏輯
    .....
    SourceClass sourceClass = asSourceClass(configClass);
    do {
        //解析@Configuration註解的類,doProcessConfigurationClass才真正解析@Configuration類
	sourceClass = doProcessConfigurationClass(configClass, sourceClass);
        }
    ......
}

上面的方法中doProcessConfigurationClass才解析@Configuration註解的類,我們查看doProcessConfigurationClass方法源碼:

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)throws IOException {
    //是否擁有@ComponentScan註解
    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) {
	    // 配置類上有 @ComponentScan 註解 我們的主類上是有該註解的。
	    Set<BeanDefinitionHolder> scannedBeanDefinitions =this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
				.......
	}
    }
    ......
}

doProcessConfigurationClass源碼中會判斷@Configuration註解的類上是否包含@ComponentScan註解,如果包含則處理該註解,這個配置的就是要掃描的包的名稱:@ComponentScan是由ComponentScanAnnotationParser實例進行解析,我們繼續查看的ComponentScanAnnotationParser的parse方法:

public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, final String declaringClass) {
    ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
    //獲取@ComponentScan配置的包
    Set<String> basePackages = new LinkedHashSet<>();
    String[] basePackagesArray = componentScan.getStringArray("basePackages");
    for (String pkg : basePackagesArray) {
        String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg),
ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    Collections.addAll(basePackages, tokenized);
    }
    for (Class<?> clazz : componentScan.getClassArray("basePackageClasses")) {
        basePackages.add(ClassUtils.getPackageName(clazz));
    }
    //如果@ComponentScan註解配置的包爲空,則使用@Configuration註解的類的包作爲掃描包
    if (basePackages.isEmpty()) {
			basePackages.add(ClassUtils.getPackageName(declaringClass));
    }
    //接下李就是掃描寶,然後將包內的Bean註冊到Spring容器中,下面的邏輯就不看了,
    return scanner.doScan(StringUtils.toStringArray(basePackages));
}

ComponentScanAnnotationParser的parse方法的邏輯主要是獲取@ComponentScan註解中配置的包,然後交給ClassPathBeanDefinitionScanner掃描包,如果@ComponentScan沒有配置包,使用basePackages.add(ClassUtils.getPackageName(declaringClass))將@Configuration註解的類的包設置爲要掃描的包。

總結:

上面走了那麼多的代碼,其實主要做了兩件事,1.將@SpringBootApplication註解的類註冊到Spring容器,2.調用Spring容器的refresh()方法註冊Bean時,將要掃描的包配置爲@SpringBootApplication註解的類的包。其實我們使用new AnnotationConfigApplicationContext(ConfigrationBean.class)也是一樣的道理,我們可以看下該構造方法:

public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) {
    this();
    //第一步:註冊@Configuration註解類到Spring容器
    register(annotatedClasses);
    //調用refresh註冊其他Bean。
    //如果@Configuration註解的類上有@ComponentScan註解
    //解析時根據@ComponentScan掃描包
    //如果@ComponentScan配置了包,則使用@ComponentScan配置的包
    //如果@ComponentScan沒有配置包,則掃描其註解的類的包
    refresh();
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章