乐趣区

配置类需要标注Configuration却不知原因那这次就不能给你涨薪喽

专一 Java 畛域分享、成长,回绝浅尝辄止。关注公众号【BAT 的乌托邦】开启专栏式学习,回绝浅尝辄止。本文 https://www.yourbatman.cn 已收录,外面一并有 Spring 技术栈、MyBatis、中间件等小而美的专栏供以学习哦。

前言

各位小伙伴大家好,我是 A 哥。这是继上篇文章:真懂 Spring 的 @Configuration 配置类?你可能自我感觉太良好 的原理 / 源码解释篇。依照本公众号的定位,原理个别跑不了,尽管很干燥,但还得做,毕竟做难事必有所得,真的把握了才有底气谈涨薪嘛。

Tips:鉴于常常有些同学无奈辨别某个性能 / 某项能力属于 Spring Framework 的还是 Spring Boot,你能够参考文章里的【版本约定】目录,那里会阐明本文的版本依赖,也就是性能所属喽。比方本文内容它就属于Spring Framework,和Spring Boot 木有关系。


版本约定

本文内容若没做非凡阐明,均基于以下版本:

  • JDK:1.8
  • Spring Framework:5.2.2.RELEASE

注释

Spring 的 IoC 就像个“大熔炉”,什么都当作 Bean 放在外面。然而,尽管它们都放在了一起,然而理论在性能上是有区别的,比方咱们相熟的 BeanPostProcessor 就属于后置处理器性能的 Bean,还有本文要探讨的 @Configuration 配置 Bean 也属于一种非凡的组件。

判断一个 Bean 是否是 Bean 的后置处理器 很不便,只需看它是否实现了 BeanPostProcessor 接口即可;那么如何去确定一个 Bean 是否是 @Configuration 配置 Bean 呢?若是,如何辨别是 Full 模式还是 Lite 模式呢?这便就是本文将要探讨的内容。


如何判断一个组件是否是 @Configuration 配置?

首先须要明确:@Configuration配置前提必须是 IoC 治理的一个组件(也就是常说的 Bean)。Spring 应用 BeanDefinitionRegistry 注册核心治理着所有的 Bean 定义信息,那么对于这些 Bean 信息哪些属于 @Configuration 配置呢,这是须要甄选进去的。

判断一个 Bean 是否是 @Configuration 配置类这个逻辑对立交由 ConfigurationClassUtils 这个工具类去实现。


ConfigurationClassUtils 工具类

见名之意,它是和配置无关的一个工具类,提供几个动态工具办法供以应用。它是 Spring 3.1 新增,对于它的作用,官网给的解释是:用于标识 @Configuration 类的实用程序 (Utilities)。它次要提供了一个办法:checkConfigurationClassCandidate() 用于查看给定的 Bean 定义是否是配置类的候选对象(或者在配置 / 组件类中申明的嵌套组件类),并做相应的标记


checkConfigurationClassCandidate()

它是一个 public static 工具办法,用于判断某个 Bean 定义是否是 @Configuration 配置。

ConfigurationClassUtils:public static boolean checkConfigurationClassCandidate(BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
        ...
        // 依据 Bean 定义信息,拿到器对应的注解元数据
        AnnotationMetadata metadata = xxx;
        ...
        
        // 依据注解元数据判断该 Bean 定义是否是配置类。若是:那是 Full 模式还是 Lite 模式
        Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
        if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        } else if (config != null || isConfigurationCandidate(metadata)) {beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        } else {return false;}
        
        ...
        
        // 到这。它必定是一个残缺配置(Full or Lite)这里进一步把 @Order 排序值放上去
        Integer order = getOrder(metadata);
        if (order != null) {beanDef.setAttribute(ORDER_ATTRIBUTE, order);
        }

        return true;
    }

步骤总结:

  1. 依据 Bean 定义信息解析成为一个注解元数据对象AnnotationMetadata metadata

    1. 可能是个AnnotatedBeanDefinition,也可能是个StandardAnnotationMetadata
  2. 依据注解元数据 metadata 判断是否是个 @Configuration 配置类,有如下三种可能 case:

    1. 标注有 @Configuration 注解 并且 该注解的 proxyBeanMethods = false,那么 mark 一下它是Full 模式 的配置。否则进入下一步判断
    2. 标注有 @Configuration 注解 或者 合乎 Lite 模式的条件(上文有说一共有 5 种可能是 Lite 模式,源码处在 isConfigurationCandidate(metadata) 这个办法里表述),那么 mark 一下它是 Lite 模式 的配置。否则进入下一步判断
    3. 不是配置类,并且返回后果return false
  3. 能进行到这一步,阐明该 Bean 必定是个配置类了(Full 模式或者 Lite 模式),那就取出其 @Order 值(若有的话),而后 mark 进 Bean 定义外面去

这个 mark 动作很有意义:前面判断一个配置类是 Full 模式还是 Lite 模式,甚至判断它是否是个配置类均可通过 beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE) 这样实现判断


办法应用处

