Spring源码研究之注解扫描

虽然在两年前已跟随《Spring源码深度解析》一书看过Spring源码的核心实现, 但就注解这块的解析一直没有时间了解. 导致每次碰到此类问题时心理没有底气. 这种感觉着实让人不爽, 加之距离上次阅读源码已过去比较长时间了, 所以也借机再次领略下Spring里的精妙设计, 体会OOP理念以及设计模式的实际应用。

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 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 findCandidateComponents(String basePackage) {
        Set candidates = new LinkedHashSet();

        // 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方法里有这么一个细节,
        //    的"use-default-filters"的属性值默认是true . 这一点可以在configureScanner方法中进行验证
        //  于是我们追踪对useDefaultFilters字段的调用来到ClassPathBeanDefinitionScanner的基类ClassPathScanningCandidateComponentProvider中就会发现
        //   useDefaultFilters字段为true时, 会默认注册如下几个AnnotationTypeFilter到includeFilters字段中:
        //      1. new AnnotationTypeFilter(Component.class)
        //      2. new AnnotationTypeFilter(((Class) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)
        //      3. new AnnotationTypeFilter(((Class) 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. 细粒度控制

这里只放本人使用到了的; 更多的参见开涛的博客.

使用 ,

filter机制在Spring3有五种type
Spring源码研究之注解扫描_第1张图片

<context:component-scan base-package="com.kq">
    
    <context:exclude-filter type="regex" expression="com\.kq\.common\.singleuser.*"/> 
context:component-scan>

10. 扩展阅读

本文发表后, 被CSDN推荐了如下链接, 遂一并合并进来.

  1. context:component-scan标签的use-default-filters属性的作用以及原理分析

    在Spring+SpringMVC+Mybatis的集成开发中,经常会遇到事务配置不起作用等问题
    
  2. spring启动component-scan类扫描加载过程
  3. 【第十二章】零配置 之 12.3 注解实现Bean定义 ——跟我学spring3 – (2017/10/3 22:58:59 新增) - 开涛大神的, 更偏向应用!值得一读!

11. 总结

  1. Spring源码读起来很流畅, 报纸一样的排版让人阅读起来有种赏心悦目的感觉. 本人曾在三年前有幸拜读了Bob大叔的《Clean Code》, Spring源码的大部分都是满足其中的规则.
  2. 以上一段逻辑追溯下来, 你就会发现Spring源码里深刻贯彻了”多用组合,少用继承”的思想, 其内部的很多功能都是委托了其他单独的组件. 例如ClassPathBeanDefinitionScanner类中的beanNameGenerator字段(负责生成唯一性的Bean Name)和scopeMetadataResolver字段(负责检索)

你可能感兴趣的:(Spring)