深入探究 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;
	}
复制代码
复制代码
相关推荐
陈大爷(有低保)11 分钟前
UDP Socket聊天室(Java)
java·网络协议·udp
阿华的代码王国22 分钟前
MySQL ------- 索引(B树B+树)
数据库·mysql
kinlon.liu25 分钟前
零信任安全架构--持续验证
java·安全·安全架构·mfa·持续验证
哈喽,树先生31 分钟前
1.Seata 1.5.2 seata-server搭建
spring·springcloud
王哲晓1 小时前
Linux通过yum安装Docker
java·linux·docker
java6666688881 小时前
如何在Java中实现高效的对象映射:Dozer与MapStruct的比较与优化
java·开发语言
Violet永存1 小时前
源码分析:LinkedList
java·开发语言
执键行天涯1 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
liupenglove1 小时前
golang操作mysql利器-gorm
mysql·golang
Jarlen1 小时前
将本地离线Jar包上传到Maven远程私库上,供项目编译使用
java·maven·jar