主頁 > 後端開發 > 死磕Spring之IoC篇 - BeanDefinition 的決議程序(面向注解)

死磕Spring之IoC篇 - BeanDefinition 的決議程序(面向注解)

2021-02-27 06:17:24 後端開發

該系列文章是本人在學習 Spring 的程序中總結下來的,里面涉及到相關原始碼,可能對讀者不太友好,請結合我的原始碼注釋 Spring 原始碼分析 GitHub 地址 進行閱讀

Spring 版本:5.1.14.RELEASE

開始閱讀這一系列文章之前,建議先查看《深入了解 Spring IoC(面試題)》這一篇文章

該系列其他文章請查看:《死磕 Spring 之 IoC 篇 - 文章導讀》

BeanDefinition 的決議程序(面向注解)

前面的幾篇文章對 Spring 決議 XML 檔案生成 BeanDefinition 并注冊的程序進行了較為詳細的分析,這種定義 Bean 的方式是面向資源(XML)的方式,面向注解定義 Bean 的方式 Spring 的處理程序又是如何進行的?本文將會分析 Spring 是如何將 @Component 注解或其派生注解 標注的 Class 類決議成 BeanDefinition(Bean 的“前身”)并注冊,

在上一篇 《決議自定義標簽(XML 檔案)》文章中提到了處理 <context:component-scan /> 標簽的程序中,底層借助于 ClassPathBeanDefinitionScanner 掃描器,去掃描指定路徑下符合條件的 BeanDefinition 們,這個類就是處理 @Component 注解定義 Bean 的底層實作,關于 @ComponentScan 注解的原理也是基于這個掃描器來實作的,我們先來看看這個掃描器的處理程序,

類圖

ClassPathBeanDefinitionScanner

org.springframework.context.annotation.ClassPathBeanDefinitionScanner,繼承 ClassPathScanningCandidateComponentProvider,classpath 下 BeanDefinition 的掃描器,支持設定過濾器

默認有三個過濾器: @Component 注解的過濾器,Java EE 6 的 javax.annotation.ManagedBean 注解過濾器,JSR-330 的 javax.inject.Named 注解過濾器,這里我們重點關注第一個過濾器

建構式

public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateComponentProvider {

	/** BeanDefinition 注冊中心 DefaultListableBeanFactory */
	private final BeanDefinitionRegistry registry;

	/** BeanDefinition 的默認配置 */
	private BeanDefinitionDefaults beanDefinitionDefaults = new BeanDefinitionDefaults();

	@Nullable
	private String[] autowireCandidatePatterns;

	/** Bean 的名稱生成器 */
	private BeanNameGenerator beanNameGenerator = new AnnotationBeanNameGenerator();

	private ScopeMetadataResolver scopeMetadataResolver = new AnnotationScopeMetadataResolver();

	/** 是否注冊幾個關于注解的 PostProcessor 處理器 */
	private boolean includeAnnotationConfig = true;

	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry) {
		this(registry, true);
	}

	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters) {
		this(registry, useDefaultFilters, getOrCreateEnvironment(registry));
	}

	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment) {
		this(registry, useDefaultFilters, environment,
				(registry instanceof ResourceLoader ? (ResourceLoader) registry : null));
	}

	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment, @Nullable ResourceLoader resourceLoader) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;

		if (useDefaultFilters) {
			// 注冊默認的過濾器,@Component 注解的過濾器(具有層次性)
			registerDefaultFilters();
		}
		setEnvironment(environment);
		// 設定資源加載物件,會嘗試加載出 CandidateComponentsIndex 物件(保存 `META-INF/spring.components` 檔案中的內容,不存在該物件為 `null`)
		setResourceLoader(resourceLoader);
	}
}

