spring自动扫描机制

         记得之前的项目中leader曾经要求我自己写一个类似spring的自动代码扫描器以便扫描我们代码中一些自定义的method,annotation或者注释方式,并且我们要对扫描结果做自己的处理。          

         这就促使我去看spring中是如何自动扫描的,按照我的认识,应该不外乎2种方法。一是装载配置的根路径及其子文件夹下所有的类文件,并且根据每个被装载的文件的反射属性得到我们想要的annotation或者method等metadata,这个方法按理说比较完美,也比较简单,因为反射能够保证得到的metadata的正确性,但是如果对每个class都装载并反射,应该会非常的慢速,并且没有得到lazyload的好处;二是扫描所有根路径及其子文件下的所有源文件,直接分析源文件的文本属性,同样能得到这些metadata,但是这个方式缺点较多,首先要保证各种写法下都能够正确的解析metadata,如果严格起来就类似词法分析了,另外并非所有源文件都会被放到生产环境中(所以这个方法几乎是不可能的),但是这种方式相对于前一种应该会较快些。

         为了能够对这个扫描机制有更深的认识,我还是决定一探spring的究竟。首先我找到ComponentScanAnnotationParser,这个类是负责处理ComponentScan的,中间最重要的方法就是parse, parse是一个很长的方法,请容我将中间不重要的代码省略,这时你发现,parse实际上调用了scanner的doScan方法。

	public Set<BeanDefinitionHolder> parse(Map<String, Object> componentScanAttributes) {
		ClassPathBeanDefinitionScanner scanner =
			new ClassPathBeanDefinitionScanner(registry, (Boolean)componentScanAttributes.get("useDefaultFilters"));
		//……
		return scanner.doScan(basePackages.toArray(new String[]{}));
	}
好吧,我们顺着挖进去,scanner的doScan方法又做了什么呢,对于我们配置的所有包的位置,他都会调用findCandidateComponents去找到候选的component(之所以是候选,因为找到的component未必生效)

	protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
		Assert.notEmpty(basePackages, "At least one base package must be specified");
		Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
		for (String basePackage : basePackages) {
			Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
			for (BeanDefinition candidate : candidates) {
				//……
			}						
		}
		return beanDefinitions;
	}
        看起来,findCandidateComponenets这个方法就已经得到了反射的annotation了,我们再挖进去,findCandidateComponenets首先调用this.resourcePatternResolver.getResources()得到一堆resource,然后对每个resource调用this.metadataReaderFactory.getMetadataReader(resource)得到MetadataReader,最后判断该metadataReader是否是candidate,如果是,加入集合,所有resource都判断完以后,返回集合。其实这里的MetadataReader就可以得到我们class的元信息,因此,势必要再挖一下MetadataReader。

          但是在那之前,我们可以先看看spring是如何配置和定义Resource的,resourcePatternResolver事实上是PathMatchingResourcePatternResolver的实例。通过getResources及其子方法(有兴趣可以自己去挖代码),我们可以看出来,他会处理带通配符和不带通配符的配置,其中带通配符的配置又可以分为3种,分别是jar文件,vfs协议的文件以及普通文件,其中jar文件会装载所有的子文件。这里不免疑问,得到一堆Resource有什么好处,实际上,好处正是隐藏了背后的所有文件类型,将所有的资源抽象。

	public Set<BeanDefinition> findCandidateComponents(String basePackage) {
		Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
		try {
			String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
					resolveBasePackage(basePackage) + "/" + this.resourcePattern;
			Resource[] resources = this.resourcePatternResolver.getResources(packageSearchPath);
			for (Resource resource : resources) {
				if (resource.isReadable()) {
					try {
						MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(resource);
						if (isCandidateComponent(metadataReader)) {
							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 {
								//……
							}
						}
						else {
							//……
						}
					}
					catch (Throwable ex) {
						throw new BeanDefinitionStoreException(
								"Failed to read candidate component class: " + resource, ex);
					}
				}
				else {
					//……
				}
			}
		}
		catch (IOException ex) {
			throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
		}
		return candidates;
	}

          接下来,从metadataReaderFactory得到MetadataReader,事实上这里的metadataReaderFactory是一个CachingMetadataReaderFactory,它用cache做了唯一性处理,如下。

	public MetadataReader getMetadataReader(Resource resource) throws IOException {
		if (getCacheLimit() <= 0) {
			return super.getMetadataReader(resource);
		}
		synchronized (this.classReaderCache) {
			MetadataReader metadataReader = this.classReaderCache.get(resource);
			if (metadataReader == null) {
				metadataReader = super.getMetadataReader(resource);
				this.classReaderCache.put(resource, metadataReader);
			}
			return metadataReader;
		}
	}

         构造SimpleMetadataReader的过程中,会同时构造AnnotationMetadataReadingVisitor(继承自ClassVisitor),并且调用classReader.accept(),这是什么呢?原来这里的classReader跟classVisitor是asm提供的字节码访问类,从字面上就能看出来,其使用了visitor模式,事实上就是classReader.accept的时候会遍历整个类的字节码,遍历的时候如果碰到method,就会调用ClassVisitor中定义的visitMethod,碰到field,就会调用visitField(同样是回调)。asm的介绍见参考文档。

	SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
		InputStream is = resource.getInputStream();
		ClassReader classReader = null;
		try {
			classReader = new ClassReader(is);
		} finally {
			is.close();
		}

		AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(classLoader);
		classReader.accept(visitor, true);
		
		this.annotationMetadata = visitor;
		// (since AnnotationMetadataReader extends ClassMetadataReadingVisitor)
		this.classMetadata = visitor;
		this.resource = resource;
	}

         总之,真象大白了,spring做代码扫描的确是通过编译好的字节码去做的,与之前设想的类似,但是是通过asm对字节码的处理,个人认为应该比反射要快,这样就避免了前述的缺点。来到这里,要写一个代码扫描工具就并不是很困难了。


参考文档

         Java字节码框架ASM-读写字节码的用法 http://simpleframework.net/blog/v/45594.html

         Spring的bean创建顺序 https://blog.csdn.net/sky_ground/article/details/63688397

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值