spring自动扫描机制

来源:互联网 发布:猪年春晚不提猪 知乎 编辑:程序博客网 时间:2024/05/23 12:02

spring自动扫描机制

分类: spring  |  标签: spring,classloader,resources,string,class,文档  |  作者: liweisnake 相关  |  发布日期 : 2014-12-19  |  热度 : 490°

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

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

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

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

01protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
02    Assert.notEmpty(basePackages, "At least one base package must be specified");
03    Set<BeanDefinitionHolder> beanDefinitions = newLinkedHashSet<BeanDefinitionHolder>();
04    for (String basePackage : basePackages) {
05        Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
06        for (BeanDefinition candidate : candidates) {
07            //……
08        }                      
09    }
10    return beanDefinitions;
11}
        看起来,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有什么好处,实际上,好处正是隐藏了背后的所有文件类型,将所有的资源抽象。

01public Set<BeanDefinition> findCandidateComponents(String basePackage) {
02    Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();
03    try {
04        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
05                resolveBasePackage(basePackage) + "/" this.resourcePattern;
06        Resource[] resources =this.resourcePatternResolver.getResources(packageSearchPath);
07        for (Resource resource : resources) {
08            if (resource.isReadable()) {
09                try {
10                    MetadataReader metadataReader =this.metadataReaderFactory.getMetadataReader(resource);
11                    if (isCandidateComponent(metadataReader)) {
12                        ScannedGenericBeanDefinition sbd = newScannedGenericBeanDefinition(metadataReader);
13                        sbd.setResource(resource);
14                        sbd.setSource(resource);
15                        if (isCandidateComponent(sbd)) {
16                            if (debugEnabled) {
17                                logger.debug("Identified candidate component class: " + resource);
18                            }
19                            candidates.add(sbd);
20                        }
21                        else {
22                            //……
23                        }
24                    }
25                    else {
26                        //……
27                    }
28                }
29                catch (Throwable ex) {
30                    throw new BeanDefinitionStoreException(
31                            "Failed to read candidate component class: " + resource, ex);
32                }
33            }
34            else {
35                //……
36            }
37        }
38    }
39    catch (IOException ex) {
40        throw new BeanDefinitionStoreException("I/O failure during classpath scanning", ex);
41    }
42    return candidates;
43}

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

01public MetadataReader getMetadataReader(Resource resource) throws IOException {
02    if (getCacheLimit() <= 0) {
03        return super.getMetadataReader(resource);
04    }
05    synchronized (this.classReaderCache) {
06        MetadataReader metadataReader = this.classReaderCache.get(resource);
07        if (metadataReader == null) {
08            metadataReader = super.getMetadataReader(resource);
09            this.classReaderCache.put(resource, metadataReader);
10        }
11        return metadataReader;
12    }
13}

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

01SimpleMetadataReader(Resource resource, ClassLoader classLoader) throws IOException {
02    InputStream is = resource.getInputStream();
03    ClassReader classReader = null;
04    try {
05        classReader = new ClassReader(is);
06    finally {
07        is.close();
08    }
09 
10    AnnotationMetadataReadingVisitor visitor = newAnnotationMetadataReadingVisitor(classLoader);
11    classReader.accept(visitor, true);
12     
13    this.annotationMetadata = visitor;
14    // (since AnnotationMetadataReader extends ClassMetadataReadingVisitor)
15    this.classMetadata = visitor;
16    this.resource = resource;
17}

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


参考文档

         Java字节码框架ASM-读写字节码的用法 链接地址

0 0
原创粉丝点击