屬性不多,建構式都會進入最下面這個構造方法,主要呼叫了兩個方法,如下:

  1. 呼叫父類的 registerDefaultFilters() 方法,注冊幾個默認的過濾器,方法如下:

    protected void registerDefaultFilters() {
        // 添加 @Component 注解的過濾器(具有層次性),@Component 的派生注解都符合條件
        this.includeFilters.add(new AnnotationTypeFilter(Component.class));
        ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader();
        try {
            this.includeFilters.add(new AnnotationTypeFilter(
                    ((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false));
            logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning");
        }
        catch (ClassNotFoundException ex) {
            // JSR-250 1.1 API (as included in Java EE 6) not available - simply skip.
        }
        try {
            this.includeFilters.add(new AnnotationTypeFilter(
                    ((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false));
            logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning");
        }
        catch (ClassNotFoundException ex) {
            // JSR-330 API not available - simply skip.
        }
    }
    

    添加 @Component 注解的過濾器(具有層次性),@Component 的派生注解都符合條件

    也會添加 Java EE 6 的 javax.annotation.ManagedBean 注解過濾器,JSR-330 的 javax.inject.Named 注解過濾器

  2. 呼叫父類的 setResourceLoader(@Nullable ResourceLoader resourceLoader) 方法,設定資源加載物件并嘗試加載出 CandidateComponentsIndex 物件,方法如下:

    @Override
    public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
        this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
        this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
        // 獲取所有 `META-INF/spring.components` 檔案中的內容
        this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(this.resourcePatternResolver.getClassLoader());
    }
    

    這里有個關鍵的步驟,加載出 CandidateComponentsIndex 物件,嘗試去獲取所有 META-INF/spring.components 檔案中的內容,后續進行分析

1. scan 方法

scan(String... basePackages) 方法,掃描出包路徑下符合條件 BeanDefinition 并注冊,方法如下:

public int scan(String... basePackages) {
    // <1> 獲取掃描前的 BeanDefinition 數量
    int beanCountAtScanStart = this.registry.getBeanDefinitionCount();

    // <2> 進行掃描,將過濾出來的所有的 .class 檔案生成對應的 BeanDefinition 并注冊
    doScan(basePackages);

    // Register annotation config processors, if necessary.
    // <3> 如果 `includeAnnotationConfig` 為 `true`(默認),則注冊幾個關于注解的 PostProcessor 處理器(關鍵)
    // 在其他地方也會注冊,內部會進行判斷,已注冊的處理器不會再注冊
    if (this.includeAnnotationConfig) {
        AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
    }

    // <4> 回傳本次掃描注冊的 BeanDefinition 數量
    return (this.registry.getBeanDefinitionCount() - beanCountAtScanStart);
}

程序如下:

  1. 獲取掃描前的 BeanDefinition 數量

  2. 進行掃描,將過濾出來的所有的 .class 檔案生成對應的 BeanDefinition 并注冊,呼叫 doScan(String... basePackages) 方法

  3. 如果 includeAnnotationConfigtrue(默認),則注冊幾個關于注解的 PostProcessor 處理器(關鍵),在其他地方也會注冊,內部會進行判斷,已注冊的處理器不會再注冊,記住這個 AnnotationConfigUtils

  4. 回傳本次掃描注冊的 BeanDefinition 數量

2. doScan 方法

doScan(String... basePackages) 方法,掃描出包路徑下符合條件 BeanDefinition 并注冊,方法如下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
    Assert.notEmpty(basePackages, "At least one base package must be specified");
    // <1> 定義個 Set 集合 `beanDefinitions`,用于保存本次掃描成功注冊的 BeanDefinition 們
    Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
    for (String basePackage : basePackages) { // 遍歷需要掃描的包名
        // <2> 【核心】掃描包路徑,通過 ASM(Java 位元組碼的操作和分析框架)決議出所有符合條件的 BeanDefinition
        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
        // <3> 對第 `2` 步決議出來的 BeanDefinition 依次處理,并注冊
        for (BeanDefinition candidate : candidates) {
            // <3.1> 決議出 @Scope 注解的元資訊并設定
            ScopeMetadata scopeMetadata = https://www.cnblogs.com/lifullmoon/p/this.scopeMetadataResolver.resolveScopeMetadata(candidate);
            candidate.setScope(scopeMetadata.getScopeName());

            // <3.2> 獲取或者生成一個的名稱 `beanName`
            String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);

            // <3.3> 設定相關屬性的默認值
            if (candidate instanceof AbstractBeanDefinition) {
                postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
            }

            // <3.4> 根據這個類的相關注解設定屬性值(存在則會覆寫默認值)
            if (candidate instanceof AnnotatedBeanDefinition) {
                AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
            }

            // <3.5> 檢查 beanName 是否已存在,已存在但是不兼容則會拋出例外
            if (checkCandidate(beanName, candidate)) {
                // <3.6> 將 BeanDefinition 封裝成 BeanDefinitionHolder 物件,這里多了一個 `beanName`
                BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
                // <3.7> 如果代理模式是 `TARGET_CLASS`,則再創建一個 BeanDefinition 代理物件(重新設定了相關屬性),原始 BeanDefinition 已注冊
                definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
                // <3.8> 添加至 `beanDefinitions` 集合
                beanDefinitions.add(definitionHolder);
                // <3.9> 注冊該 BeanDefinition
                registerBeanDefinition(definitionHolder, this.registry);
            }
        }
    }
    // <4> 回傳 `beanDefinitions`(已注冊的 BeanDefinition 集合)
    return beanDefinitions;
}

程序如下:

  1. 定義個 Set 集合 beanDefinitions,用于保存本次掃描成功注冊的 BeanDefinition 們
  2. 【核心】掃描包路徑,通過 ASM(Java 位元組碼的操作和分析框架)決議出所有符合條件的 BeanDefinition,呼叫父類的 findCandidateComponents(String basePackage) 方法
  3. 對第 2 步決議出來的 BeanDefinition 依次處理,并注冊
    1. 決議出 @Scope 注解的元資訊并設定
    2. 獲取或者生成一個的名稱 beanName
    3. 設定相關屬性的默認值
    4. 根據這個類的相關注解設定屬性值(存在則會覆寫默認值)
    5. 檢查 beanName 是否已存在,已存在但是不兼容則會拋出例外
    6. 將 BeanDefinition 封裝成 BeanDefinitionHolder 物件,這里多了一個 beanName
    7. 如果代理模式是 TARGET_CLASS,則再創建一個 BeanDefinition 代理物件(重新設定了相關屬性),原始 BeanDefinition 已注冊
    8. 添加至 beanDefinitions 集合
    9. 注冊該 BeanDefinition
  4. 回傳 beanDefinitions(已注冊的 BeanDefinition 集合)

2 步是這個掃描程序的核心步驟,在父類 ClassPathScanningCandidateComponentProvider 中進行分析,接下來的處理程序不復雜,獲取相關屬性進行配置

7 步創建代理物件,和 AOP 相關,感興趣的可自行查看

ClassPathScanningCandidateComponentProvider

org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider,classpath 下掃描符合條件的 BeanDefinition

建構式

public class ClassPathScanningCandidateComponentProvider implements EnvironmentCapable, ResourceLoaderAware {

	static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
	private String resourcePattern = DEFAULT_RESOURCE_PATTERN;

	/** 包含過濾器 */
	private final List<TypeFilter> includeFilters = new LinkedList<>();
	/** 排除過濾器 */
	private final List<TypeFilter> excludeFilters = new LinkedList<>();

	@Nullable
	private Environment environment;

	/** {@link Condition} 注解計算器 */
	@Nullable
	private ConditionEvaluator conditionEvaluator;

	/** 資源加載器,默認 PathMatchingResourcePatternResolver */
	@Nullable
	private ResourcePatternResolver resourcePatternResolver;

	/** MetadataReader 工廠 */
	@Nullable
	private MetadataReaderFactory metadataReaderFactory;

	/** 所有 `META-INF/spring.components` 檔案的內容 */
	@Nullable
	private CandidateComponentsIndex componentsIndex;

	protected ClassPathScanningCandidateComponentProvider() {
	}

	public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters) {
		this(useDefaultFilters, new StandardEnvironment());
	}

	public ClassPathScanningCandidateComponentProvider(boolean useDefaultFilters, Environment environment) {
		if (useDefaultFilters) {
			registerDefaultFilters();
		}
		setEnvironment(environment);
		setResourceLoader(null);
	}
    
    @Override
	public void setResourceLoader(@Nullable ResourceLoader resourceLoader) {
		this.resourcePatternResolver = ResourcePatternUtils.getResourcePatternResolver(resourceLoader);
		this.metadataReaderFactory = new CachingMetadataReaderFactory(resourceLoader);
		// 獲取所有 `META-INF/spring.components` 檔案中的內容
		this.componentsIndex = CandidateComponentsIndexLoader.loadIndex(this.resourcePatternResolver.getClassLoader());
	}
}

