Spring源码之注解扫描

原文:https://blog.csdn.net/lqzkcx3/article/details/78150513

1. 前言
我们都知道在Spring的核心配置文件中, 通过加入以下代码即可实现注解配置Spring Bean.

<context:component-scan base-package="xx.yyy.zzz" />


2. 前置知识
将上面的这段标签并入到Spring解析主流程逻辑的正是对 BeanDefinitionParser 接口的使用。具体的逻辑就不在这里赘述了。

3. ContextNamespaceHandler
通过对查看spring-context-xx.jar中META-INF目录下的 spring.handlers和spring.schemas文件就会发现自定义标签context前缀的解析工作是由ContextNamespaceHandler来负责完成的.

通过观察ContextNamespaceHandler 中的实现逻辑, 我们可以看到下面这样一行代码:

// 也就是说针对component-scan的解析工作就被全权委托给了`ComponentScanBeanDefinitionParser` 类
registerBeanDefinitionParser("component-scan", new ComponentScanBeanDefinitionParser());
4. ComponentScanBeanDefinitionParser
核心逻辑是对所实现接口 BeanDefinitionParser 定义的唯一方法 parse 的实现了.

    public BeanDefinition parse(Element element, ParserContext parserContext) {
        // private static final String BASE_PACKAGE_ATTRIBUTE = "base-package";
        // ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS = ",; \t\n";
        // 所以我们这里在设置 base-package 的值时, 可以通过上面指示的分隔符进行多个package的指定.
        String[] basePackages = StringUtils.tokenizeToStringArray(element.getAttribute(BASE_PACKAGE_ATTRIBUTE), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);

    // ------------- 以下是4.3.12 的与上述功能类似的代码 (2017/12/8)
    /*
    String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE);
    // 注意这里新增的代码使得我们的base-package属性也可以使用Ant-style 模式的匹配符号.
    basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage);
    String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS);
    */

        // Actually scan for bean definitions and register them.
        // 核心扫描逻辑; 本次关注的重点
        ClassPathBeanDefinitionScanner scanner = configureScanner(parserContext, element);
        Set<BeanDefinitionHolder> beanDefinitions = scanner.doScan(basePackages);
        // 注册所扫描到的符合要求的Bean 
        // 2017/10/31 新增; 这个方法里有些细节被忽略了, 默认情况下 annotationConfig 字段为true, 这就会导致默认情况下会向容器中注册针对@Configuration, @Autowired, @Value, @Inject, @Required等处理器.
        // http://m.blog.csdn.net/honghailiang888/article/details/74981445
       registerComponents(parserContext.getReaderContext(), beanDefinitions, element);
        // 满足接口定义的契约要求
        return null;
    }
5. ClassPathBeanDefinitionScanner
上面的四行代码中, 核心的就是第二 , 三行代码了. 其中第二行代码就是按照用户的自定义需求构建出一个ClassPathBeanDefinitionScanner 实例, 所以我们将关注点主要集中在第三行代码, 也就是 scanner.doScan(basePackages); 的实现上.

首先谈谈这个方法doScan 的命名, 在阅读《Spring源码深度解析》一书时, 作者专门谈到了在Spring源码中, 一般真正的实现逻辑是由名为doXX的方法来完成的. 而XX只是负责进行调度处理. 这里同样也不例外.

    protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
        Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<BeanDefinitionHolder>();
        for (String basePackage : basePackages) {
            // 核心逻辑
            Set<BeanDefinition> candidates = findCandidateComponents(basePackage);

            // 对找到的每个BeanDefinition进行属性配置; 具体代码略
            // xxxx

        return beanDefinitions;
    }


7. findCandidateComponents(basePackage) 方法
该类定义在ClassPathBeanDefinitionScanner 类的基类ClassPathScanningCandidateComponentProvider 中, 其实看看这个基类的名字我们就大概可以猜测类似过滤的操作应该就是在其内部完成的.

