【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)(上)

简介: 【小家Spring】Spring解析@Configuration注解的处理器:ConfigurationClassPostProcessor(ConfigurationClassParser)(上)

前言


在Spring3.0以后,官方推荐我们使用注解去驱动Spring应用。那么很多人就一下子懵了,不需要xml配置文件了,那我的那些配置项怎么办呢?

@Configuration是Spring3.0推出来的注解,用来代替xml配置文件。

若一个Class类被标注了这个注解,我们就认为这个类就是一个配置类,然后在这个类里面就可以写相应的其它配置了,比如@Bean等等


既然@Configuration这么重要,它也作为管理其它配置、读取其它配置的入口,大多数小伙伴却并不知道它加载的时机以及解析的方式,这就造成了遇到一些稍微复杂点的问题时,无法入手去定位问题


本文旨在介绍一下Spring是怎么解析@Configuration注解驱动的配置文件的,这里ConfigurationClassPostProcessor这个处理器就闪亮登场了~

附:@Configuration源码:


@Target(ElementType.TYPE) // 它只能标注在类上
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // 它也是个Spring的组件,会被扫描
public @interface Configuration {
  @AliasFor(annotation = Component.class)
  String value() default ""; //也可以自定义Bean的名称
}

Spring源码基于的Spring版本为:5.0.6.RELEASE(下同)

Spring源码基于的Spring版本为:5.0.6.RELEASE(下同)

Spring源码基于的Spring版本为:5.0.6.RELEASE(下同)


ConfigurationClassPostProcessor初识



public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPostProcessor,
    PriorityOrdered, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware { ... }


可以看出它是一个BeanFactoryPostProcessor,并且它是个功能更强大些的BeanDefinitionRegistryPostProcessor,有能力去处理一些Bean的定义信息~


需要知道的是,ConfigurationClassPostProcessor是Spring内部对BeanDefinitionRegistryPostProcessor接口的唯一实现。BeanFactoryPostProcessor顶级接口的实现类如下:


image.png


ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry处理逻辑