建構式在上一小節的 ClassPathBeanDefinitionScanner 的建構式中都已經講過了

我們來看到 componentsIndex 屬性,呼叫 CandidateComponentsIndexLoader#loadIndex(@Nullable ClassLoader classLoader) 方法生成的

CandidateComponentsIndexLoader

org.springframework.context.index.CandidateComponentsIndexLoader,CandidateComponentsIndexLoader 的加載器,代碼如下:

public final class CandidateComponentsIndexLoader {

	public static final String COMPONENTS_RESOURCE_LOCATION = "META-INF/spring.components";

	public static final String IGNORE_INDEX = "spring.index.ignore";

	private static final boolean shouldIgnoreIndex = SpringProperties.getFlag(IGNORE_INDEX);

	private static final Log logger = LogFactory.getLog(CandidateComponentsIndexLoader.class);

	/** CandidateComponentsIndex 的快取,與 ClassLoader 對應 */
	private static final ConcurrentMap<ClassLoader, CandidateComponentsIndex> cache = new ConcurrentReferenceHashMap<>();

	private CandidateComponentsIndexLoader() {
	}
    
	@Nullable
	public static CandidateComponentsIndex loadIndex(@Nullable ClassLoader classLoader) {
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse = CandidateComponentsIndexLoader.class.getClassLoader();
		}
		// 獲取所有 `META-INF/spring.components` 檔案中的內容
		return cache.computeIfAbsent(classLoaderToUse, CandidateComponentsIndexLoader::doLoadIndex);
	}

	@Nullable
	private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) {
		// 是否忽略 Index 的提升,通過配置 `spring.index.ignore` 變數,默認為 `false`
		if (shouldIgnoreIndex) {
			return null;
		}

		try {
			// 獲取所有的 `META-INF/spring.components` 檔案
			Enumeration<URL> urls = classLoader.getResources(COMPONENTS_RESOURCE_LOCATION);
			if (!urls.hasMoreElements()) {
				return null;
			}
			// 加載所有 `META-INF/spring.components` 檔案的內容
			List<Properties> result = new ArrayList<>();
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
				result.add(properties);
			}
			if (logger.isDebugEnabled()) {
				logger.debug("Loaded " + result.size() + "] index(es)");
			}
			// 總共配置多少個 component 組件
			int totalCount = result.stream().mapToInt(Properties::size).sum();
			// 如果配置了 component 組件,則封裝成 CandidateComponentsIndex 物件并回傳
			return (totalCount > 0 ? new CandidateComponentsIndex(result) : null);
		}
		catch (IOException ex) {
			throw new IllegalStateException("Unable to load indexes from location [" +
					COMPONENTS_RESOURCE_LOCATION + "]", ex);
		}
	}
}