以下代码进行了一定的裁剪, 以节省篇幅. 

  public Set<BeanDefinition> findCandidateComponents(String basePackage) {
        Set<BeanDefinition> candidates = new LinkedHashSet<BeanDefinition>();

        // ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX = "classpath*:";
        // 通过观察resolveBasePackage()方法的实现, 我们可以在设置basePackage时, 使用形如${}的占位符, Spring会在这里进行替换
        // this.resourcePattern 默认为 "**/*.class"
        String packageSearchPath = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX +
                resolveBasePackage(basePackage) + "/" + this.resourcePattern;
        // 使用上面拼接出的形如 "classpath*:xx/yyy/zzz/**/*.class", 将其检索为Spring内置的Resource对象(这样就统一化了资源的差异)
        Resource[] resources = this.resourcePatternResolver.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 = this.metadataReaderFactory.getMetadataReader(resource);
                    // 这个isCandidateComponent就是核心逻辑了, 
                    // 上面将class文件内容转换为Resource时, 是将所有的文件都读取进来了
                    // 这显然是不满足我们的要求的, 我们就需要进行相应的过滤
                    if (isCandidateComponent(metadataReader)) {
                        ScannedGenericBeanDefinition sbd = new ScannedGenericBeanDefinition(metadataReader);
                        sbd.setResource(resource);
                        sbd.setSource(resource);
                        // 这里只是判断扫描到的这个类是否可以被实例化, 以及是否是a top-level class or a nested class (static inner class) 
                        if (isCandidateComponent(sbd)) {
                            /* 打印日志 */

                            candidates.add(sbd);
                        }
                        else {/* 打印日志 */ }
                    }
                    else {/* 打印日志 */ }
                }
                catch (Throwable ex) {

                }
            }
            else {/* 打印日志 */ }
        }

        return candidates;
    }


8. isCandidateComponent()方法
 

   protected boolean isCandidateComponent(MetadataReader metadataReader) throws IOException {
        // this.excludeFilters除非用户显式配置, 否则默认为空
        for (TypeFilter tf : this.excludeFilters) {
            if (tf.match(metadataReader, this.metadataReaderFactory)) {
                return false;
            }
        }
        // 这里就需要注意一下了
        // 我在前面没有进行讲解的configureScanner方法里有这么一个细节,
        //   <context:component-scan/> 的"use-default-filters"的属性值默认是true . 这一点可以在configureScanner方法中进行验证
        //  于是我们追踪对useDefaultFilters字段的调用来到ClassPathBeanDefinitionScanner的基类ClassPathScanningCandidateComponentProvider中就会发现
        //   useDefaultFilters字段为true时, 会默认注册如下几个AnnotationTypeFilter到includeFilters字段中:
        //      1. new AnnotationTypeFilter(Component.class)
        //      2. new AnnotationTypeFilter(((Class<? extends Annotation>) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)
        //      3. new AnnotationTypeFilter(((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Named", cl)), false)
        //   多说一句:
        //      与Component注解位于同一个package下的Repository, Controller, Service都被@Component注解所修饰
        //   再多说一句(2017/10/31):
        //      @Configuration 也被@Component注解所修饰
        for (TypeFilter tf : this.includeFilters) {
            if (tf.match(metadataReader, this.metadataReaderFactory)) {
                AnnotationMetadata metadata = metadataReader.getAnnotationMetadata();
                if (!metadata.isAnnotated(Profile.class.getName())) {
                    return true;
                }
                AnnotationAttributes profile = MetadataUtils.attributesFor(metadata, Profile.class);
                return this.environment.acceptsProfiles(profile.getStringArray("value"));
            }
        }
        // 注意这里默认返回是false, 也就是说要是通不过includeFilters的条件, 该Bean就不满足要求, 不会进入Spring容器
        return false;
    }



上面的代码里的解释应该足够详细了, 再多说一句就是 我们的注解方式注册Bean到Spring容器是通过扩展方式(excludeFilters和includeFilters)来完成的, 而非写死在主逻辑里面. 这一点非常值得借鉴!

9. 细粒度控制
这里只放本人使用到了的; 更多的参见开涛的博客.

使用 <context:include-filter /> , <context:exclude-filter />

filter机制在Spring3有五种type 


<context:component-scan base-package="com.kq">
    <!-- 排除base-package下的某个package-->
    <context:exclude-filter type="regex" expression="com.kq.common.singleuser.*"/> 
</context:component-scan>

 
 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值