为了更好的理解它的运行过程,我们需要知道它在什么时候调用:AbstractApplicationContext#refresh 中的第5步时进行调用postProcessBeanDefinitionRegistry:


  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
    //根据BeanDefinitionRegistry,生成registryId 是全局唯一的。
    int registryId = System.identityHashCode(registry);
    // 判断,如果这个registryId 已经被执行过了,就不能够再执行了,否则抛出异常
    if (this.registriesPostProcessed.contains(registryId)) {
      throw new IllegalStateException(
          "postProcessBeanDefinitionRegistry already called on this post-processor against " + registry);
    }
    if (this.factoriesPostProcessed.contains(registryId)) {
      throw new IllegalStateException(
          "postProcessBeanFactory already called on this post-processor against " + registry);
    }
    // 已经执行过的registry  防止重复执行
    this.registriesPostProcessed.add(registryId);
    // 调用processConfigBeanDefinitions 进行Bean定义的加载.代码如下
    processConfigBeanDefinitions(registry);
  }
  // 解释一下:我们配置类是什么时候注册进去的呢???(此处只讲注解驱动的Spring和SpringBoot)
  // 注解驱动的Spring为:自己在`MyWebAppInitializer`里面自己手动指定进去的
  // SpringBoot为它自己的main引导类里去加载进来的,后面详说SpringBoot部分
  public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
    List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
    // 获取已经注册的bean名称(此处有7个   6个Bean+rootConfig)
    String[] candidateNames = registry.getBeanDefinitionNames();
    for (String beanName : candidateNames) {
      BeanDefinition beanDef = registry.getBeanDefinition(beanName);
      // 这个判断很有意思~~~ 如果你的beanDef现在就已经确定了是full或者lite,说明你肯定已经被解析过了,,所以再来的话输出个debug即可(其实我觉得输出warn也行~~~)
      if (ConfigurationClassUtils.isFullConfigurationClass(beanDef) || ConfigurationClassUtils.isLiteConfigurationClass(beanDef)) {
        if (logger.isDebugEnabled()) {
          logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
        }
      }
      // 检查是否是@Configuration的Class,如果是就标记下属性:full 或者lite。beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL) 
      // 加入到configCandidates里保存配置文件类的定义
      // 显然此处,仅仅只有rootConfig一个类符合条件
      else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
        configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
      }
    }
    // Return immediately if no @Configuration classes were found
    // 如果一个配置文件类都没找到,现在就不需要再继续下去了
    if (configCandidates.isEmpty()) {
      return;
    }
    // Sort by previously determined @Order value, if applicable
    // 把配置文件们:按照@Order注解进行排序  这个意思是,我们@Configuration注解的配置文件是支持order排序的。(备注:普通bean不行的~~~)
    configCandidates.sort((bd1, bd2) -> {
      int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
      int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
      return Integer.compare(i1, i2);
    });
    // Detect any custom bean name generation strategy supplied through the enclosing application context
    // 此处registry是DefaultListableBeanFactory 这里会进去
    // 尝试着给Bean扫描方式,以及import方法的BeanNameGenerator赋值(若我们都没指定,那就是默认的AnnotationBeanNameGenerator:扫描为首字母小写,import为全类名)
    SingletonBeanRegistry sbr = null;
    if (registry instanceof SingletonBeanRegistry) {
      sbr = (SingletonBeanRegistry) registry;
      if (!this.localBeanNameGeneratorSet) {
        BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(CONFIGURATION_BEAN_NAME_GENERATOR);
        if (generator != null) {
          this.componentScanBeanNameGenerator = generator;
          this.importBeanNameGenerator = generator;
        }
      }
    }
    // web环境,这里都设置了StandardServletEnvironment
    // 一般来说到此处,env环境不可能为null了~~~ 此处做一个容错处理~~~
    if (this.environment == null) {
      this.environment = new StandardEnvironment();
    }
    // Parse each @Configuration class
    // 这是重点:真正解析@Configuration类的,其实是ConfigurationClassParser 这个解析器来做的
    // parser 后面用于解析每一个配置类~~~~
    ConfigurationClassParser parser = new ConfigurationClassParser(
        this.metadataReaderFactory, this.problemReporter, this.environment,
        this.resourceLoader, this.componentScanBeanNameGenerator, registry);
    Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
    // 装载已经处理过的配置类,最大长度为:configCandidates.size()
    Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());
    do {
      // 核心方法:具体详解如下
      parser.parse(candidates);
      // 校验 配置类不能使final的,因为需要使用CGLIB生成代理对象,见postProcessBeanFactory方法
      parser.validate();
      Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
      configClasses.removeAll(alreadyParsed);
      // Read the model and create bean definitions based on its content
      // 如果Reader为null,那就实例化ConfigurationClassBeanDefinitionReader来加载Bean,并加入到alreadyParsed中,用于去重(避免譬如@ComponentScan直接互扫)
      if (this.reader == null) {
        this.reader = new ConfigurationClassBeanDefinitionReader(
            registry, this.sourceExtractor, this.resourceLoader, this.environment,
            this.importBeanNameGenerator, parser.getImportRegistry());
      }
      // 此处注意:调用了ConfigurationClassBeanDefinitionReader的loadBeanDefinitionsd的加载配置文件里面的@Bean/@Import们,具体讲解请参见下面
      // 这个方法是非常重要的,因为它决定了向容器注册Bean定义信息的顺序问题~~~
      this.reader.loadBeanDefinitions(configClasses);
      alreadyParsed.addAll(configClasses);
      candidates.clear();
      // 如果registry中注册的bean的数量 大于 之前获得的数量,则意味着在解析过程中又新加入了很多,那么就需要对其进行解继续析
      if (registry.getBeanDefinitionCount() > candidateNames.length) {
        String[] newCandidateNames = registry.getBeanDefinitionNames();
        Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
        Set<String> alreadyParsedClasses = new HashSet<>();
        for (ConfigurationClass configurationClass : alreadyParsed) {
          alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
        }
        for (String candidateName : newCandidateNames) {
          // 这一步挺有意思:若老的oldCandidateNames不包含。也就是说你是新进来的候选的Bean定义们,那就进一步的进行一个处理
          // 比如这里的DelegatingWebMvcConfiguration,他就是新进的,因此它继续往下走
          // 这个@Import进来的配置类最终会被ConfigurationClassPostProcessor这个后置处理器的postProcessBeanFactory 方法,进行处理和cglib增强
          if (!oldCandidateNames.contains(candidateName)) {
            BeanDefinition bd = registry.getBeanDefinition(candidateName);
            if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
                !alreadyParsedClasses.contains(bd.getBeanClassName())) {
              candidates.add(new BeanDefinitionHolder(bd, candidateName));
            }
          }
        }
        candidateNames = newCandidateNames;
      }
    }
    while (!candidates.isEmpty());
    // Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
    //如果SingletonBeanRegistry 不包含org.springframework.context.annotation.ConfigurationClassPostProcessor.importRegistry,
    //则注册一个,bean 为 ImportRegistry. 一般都会进行注册的
    if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
      sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
    }
    //清楚缓存 元数据缓存
    if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
      // Clear cache in externally provided MetadataReaderFactory; this is a no-op
      // for a shared cache since it'll be cleared by the ApplicationContext.
      ((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
    }
  }