CandidateComponentsIndexLoader 被 final 修飾,也不允許實體化,提供 loadIndex(@Nullable ClassLoader classLoader) 靜態方法,獲取所有 META-INF/spring.components 檔案中的內容,存在檔案并包含內容則創建對應的 CandidateComponentsIndex 物件

整過程序不復雜,如下:

  1. 根據 spring.index.ignore 變數判斷是否需要忽略本次加載程序,默認為 false
  2. 獲取所有的 META-INF/spring.components 檔案
  3. 加載出所有 META-INF/spring.components 檔案的內容,并生成多個 key-value
  4. 內容不為空則創建對應的 CandidateComponentsIndex 物件回傳

例如 META-INF/spring.components 檔案這樣配置:

example.scannable.AutowiredQualifierFooService=example.scannable.FooService
example.scannable.DefaultNamedComponent=org.springframework.stereotype.Component
example.scannable.NamedComponent=org.springframework.stereotype.Component
example.scannable.FooService=example.scannable.FooService
example.scannable.FooServiceImpl=org.springframework.stereotype.Component,example.scannable.FooService
example.scannable.ScopedProxyTestBean=example.scannable.FooService
example.scannable.StubFooDao=org.springframework.stereotype.Component
example.scannable.NamedStubDao=org.springframework.stereotype.Component
example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Component
example.scannable.sub.BarComponent=org.springframework.stereotype.Component