通晓了 checkConfigurationClassCandidate() 可能判断一个 Bean(定义)是否是一个配置类,那么它在什么时候会被应用呢?通过查找能够发现它被如下两处应用到:

  • 应用处:ConfigurationClassPostProcessor.processConfigBeanDefinitions()解决配置 Bean 定义阶段。
ConfigurationClassPostProcessor:public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
        
        // 拿出以后所有的 Bean 定义信息,一个个的查看是否是配置类    
        String[] candidateNames = registry.getBeanDefinitionNames();
        for (String beanName : candidateNames) {BeanDefinition beanDef = registry.getBeanDefinition(beanName);
            if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {logger.debug("Bean definition has already been processed as a configuration class:" + beanDef);
            }
            // 如果该 Bean 定义不是配置类,那就持续判断一次它是否是配置类,若是就退出后果汇合里
            else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
            }
        }
        ...
    }

ConfigurationClassPostProcessor是个 BeanDefinitionRegistryPostProcessor,会在BeanFactory 筹备好后 执行生命周期办法。因而自然而然的,checkConfigurationClassCandidate()会在此阶段调用,用于辨别出哪些是配置 Bean。

值得注意的是 ConfigurationClassPostProcessor 的执行期间是十分晚期的(BeanFactory筹备好后就执行嘛),这个时候容器内的 Bean 定义 很少 。这个时候只有 主配置类 才被注册了进来,那些想通过 @ComponentScan 扫进来的配置类都还没到“工夫”,这个工夫节点很重要,请留神辨别。为了不便你了解,我别离把 Spring 和 Spring Boot 在此阶段的 Bean 定义信息截图展现如下:


以上是 Spring 环境,对应代码为:

new AnnotationConfigApplicationContext(AppConfig.class);


以上是 Spring Boot 环境,对应代码为:

@SpringBootApplication
public class Boot2Demo1Application {public static void main(String[] args) {SpringApplication.run(Boot2Demo1Application.class, args);
    }
}

相比之下,Spring Boot 里多了 internalCachingMetadataReaderFactory 这个 Bean 定义。起因是 SB 定义了一个 CachingMetadataReaderFactoryPostProcessor 把它放进去的,因为此 Processor 也是个 BeanDefinitionRegistryPostProcessor 并且 order 值为 Ordered.HIGHEST_PRECEDENCE,所以它会优先于ConfigurationClassPostProcessor 执行把它注册进去~

  • 应用处:ConfigurationClassParser.doProcessConfigurationClass() 解析 @Configuration 配置类阶段。所处的大阶段同上应用处,仍旧是 ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry() 阶段
ConfigurationClassParser:@Nullable
    protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass) throws IOException {
        ... // 先解析 nested 外部类(外部类会存在 @Bean 办法嘛~)... // 解析 @PropertySource 资源,退出到 environment 环境
        ... // 解析 @ComponentScan 注解,把组件扫描进来
        scannedBeanDefinitions = ComponentScanAnnotationParser.parse(componentScan, ...);
            // 把扫描到的 Bean 定义信息仍旧须要一个个的判断,是否是配置类    
            // 若是配置类,就持续当作一个 @Configuration 配置类来解析 parse() 递归嘛
            for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
                ...
                if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {parse(bdCand.getBeanClassName(), holder.getBeanName());
                }
            }
        ... // 解析 @Import 注解
        ... // 解析 @ImportResource 注解
        ... // 解析以后配置里配置的 @Bean 办法
        ... // 解析接口默认办法(因为配置类可能实现接口,而后接口默认办法可能标注有 @Bean)... // 解决父类(递归,直到父类为 java. 打头的为止)}

这个办法是 Spring 对配置类解析的 最外围步骤 ,通过它顺带也可能解答你的纳闷了吧:为何你仅需在类上标注一个@Configuration 注解即可让它成为一个配置类?因为被 Scan 扫描进去了嘛~

通过以上 两个应用处 的剖析和比照,对于 @Configuration 配置类的了解,你至多应该把握了如下讯息:

  1. @Configuration配置类必定是个组件,存在于 IoC 容器里
  2. @Configuration配置类是 有主次之分 的,主配置类是驱动整个程序的入口,能够是一个,也能够是多个(若存在多个,反对应用 @Order 排序)
  3. 咱们平时个别只书写 次配置类 (而且个别写多个),它 个别 是借助主配置类的 @ComponentScan 能力实现加载进而解析的(当然也可能是@Import、又或是被其它次配置类驱动的)
  4. 配置类能够存在嵌套(如外部类),继承,实现接口等个性

聊完了最为重要的 checkConfigurationClassCandidate() 办法,当然还有必要看看 ConfigurationClassUtils 的另一个工具办法isConfigurationCandidate()


isConfigurationCandidate()

它是一个 public static 工具办法,通过给定的注解元数据信息来判断它是否是一个Configuration

