Spring扫描类过程解析和案例

1. 前言

Spring 启动最重要的第一步,就是扫描需要由Spring管理的类信息,例如@Component、@Controller、@Service等。

通过这篇文章,你将会了解类的扫描实现,并且,我们也会写一个Demo来模拟扫描

2. 源码分析

2.1 主要入口

扫描类的主入口是 ConfigurationClassPostProcessor=>postProcessBeanDefinitionRegistry,调用堆栈如下(了解就行):

1. main 方法
2. AnnotationConfigApplicationContext 构造函数
3. AbstractApplicationContext.refresh
4. AbstractApplicationContext.invokeBeanFactoryPostProcessors
5. PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors
6. PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors

第6步 invokeBeanDefinitionRegistryPostProcessors 方法如下,内部调用 BeanDefinitionRegistryPostProcessor 类型的方法,而 ConfigurationClassPostProcessor 就是Spring框架自带的实现类。

	private static void invokeBeanDefinitionRegistryPostProcessors(
			Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

		for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessBeanDefinitionRegistry(registry);
		}
	}

在这里插入图片描述

2.2 scanCandidateComponents

核心方法: ClassPathScanningCandidateComponentProvider.scanCandidateComponents()。

说明:

  • 扫描类并返回Resource[]数组
  • 遍历Resource[]数组并转换为 ScannedGenericBeanDefinition 数组
	private Set<BeanDefinition> scanCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<>();
		try {
			//获取扫描的包路径,我这里的测试用例。packageSearchPath = classpath*:com/train/**/*.class
			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);
				}
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							//将Resource对象转换为BeanDefinition,记录了SpringBean的描述信息
							ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
							sbd.setResource(resource);
							sbd.setSource(resource);
							//下面基本都是日志了,无关紧要
							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 (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to read candidate component class: " + resource, ex);
					}
				}
				else {
					if (traceEnabled) {
						logger.trace("Ignored because not readable: " + resource);
					}
				}
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}

上面有个变量 packageSearchPath ,值是 “classpath*:com/train/**/*.class”。

可以确定,扫描类的原理就是扫描编译后的 class 文件

2.3 doRetrieveMatchingFiles

PathMatchingResourcePatternResolver => doRetrieveMatchingFiles

根据测试用例,这里的方法参数如下:

  • fullPattern:F:/Source/spring-framework/learning/out/production/classes/com/train/**/*.class
  • dir:F:\Source\spring-framework\learning\out\production\classes\com\train
	protected void doRetrieveMatchingFiles(String fullPattern, File dir, Set<File> result) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Searching directory [" + dir.getAbsolutePath() +
					"] for files matching pattern [" + fullPattern + "]");
		}
		//递归算法,层层往下遍历,读取class文件并返回 Set<File> 集合
		for (File content : listDirectory(dir)) {
			String currPath = StringUtils.replace(content.getAbsolutePath(), File.separator, "/");
			if (content.isDirectory() && getPathMatcher().matchStart(fullPattern, currPath + "/")) {
				if (!content.canRead()) {
					if (logger.isDebugEnabled()) {
						logger.debug("Skipping subdirectory [" + dir.getAbsolutePath() +
								"] because the application is not allowed to read the directory");
					}
				}
				else {
					doRetrieveMatchingFiles(fullPattern, content, result);
				}
			}
			if (getPathMatcher().match(fullPattern, currPath)) {
				result.add(content);
			}
		}
	}

2.4 问题总结

问题一: class 文件的加载顺序是怎样的?

查看 2.3 注释描述的 listDirectory 方法:

	protected File[] listDirectory(File dir) {
		File[] files = dir.listFiles();
		if (files == null) {
			if (logger.isInfoEnabled()) {
				logger.info("Could not retrieve contents of directory [" + dir.getAbsolutePath() + "]");
			}
			return new File[0];
		}
		Arrays.sort(files, Comparator.comparing(File::getName));
		return files;
	}

重点看倒数第二行,Arrays.sort(files, Comparator.comparing(File::getName));

结论:每个package目录分别按文件名排序加载

问题二: 扫描的结果应该是返回Resource数组,这里是File集合是如何转换成Resource数组的?

调试进入PathMatchingResourcePatternResolver => doFindMatchingFileSystemResources 源码

	protected Set<Resource> doFindMatchingFileSystemResources(File rootDir, String subPattern) throws IOException {
		if (logger.isTraceEnabled()) {
			logger.trace("Looking for matching resources in directory tree [" + rootDir.getPath() + "]");
		}
		//扫描获取File集合
		Set<File> matchingFiles = retrieveMatchingFiles(rootDir, subPattern);
		//声明Resource集合
		Set<Resource> result = new LinkedHashSet<>(matchingFiles.size());
		for (File file : matchingFiles) {
			result.add(new FileSystemResource(file));
		}
		return result;
	}

重点查看上面的 for 循环,结论:

FileSystemResource 构造函数可以接收 File 类型对象,其内部依赖于 File

FileSystemResource 与 Resource 关系如下

在这里插入图片描述

问题三: Resource 又是如何转换为 ScannedGenericBeanDefinition?

回到2.2调试的源码,关键是这面这句代码:

MetadataReader metadataReader = getMetadataReaderFactory().getMetadataReader(resource);

MetadataReader 是 spring 封装的元数据读取器,通过 MetadataReaderFactory.getMetadataReader(resource) 即可将Resource对象转换为元数据读取器。最后,我们就能直接取到需要的元数据了。

metadataReader 字段信息如下,其中很重要的一个就是className。只要有了类名,就能反射得到Java 的 Class,因此也就能进一步得到更多的信息。
在这里插入图片描述

3. 扫描Demo

假设,现在我们需要在Spring项目扫描指定package包含的所有类,该如何操作呢?

直接模拟spring源码即可:

		PathMatchingResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
		CachingMetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory();
	
		try {
			//扫描得到Resource数组
			Resource[] resources = resourceResolver.getResources("classpath*:com/train/inherit/**/*.class");
			//遍历Resources,逐个获取信息
			for (Resource resource : resources) {
				MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);
				String className = metadataReader.getAnnotationMetadata().getClassName();
				Class<?> superClazz = Class.forName(className);
				//其他操作......
			}
		} catch (IOException | ClassNotFoundException e) {
			e.printStackTrace();
		}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

°Fuhb

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值