生成的 CandidateComponentsIndex 物件如下所示:

3. findCandidateComponents 方法

findCandidateComponents(String basePackage) 方法,決議出包路徑下所有符合條件的 BeanDefinition,方法如下:

public Set<BeanDefinition> findCandidateComponents(String basePackage) {
    /*
     * 2. 掃描包路徑,通過 ASM(Java 位元組碼的操作和分析框架)決議出符合條件的 AnnotatedGenericBeanDefinition 們,并回傳
     * 說明:
     * 針對 `1` 決議程序中去掃描指定路徑下的 .class 檔案的性能問題,從 Spring 5.0 開始新增了一個 @Indexed 注解(新特性),
     * @Component 注解上面就添加了 @Indexed 注解
     * 
     * 這里不會去掃描指定路徑下的 .class 檔案,而是讀取所有 `META-INF/spring.components` 檔案中符合條件的類名,
     * 直接添加 .class 后綴就是編譯檔案,而不要去掃描
     *
     * 沒在哪看見這樣使用過,可以參考 ClassPathScanningCandidateComponentProviderTest#customAnnotationTypeIncludeFilterWithIndex 測驗方法
     */
    if (this.componentsIndex != null // `componentsIndex` 不為空,存在 `META-INF/spring.components` 檔案并且決議出資料則會創建
            && indexSupportsIncludeFilters()) // `includeFilter` 過濾器的元素(注解或類)必須標注 @Indexed 注解
    {
        return addCandidateComponentsFromIndex(this.componentsIndex, basePackage);
    }
    else {
        /*
         * 1. 掃描包路徑,通過 ASM(Java 位元組碼的操作和分析框架)決議出符合條件的 ScannedGenericBeanDefinition 們,并回傳
         * 首先需要去掃描指定路徑下所有的 .class 檔案,該程序對于性能有不少的損耗
         * 然后通過 ASM 根據 .class 檔案可以獲取到這個類的所有元資訊,也就可以決議出對應的 BeanDefinition 物件
         */
        return scanCandidateComponents(basePackage);
    }
}