需要注意下面两个核心方法,是如何判断某个类是否为配置类的(判断是full模式,还是lite模式的配置文件):

  //如果类上有@Configuration注解说明是一个完全(Full)的配置类
  public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {
    return metadata.isAnnotated(Configuration.class.getName());
  }
  public static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) {
    // Do not consider an interface or an annotation...
    if (metadata.isInterface()) {
      return false;
    }
    // Any of the typical annotations found?
    for (String indicator : candidateIndicators) {
      if (metadata.isAnnotated(indicator)) {
        return true;
      }
    }
    // Finally, let's look for @Bean methods...
    try {
      return metadata.hasAnnotatedMethods(Bean.class.getName());
    } catch (Throwable ex) {
      return false;
    }
  }
candidateIndicators内容如下:
  private static final Set<String> candidateIndicators = new HashSet<>(8);
  static {
    candidateIndicators.add(Component.class.getName());
    candidateIndicators.add(ComponentScan.class.getName());
    candidateIndicators.add(Import.class.getName());
    candidateIndicators.add(ImportResource.class.getName());
  }

1.如果类上有@Configuration注解说明是一个完全(Full)的配置类


2.如果如果类上面有@Component,@ComponentScan,@Import,@ImportResource这些注解,那么就是一个简化配置类。如果不是上面两种情况,那么有@Bean注解修饰的方法也是简化配置类

完整@Configuration和Lite @Bean模式(Full模式和Lite模式的区别)


首先看看Spring对此的定义:在ConfigurationClassUtils里

  // 只要这个类标注了:@Configuration注解就行  哪怕是接口、抽象类都木有问题
  public static boolean isFullConfigurationCandidate(AnnotationMetadata metadata) {
    return metadata.isAnnotated(Configuration.class.getName());
  }
  // 判断是Lite模式:(首先肯定没有@Configuration注解)
  // 1、不能是接口
  // 2、但凡只有标注了一个下面注解,都算lite模式:@Component @ComponentScan @Import @ImportResource
  // 3、只有存在有一个方法标注了@Bean注解,那就是lite模式
  public static boolean isLiteConfigurationCandidate(AnnotationMetadata metadata) {
    // 不能是接口
    if (metadata.isInterface()) {
      return false;
    }
    // 但凡只有标注了一个下面注解,都算lite模式:@Component @ComponentScan @Import @ImportResource
    for (String indicator : candidateIndicators) {
      if (metadata.isAnnotated(indicator)) {
        return true;
      }
    }
    // 只有存在有一个方法标注了@Bean注解,那就是lite模式
    try {
      return metadata.hasAnnotatedMethods(Bean.class.getName());
    }
  }
  // 不管是Full模式还是Lite模式,都被认为是候选的配置类  是上面两个方法的结合
  public static boolean isConfigurationCandidate(AnnotationMetadata metadata) {
    return (isFullConfigurationCandidate(metadata) || isLiteConfigurationCandidate(metadata));
  }
  // 下面两个方法是直接判断Bean定义信息,是否是配置类,至于Bean定义里这个属性啥时候放进去的,请参考
  //ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)方法,它会对每个Bean定义信息进行检测(毕竟刚开始Bean定义信息是非常少的,所以速度也很快)
  public static boolean isFullConfigurationClass(BeanDefinition beanDef) {
    return CONFIGURATION_CLASS_FULL.equals(beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE));
  }
  public static boolean isLiteConfigurationClass(BeanDefinition beanDef) {
    return CONFIGURATION_CLASS_LITE.equals(beanDef.getAttribute(CONFIGURATION_CLASS_ATTRIBUTE));
  }


