深入探究 Spring 的扫描原理

在 Spring 框架中,扫描机制是其核心功能之一,它负责在应用程序启动时自动发现和注册相关的组件。本文将深入探讨 Spring 的扫描原理,包括扫描过程以及相关源码的分析。

spring提供了两种扫描方式来进行扫描:1.我们可以通过AnnotationConfigApplicationContext的scan方法中进行设置固定的包名路径;2.我们可以通过@ComponentScan注解来进行配置要进行扫描的包路径。这两种方式底层都是进行执行的是ClassPathBeanDefinitionScanner 类来进行扫描的操作。这两种方式的主要区别在于执行的时机不同,本文介绍**@ComponentScan**注解来进行配置的扫描方式

1.spring的扫描时机

spring 容器初始化的时候,会进行执行invokeBeanFactoryPostProcessors方法,内部会进行调用ConfigurationClassPostProcessor的postProcessBeanDefinitionRegistry方法的后完成了bean的扫描操作。

2.spring 扫描流程

通过我们对代码的分析,我们发现spring进行扫描的入口为org.springframework.context.annotation.ComponentScanAnnotationParser#parse。

java 复制代码
public Set<BeanDefinitionHolder> parse(AnnotationAttributes componentScan, String declaringClass) {
	  //扫描器的初始化操作	
     ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry,
				componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
        //获取名字生成器的策略 (针对的是bean的名字)
		Class<? extends BeanNameGenerator> generatorClass = componentScan.getClass("nameGenerator");
		boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass);
		scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator :
				BeanUtils.instantiateClass(generatorClass));

		ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy");
		if (scopedProxyMode != ScopedProxyMode.DEFAULT) {
			scanner.setScopedProxyMode(scopedProxyMode);
		}
		else {
			Class<? extends ScopeMetadataResolver> resolverClass = componentScan.getClass("scopeResolver");
			scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass));
		}
        //获取扫描的包路径
		scanner.setResourcePattern(componentScan.getString("resourcePattern"));
        
        //可以自己添加一些include的过滤器
        //mybatis 框架对此有一些自己的扩展
		for (AnnotationAttributes filter : componentScan.getAnnotationArray("includeFilters")) {
			for (TypeFilter typeFilter : typeFiltersFor(filter)) {
				scanner.addIncludeFilter(typeFilter);
			}
		}
       // 添加排除扫描的过滤器
		for (AnnotationAttributes filter : componentScan.getAnnotationArray("excludeFilters")) {
			for (TypeFilter typeFilter : typeFiltersFor(filter)) {
				scanner.addExcludeFilter(typeFilter);
			}
		}

		boolean lazyInit = componentScan.getBoolean("lazyInit");
		if (lazyInit) {
			scanner.getBeanDefinitionDefaults().setLazyInit(true);
		}
        // 扫描的包路径的配置
		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));
		}
		if (basePackages.isEmpty()) {
			basePackages.add(ClassUtils.getPackageName(declaringClass));
		}

		scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
			@Override
			protected boolean matchClassName(String className) {
				return declaringClass.equals(className);
			}
		});
        // 开始进行扫描
		return scanner.doScan(StringUtils.toStringArray(basePackages));
	}

2.1 扫描器的初始化操作

通过源码,我们可以看到spring的扫描器初始化的时候默认进行注册了三个include过滤器。

java 复制代码
	public ClassPathBeanDefinitionScanner(BeanDefinitionRegistry registry, boolean useDefaultFilters,
			Environment environment, @Nullable ResourceLoader resourceLoader) {

		Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
		this.registry = registry;
        
        // 针对@CompentScan注解这个值默认为true
		if (useDefaultFilters) {
			registerDefaultFilters();
		}
		setEnvironment(environment);
		setResourceLoader(resourceLoader);
	}

进行构建ClassPathBeanDefinitionScanner类对象的时候会进行注册三个默认的include过滤器。从代码中我们可以看出这三种过滤器都是AnnotationTypeFilter类型。这三种过滤器分别是针对@Component,@ManagedBean,@Named三种注解进行扫描操作。这块有一个小问题为啥@Component注解和@ManagedBean,@Named的写法不一致呢,有大神知道的话,可以告诉我一下

java 复制代码
@SuppressWarnings("unchecked")
	protected void registerDefaultFilters() {
		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.
		}
	}

2.2 根据包名进行解析

解析@CompentScan注解得到包名, 然后根据包名扫描包下所有的文件, 遍历这些文件,通过ASM字节码技术读取这些文件信息,封装成一个metadataReader对象。调用isCandidateComponent方法,进行判断是否排除,是否添加。如果通过include的filter 则进行实例化成ScannedGenericBeanDefinition对象。

java 复制代码
private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
            //拼接扫描包的路径
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + '/' + this.resourcePattern;
           //会将该包路径下的文件解析成一个Resource对象			
Resource[] resources = getResourcePatternResolver().getResources(packageSearchPath);
			boolean traceEnabled = logger.isTraceEnabled();
			boolean debugEnabled = logger.isDebugEnabled();
			for (Resource resource : resources) {
				if (traceEnabled) {
					logger.trace("Scanning " + resource);
				}
				try {
                  //将resource对象 封装成metaDataReader
					MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
                    // includeFilter和excludeFilter的双重过滤
					if (isCandidateComponent(metadataReader)) {
                        //将metadataReader对象封装成一个ScannedGenericBeanDefinition 对象
						ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
						sbd.setSource(resource);
                        // 进行判断ScannedGenericBeanDefinition 对象是否是接口
                        // 是否是抽象类,是否加了LookUp注解等
						if (isCandidateComponent(sbd)) {
							if (debugEnabled) {
								logger.debug("Identified candidate component class: " + resource);
							}
							candidates.add(sbd);
						}
						else {
							if (debugEnabled) {
								logger.debug("Ignored because not a concrete top-level class: " + resource);
							}
						}
					}
					else {
						if (traceEnabled) {
							logger.trace("Ignored because not matching any filter: " + resource);
						}
					}
				}
				catch (FileNotFoundException ex) {
					if (traceEnabled) {
						logger.trace("Ignored non-readable " + resource + ": " + ex.getMessage());
					}
				}
				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);
		}
		return candidates;
	}
复制代码
复制代码
相关推荐
点心快奔跑3 分钟前
超详细Windows系统MySQL 安装教程
数据库·windows·mysql
serve the people5 分钟前
Prompt Composition with LangChain’s PipelinePromptTemplate
java·langchain·prompt
天天摸鱼的java工程师6 分钟前
干掉系统卡顿!Excel异步导出完整实战方案(百万数据也不慌)
java·后端
心随雨下14 分钟前
Java中将System.out内容写入Tomcat日志
java·开发语言·tomcat
Neo Wordsworth1 小时前
phpstudy 无法启动mysql 但命令可以启动mysql
mysql·phpstudy
Cikiss1 小时前
图解 MySQL JOIN
数据库·后端·mysql
-指短琴长-1 小时前
ProtoBuf速成【基于C++讲解】
android·java·c++
Cx330❀1 小时前
《C++ 搜索二叉树》深入理解 C++ 搜索二叉树:特性、实现与应用
java·开发语言·数据结构·c++·算法·面试
员大头硬花生1 小时前
六、InnoDB引擎-架构-结构
数据库·mysql·oracle
爱吃烤鸡翅的酸菜鱼1 小时前
深度解析《AI+Java编程入门》:一本为零基础重构的Java学习路径
java·人工智能·后端·ai