這個方法的實作有兩種方式,都是基于 ASM(Java 位元組碼的操作和分析框架)實作的,默認情況下都是第 1 種,分別如下:

  • 1,呼叫 scanCandidateComponents(String basePackage) 方法,默認

    掃描包路徑,通過 ASM(Java 位元組碼的操作和分析框架)決議出符合條件的 ScannedGenericBeanDefinition 們,并回傳,首先需要去掃描指定路徑下所有的 .class 檔案,該程序對于性能有不少的損耗;然后通過 ASM 根據 .class 檔案可以獲取到這個類的所有元資訊,也就可以決議出對應的 BeanDefinition 物件

  • 2componentsIndex 不為空,也就是說是通過 META-INF/spring.components 檔案配置的 Bean,并且定義 Bean 的注解必須標注 @Index 注解,則呼叫 addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) 方法進行決議

    掃描包路徑,通過 ASM(Java 位元組碼的操作和分析框架)決議出符合條件的 AnnotatedGenericBeanDefinition 們,并回傳,針對 1 決議程序中去掃描指定路徑下的 .class 檔案的性能問題,從 Spring 5.0 開始新增了一個 @Index 注解(新特性),@Component 注解上面就添加了 @Index 注解;這里不會去掃描指定路徑下的 .class 檔案,而是讀取所有 META-INF/spring.components 檔案中符合條件的類名,直接添加 .class 后綴就是編譯檔案,而不要去掃描,提高性能,

ASM 是一個 Java 位元組碼操控框架,它能被用來動態生成類或者增強既有類的功能,ASM 可以直接產生二進制 class 檔案,也可以在類被加載入 Java 虛擬機之前動態改變類行為,Java Class 被存盤在嚴格格式定義的 .class 檔案里,這些類檔案擁有足夠的元資料來決議類中的所有元素:類名稱、方法、屬性以及 Java 位元組碼(指令),ASM 從類檔案中讀入資訊后,能夠改變類行為,分析類資訊,甚至能夠根據用戶要求生成新類,

Spring 在很多地方都使用到了 ASM

4. scanCandidateComponents 方法

scanCandidateComponents(String basePackage) 方法,決議出包路徑下所有符合條件的 BeanDefinition,方法如下:

private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
    // <1> 定義 `candidates` 用于保存符合條件的 BeanDefinition
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        // <2> 根據包名生成一個掃描的路徑,例如 `classpath*:包路徑/**/*.class`
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                resolveBasePackage(basePackage) + '/' + this.resourcePattern;
        // <3> 掃描到包路徑下所有的 .class 檔案
        Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
        boolean traceEnabled = logger.isTraceEnabled();
        boolean debugEnabled = logger.isDebugEnabled();
        // <4> 開始對第 `3` 步掃描到的所有 .class 檔案(需可讀)進行處理,符合條件的類名會決議出一個 ScannedGenericBeanDefinition
        for (Resource resource : resources) {
            if (resource.isReadable()) { // 檔案資源可讀
                try {
                    // <4.1> 根據這個類名找到 `.class` 檔案,通過 ASM(Java 位元組碼操作和分析框架)獲取這個類的所有資訊
                    // `metadataReader` 物件中包含 ClassMetadata 類元資訊和 AnnotationMetadata 注解元資訊
                    // 也就是說根據 `.class` 檔案就獲取到了這個類的元資訊,而不是在 JVM 運行時通過 Class 物件進行操作,提高性能
                    MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                    // <4.2> 根據所有的過濾器判斷這個類是否符合條件(例如必須標注 @Component 注解或其派生注解)
                    if (isCandidateComponent(metadataReader)) {
                        // <4.3> 如果符合條件,則創建一個 ScannedGenericBeanDefinition 物件
                        ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                        // 來源和源物件都是這個 .class 檔案資源
                        sbd.setResource(resource);
                        sbd.setSource(resource);
                        /*
                         * <4.4> 再次判斷這個類是否符合條件(不是內部類并且是一個具體類)
                         * 具體類:不是介面也不是抽象類,如果是抽象類則需要帶有 @Lookup 注解
                         */
                        if (isCandidateComponent(sbd)) {
                            // <4.5> 符合條件,則添加至 `candidates` 集合
                            candidates.add(sbd);
                        }
                    }
                } catch (Throwable ex) {
                    throw new BeanDefinitionStoreException(
                            "Failed to read candidate component class: " + resource, ex);
                }
            }
        }
    } catch (IOException ex) {
        throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    // <5> 回傳 `candidates` 集合
    return candidates;
}