结论先说在前头:Full模式和Lite模式的唯一区别:Full模式的配置组件会被enhance(加强/代理),而Liter模式不会。其余使用方式都一样,比如@Bean、@Import等等


和full模式不同的是,Lite模式不能声明Bean之间的依赖关系。也就是说入参、Java方法调用,都不能达到直接注入的效果。特别是Java方法调用,就直接进方法体了。


在常见的场景中,@Bean方法将在@Configuration类中声明,确保始终使用“完整”模式,并因此将交叉方法引用重定向到容器的生命周期管理。这可以防止@Bean通过常规Java调用意外地调用相同的方法(这也就是为什么我们用方法调用,其实还是去容器里找Bean的原因,并不是新建了一个Bean),这有助于减少在“精简”模式下操作时难以跟踪的细微错误。


例子:


@Configuration 
public class AppConfig {
  @Bean
  public Foo foo() {
    return new Foo(bar()); // 这里调用的bar()方法
  }
  @Bean
  public Bar bar() {
    return new Bar();
  }
}


Foo 接受一个bar的引用来进行构造器注入:

这种方法声明的bean的依赖关系只有在@Configuration类的@Bean方法中有

效。


但是,如果换成@Component(Lite模式),则foo()方法中new Foo(bar())传入的bar()方法会每次产生一个新的Bar对象

结论:


在@Component或其他组建中使用@Bean好处是不会启动CGLIB这种重量级工具(不过在Spring中即使这里不使用,其他很多地方也在使用)。并且@Component及其相关的Stereotype组件自身就有摸框级别的功能,在这里使用@Bean注解能很好的表明一个Bean的从属和结构关系,但是需要注意直接调用方法的“副作用”。


个人建议如果没什么特别的要求就使用@Configuration,引入CGLIB并不会影响多少性能,然而坑会少很多。

在spring官网将用@Configuration创建的@Bean称呼为"Full"模式、将@Component创建的@Bean称呼为"'lite"模式,从字面上也能略知他们的差异。


只有类上标注@Configuration才是full模式。标注有@Component、@ComponentScan、@Import、@ImportResource或者啥注解都没标注但是有被标注了@Bean的方法这种也是lite模式


