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();
}