程序如下:

  1. 定義 candidates 用于保存符合條件的 BeanDefinition
  2. 根據包名生成一個掃描的路徑,例如 classpath*:包路徑/**/*.class
  3. 掃描到包路徑下所有的 .class 檔案
  4. 開始對第 3 步掃描到的所有 .class 檔案(需可讀)進行處理,符合條件的類名會決議出一個 ScannedGenericBeanDefinition
    1. 根據這個類名找到 .class 檔案,通過 ASM(Java 位元組碼操作和分析框架)獲取這個類的所有資訊,生成 metadataReader 物件,這個物件其中包含 ClassMetadata 類元資訊和 AnnotationMetadata 注解元資訊,也就是說根據 .class 檔案就獲取到了這個類的元資訊,而不是在 JVM 運行時通過 Class 物件進行操作,提高性能
    2. 根據所有的過濾器判斷這個類是否符合條件(例如必須標注 @Component 注解或其派生注解)
    3. 如果符合條件,則創建一個 ScannedGenericBeanDefinition 物件,來源和源物件都是這個 .class 檔案資源
    4. 再次判斷這個類是否符合條件(不是內部類并且是一個具體類),具體類:不是介面也不是抽象類,如果是抽象類則需要帶有 @Lookup 注解
    5. 符合條件,則添加至 candidates 集合
  5. 回傳 candidates 集合

關于 ASM 的實作本文不進行探討,感興趣的可自行研究

4. addCandidateComponentsFromIndex 方法

addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) 方法,根據 META-INF/spring.components 檔案,獲取帶有 @Indexed 注解的類名,然后決議出符合條件的 BeanDefinition,方法如下:

private Set<BeanDefinition> addCandidateComponentsFromIndex(CandidateComponentsIndex index, String basePackage) {
    // <1> 定義 `candidates` 用于保存符合條件的 BeanDefinition
    Set<BeanDefinition> candidates = new LinkedHashSet<>();
    try {
        Set<String> types = new HashSet<>();
        // <2> 根據過濾器從所有 `META-INF/spring.components` 檔案中獲取所有符合條件的**類名稱**
        for (TypeFilter filter : this.includeFilters) {
            // <2.1> 獲取過濾注解(或類)的名稱(例如 `org.springframework.stereotype.Component`)
            String stereotype = extractStereotype(filter);
            if (stereotype == null) {
                throw new IllegalArgumentException("Failed to extract stereotype from " + filter);
            }
            // <2.2> 獲取注解(或類)對應的條目,并過濾出 `basePackage` 包名下的條目(類的名稱)
            types.addAll(index.getCandidateTypes(basePackage, stereotype));
        }
        boolean traceEnabled = logger.isTraceEnabled();
        boolean debugEnabled = logger.isDebugEnabled();
        // <3> 開始對第 `2` 步過濾出來類名進行處理,符合條件的類名會決議出一個 AnnotatedGenericBeanDefinition
        for (String type : types) {
            // <3.1> 根據這個類名找到 `.class` 檔案,通過 ASM(Java 位元組碼操作和分析框架)獲取這個類的所有資訊
            // `metadataReader` 物件中包含 ClassMetadata 類元資訊和 AnnotationMetadata 注解元資訊
            // 也就是說根據 `.class` 檔案就獲取到了這個類的元資訊,而不是在 JVM 運行時通過 Class 物件進行操作,提高性能
            MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(type);
            // <3.2> 根據所有的過濾器判斷這個類是否符合條件(例如必須標注 @Component 注解或其派生注解)
            if (isCandidateComponent(metadataReader)) {
                // <3.3> 如果符合條件,則創建一個 AnnotatedGenericBeanDefinition 物件
                AnnotatedGenericBeanDefinition sbd = new AnnotatedGenericBeanDefinition(
                        metadataReader.getAnnotationMetadata());
                /*
                 * <3.4> 再次判斷這個類是否符合條件(不是內部類并且是一個具體類)
                 * 具體類:不是介面也不是抽象類,如果是抽象類則需要帶有 @Lookup 注解
                 */
                if (isCandidateComponent(sbd)) {
                    // <3.5> 符合條件,則添加至 `candidates` 集合
                    candidates.add(sbd);
                }
            }
        }
    } catch (IOException ex) {
        throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
    }
    // <4> 回傳 `candidates` 集合
    return candidates;
}