ConfigurationClassUtils:static {candidateIndicators.add(Component.class.getName());
        candidateIndicators.add(ComponentScan.class.getName());
        candidateIndicators.add(Import.class.getName());
        candidateIndicators.add(ImportResource.class.getName());
    }

    public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
        // 不思考接口 or 注解 阐明:注解的话也是一种“非凡”的接口哦
        if (metadata.isInterface()) {return false;}
        // 只有该类上标注有以上 4 个注解任意一个,都算配置类
        for (String indicator : candidateIndicators) {if (metadata.isAnnotated(indicator)) {return true;}
        }
        // 若一个注解都没标注,那就看有木有 @Bean 办法 若有那也算配置类
        return metadata.hasAnnotatedMethods(Bean.class.getName());
    }

步骤总结:

  1. 若是接口类型(含注解类型),间接不予考虑,返回 false。否则持续判断
  2. 若此类上标注有 @Component、@ComponentScan、@Import、@ImportResource 任意一个注解,就判断胜利返回 true。否则持续判断
  3. 到此步,就阐明 此类上没有标注任何注解。若存在 @Bean 办法,返回 true,否则返回 false。

须要特地特地特地留神的是:此办法它的并不思考 @Configuration 注解,是“轻量级”判断,这是它和 checkConfigurationClassCandidate() 办法的最次要区别。当然,后者依赖于前者,依赖它来依据注解元数据判断是否是 Lite 模式的配置。


Spring 5.2.0 版本变动阐明

因为本文的解说和代码均是基于 Spring 5.2.2.RELEASE 的,而并不是所有小伙伴都会用到这么新的版本。对于此局部的实现,以 Spring 5.2.0 版本为分界线实现上有些许差别,所以在此处做出阐明。


proxyBeanMethods 属性的作用

proxyBeanMethods属性是 Spring 5.2.0 版本为 @Configuration 注解新减少的一个属性:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {@AliasFor(annotation = Component.class)
    String value() default "";
    // @since 5.2
    boolean proxyBeanMethods() default true;}

它的作用是:是否容许代理 @Bean 办法。说白了:决定此配置应用 Full 模式还是 Lite 模式。为了放弃向下兼容,proxyBeanMethods的默认值是 true,应用 Full 模式配置。

Spring 5.2 提出了这个属性项,是冀望你在曾经理解了它的作用之后,显示的把它置为 false 的,因为在云原生将要到来的明天,启动速度方面 Spring 始终在做着致力,也心愿你能配合嘛。这不 Spring Boot 就“配合”得很好,它在 2.2.0 版本(依赖于 Spring 5.2.0)起就把它的所有的主动配置类的此属性改为了 false,即@Configuration(proxyBeanMethods = false)


Full 模式 /Lite 模式实现上的差别

因为 Spring 5.2.0 新增了 proxyBeanMethods 属性来管制模式,因而实现上也有些许惊讶,请各位留神甄别:

Spring 5.2.0+ 版本判断实现:

ConfigurationClassUtils:Map<String, Object> config = metadata.getAnnotationAttributes(Configuration.class.getName());
        if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) {beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        } else if (config != null || isConfigurationCandidate(metadata)) {beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        } else {return false;}

Spring 5.2.0- 版本判断实现:

ConfigurationClassUtils:if (isFullConfigurationCandidate(metadata)) {beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL);
        } else if (isLiteConfigurationCandidate(metadata)) {beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_LITE);
        } else {return false;}

思考题?

  1. 既然 isConfigurationCandidate() 判断办法是为 checkConfigurationClassCandidate() 服务,那 Spring 为何也把它设计为 public static 呢?
  2. ConfigurationClassUtils里还存在对 @Order 程序的解析办法,不是说 Spring 的 Bean 是无序的吗?这又如何了解呢?

总结

本文作为上篇文章的续篇,解释了 @Configuration 配置的 Full 模式和 Lite 模式的判断原理,同时顺带的也介绍了什么叫 主配置配和次配置类,这个概念(尽管官网并不这么叫)对你了解 Spring Framework 是十分有帮忙的。如果你应用是基于 Spring 5.2.0+ 的版本,在理解了这两篇文章内容的根底上,倡议你的配置类均采纳 Lite 模式去做,即显示设置proxyBeanMethods = false

另外对于此局部内容,有些更为感兴趣的小伙伴问到:为什么 Full 模式下通过办法调用指向的仍旧是原来的 Bean,保障了只会执行一次呢?开启的是 Full 模式这只是表象起因,想要答复此问题须要 波及到 CGLIB 加强实现的深水区 内容,为了满足这些好奇(好学)的娃子,打算会在下篇文章持续再拿一篇专程解说(预计篇幅不短,万字以上),你可订阅我的公众号持续保持关注。


关注 A 哥

  • 原创不易,码字更不易。关注 A 哥的公众号【BAT 的乌托邦】,开启有深度的专栏式学习,回绝浅尝辄止
  • 专栏式学习,咱们是认真的(关注公众号回复“常识星球”领券后再轻装入驻)
  • 加 A 哥好友(fsx641385712),备注“Java 入群”邀你进入【Java 高工、架构师】系列纯纯纯技术群

退出移动版