记得之前的项目中leader曾经要求我自己写一个类似spring的自动代码扫描器以便扫描我们代码中一些自定义的method,annotation或者注释方式,并且我们要对扫描结果做自己的处理。
这就促使我去看spring中是如何自动扫描的,按照我的认识,应该不外乎2种方法。一是装载配置的根路径及其子文件夹下所有的类文件,并且根据每个被装载的文件的反射属性得到我们想要的annotation或者method等metadata,这个方法按理说比较完美,也比较简单,因为反射能够保证得到的metadata的正确性,但是如果对每个class都装载并反射,应该会非常的慢速,并且没有得到lazyload的好处;二是扫描所有根路径及其子文件下的所有源文件,直接分析源文件的文本属性,同样能得到这些metadata,但是这个方式缺点较多,首先要保证各种写法下都能够正确的解析metadata,如果严格起来就类似词法分析了,另外并非所有源文件都会被放到生产环境中(所以这个方法几乎是不可能的),但是这种方式相对于前一种应该会较快些。
为了能够对这个扫描机制有更深的认识,我还是决定一探spring的究竟。首先我找到ComponentScanAnnotationParser,这个类是负责处理ComponentScan的,中间最重要的方法就是parse, parse是一个很长的方法,请容我将中间不重要的代码省略,这时你发现,parse实际上调用了scanner的doScan方法。
public Set parse(Map componentScanAttributes) {
ClassPathBeanDefinitionScanner scanner =
new ClassPathBeanDefinitionScanner(registry, (Boolean)componentScanAttributes.get("useDefaultFilters"));
//……
return scanner.doScan(basePackages.toArray(new String[]{}));
}
好吧,我们顺着挖进去,scanner的doScan方法又做了什么呢,对于我们配置的所有包的位置,他都会调用findCandidateComponents去找到候选的component(之所以是候选,因为找到的component未必生效)
protected Set doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set beanDefinitions = new LinkedHashSet();
for (String basePackage : basePackages) {
Set 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 findCandidateComponents(String basePackage) {
Set candidates = new LinkedHashSet();
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