程序如下:

  1. 定義 candidates 用于保存符合條件的 BeanDefinition
  2. 根據過濾器從所有 META-INF/spring.components 檔案中獲取所有符合條件的類名稱
    1. 獲取過濾注解(或類)的名稱(例如 org.springframework.stereotype.Component
    2. 獲取注解(或類)對應的條目,并過濾出 basePackage 包名下的條目(類的名稱)
  3. 開始對第 2 步過濾出來類名進行處理,符合條件的類名會決議出一個 AnnotatedGenericBeanDefinition
    1. 根據這個類名找到 .class 檔案,通過 ASM(Java 位元組碼操作和分析框架)獲取這個類的所有資訊,生成 metadataReader 物件,這個物件其中包含 ClassMetadata 類元資訊和 AnnotationMetadata 注解元資訊,也就是說根據 .class 檔案就獲取到了這個類的元資訊,而不是在 JVM 運行時通過 Class 物件進行操作,提高性能
    2. 根據所有的過濾器判斷這個類是否符合條件(例如必須標注 @Component 注解或其派生注解)
    3. 如果符合條件,則創建一個 AnnotatedGenericBeanDefinition 物件
    4. 再次判斷這個類是否符合條件(不是內部類并且是一個具體類),具體類:不是介面也不是抽象類,如果是抽象類則需要帶有 @Lookup 注解
    5. 符合條件,則添加至 candidates 集合
  4. 回傳 candidates 集合

該程序不會去掃描到所有的 .class 檔案,而是從 META-INF/spring.components 檔案中讀取,知道了類名稱也就知道了 .class 檔案的路徑,然后可以通過 ASM 進行操作了,Spring 5.0 開始新增的一個 @Indexed 注解(新特性),目的為了提高性能,

總結

本文面向注解(@Component 注解或其派生注解)定義的 Bean,Spring 是如何將他們決議成 BeanDefinition(Bean 的“前身”)并注冊的,大致程序如下:

  1. ClassPathBeanDefinitionScanner 會去掃描到包路徑下所有的 .class 檔案
  2. 通過 ASM(Java 位元組碼操作和分析框架)獲取 .class 對應類的所有元資訊
  3. 根據元資訊判斷是否符合條件(帶有 @Component 注解或其派生注解),符合條件則根據這個類的元資訊生成一個 BeanDefinition 進行注冊

關于上面的第 1 步性能損耗不少,Spring 5.0 開始新增的一個 @Indexed 注解(新特性),@Indexed 派生注解(例如 @Component)或 @Indexed 注解的類可以定義在 META-INF/spring.components 檔案中,Spring 會直接從檔案中讀取,找到符合條件的類名稱,也就找到了 .class 檔案,這樣一來對于上面第 1 步來說在性能上得到了提升,目前還沒見到這種方式,畢竟 還要再檔案中定義類名,感覺太復雜了,啟動程序慢就慢點??

到這里,對于通過 面向資源(XML、Properties)面向注解 兩種定義 Bean 的方式,Spring 將定義的資訊轉換成 BeanDefinition(Bean 的“前身”)的程序差不多都分析了,我們接下來研究一下 Bean 的生命周期,BeanDefinition 是如何變成 Bean 的,

本文你是否還有疑惑,@Bean 注解定義的 Bean 怎么沒有決議成 BeanDefinition 呢?別急,在后續的文章會進行分析

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/264060.html

標籤:Java

上一篇:關于application.yml配置讀取

下一篇:Spring Boot移除內嵌Tomcat,使用非web方式啟動

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more