相关文章
|
5月前
|
数据采集 人工智能 Java
1天消化完Spring全家桶文档!DevDocs:一键深度解析开发文档,自动发现子URL并建立图谱
DevDocs是一款基于智能爬虫技术的开源工具,支持1-5层深度网站结构解析,能将技术文档处理时间从数周缩短至几小时,并提供Markdown/JSON格式输出与AI工具无缝集成。
202 1
1天消化完Spring全家桶文档!DevDocs:一键深度解析开发文档,自动发现子URL并建立图谱
|
5月前
|
安全 Java API
深入解析 Spring Security 配置中的 CSRF 启用与 requestMatchers 报错问题
本文深入解析了Spring Security配置中CSRF启用与`requestMatchers`报错的常见问题。针对CSRF,指出默认已启用,无需调用`enable()`,只需移除`disable()`即可恢复。对于`requestMatchers`多路径匹配报错,分析了Spring Security 6.x中方法签名的变化,并提供了三种解决方案:分次调用、自定义匹配器及降级使用`antMatchers()`。最后提醒开发者关注版本兼容性,确保升级平稳过渡。
635 2
|
6月前
|
存储 Java 文件存储
微服务——SpringBoot使用归纳——Spring Boot使用slf4j进行日志记录—— logback.xml 配置文件解析
本文解析了 `logback.xml` 配置文件的详细内容,包括日志输出格式、存储路径、控制台输出及日志级别等关键配置。通过定义 `LOG_PATTERN` 和 `FILE_PATH`,设置日志格式与存储路径;利用 `&lt;appender&gt;` 节点配置控制台和文件输出,支持日志滚动策略(如文件大小限制和保存时长);最后通过 `&lt;logger&gt;` 和 `&lt;root&gt;` 定义日志级别与输出方式。此配置适用于精细化管理日志输出,满足不同场景需求。
1448 1
|
5月前
|
前端开发 安全 Java
Spring Boot 便利店销售系统项目分包设计解析
本文深入解析了基于Spring Boot的便利店销售系统分包设计,通过清晰的分层架构(表现层、业务逻辑层、数据访问层等)和模块化设计,提升了代码的可维护性、复用性和扩展性。具体分包结构包括`controller`、`service`、`repository`、`entity`、`dto`、`config`和`util`等模块,职责分明,便于团队协作与功能迭代。该设计为复杂企业级应用开发提供了实践参考。
213 0
|
3月前
|
Java 数据库连接 API
Java 对象模型现代化实践 基于 Spring Boot 与 MyBatis Plus 的实现方案深度解析
本文介绍了基于Spring Boot与MyBatis-Plus的Java对象模型现代化实践方案。采用Spring Boot 3.1.2作为基础框架,结合MyBatis-Plus 3.5.3.1进行数据访问层实现,使用Lombok简化PO对象,MapStruct处理对象转换。文章详细讲解了数据库设计、PO对象实现、DAO层构建、业务逻辑封装以及DTO/VO转换等核心环节,提供了一个完整的现代化Java对象模型实现案例。通过分层设计和对象转换,实现了业务逻辑与数据访问的解耦,提高了代码的可维护性和扩展性。
146 1
|
2月前
|
缓存 安全 Java
Spring 框架核心原理与实践解析
本文详解 Spring 框架核心知识,包括 IOC(容器管理对象)与 DI(容器注入依赖),以及通过注解(如 @Service、@Autowired)声明 Bean 和注入依赖的方式。阐述了 Bean 的线程安全(默认单例可能有安全问题,需业务避免共享状态或设为 prototype)、作用域(@Scope 注解,常用 singleton、prototype 等)及完整生命周期(实例化、依赖注入、初始化、销毁等步骤)。 解析了循环依赖的解决机制(三级缓存)、AOP 的概念(公共逻辑抽为切面)、底层动态代理(JDK 与 Cglib 的区别)及项目应用(如日志记录)。介绍了事务的实现(基于 AOP
100 0
|
2月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
108 0
|
3月前
|
Java 测试技术 数据库
说一说 SpringBoot 整合 Junit5 常用注解
我是小假 期待与你的下一次相遇 ~
|
4月前
|
安全 Java API
Spring Boot 功能模块全解析:构建现代Java应用的技术图谱
Spring Boot不是一个单一的工具,而是一个由众多功能模块组成的生态系统。这些模块可以根据应用需求灵活组合,构建从简单的REST API到复杂的微服务系统,再到现代的AI驱动应用。
|
3月前
|
Java 数据库 开发者
Spring Boot 框架超级详细总结及长尾关键词应用解析
本文深入讲解Spring Boot框架的核心概念、功能特性及实际应用,涵盖自动配置、独立运行、starter依赖等优势。通过Web开发、微服务架构、批处理等适用场景分析,结合在线书店实战案例,演示项目初始化、数据库设计、分层架构实现全流程。同时探讨热部署、多环境配置、缓存机制与事务管理等高级特性,助你高效掌握Spring Boot开发技巧。代码示例详尽,适合从入门到进阶的学习者。
1075 0

热门文章

最新文章

推荐镜像

更多
  • DNS