Spring注解配置:@Configuration 和 @Component 区别及原理详解

本文涉及的产品
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
简介: 随着`Spring Boot`的盛行,注解配置式开发受到了大家的青睐,从此告别了基于`Spring`开发的繁琐`XML`配置。这里先来提纲挈领的了解一下`Spring`内部对于配置注解的定义,如`@Component、@Configuration、@Bean、@Import`等注解,从功能上来讲,这些注解所负责的功能的确不相同,但是

1.背景

随着Spring Boot的盛行,注解配置式开发受到了大家的青睐,从此告别了基于Spring开发的繁琐XML配置。这里先来提纲挈领的了解一下Spring内部对于配置注解的定义,如@Component、@Configuration、@Bean、@Import等注解,从功能上来讲,这些注解所负责的功能的确不相同,但是从本质上来讲,Spring内部都将其作为配置注解进行处理。

对于一个成熟的框架来讲,简单及多样化的配置是至关重要的,那么Spring也是如此,从Spring的配置发展过程来看,整体的配置方式从最初比较“原始”的阶段到现在非常“智能”的阶段,这期间Spring做出的努力是非常巨大的,从XML到自动装配,从Spring到Spring Boot,从@Component到@Configuration以及@Conditional,Spring发展到今日,在越来越好用的同时,也为我们隐藏了诸多的细节,那么今天让我们一起探秘@Component与@Configuration。

我们平时在Spring的开发工作中,基本都会使用配置注解,尤其以@Component及@Configuration为主,当然在Spring中还可以使用其他的注解来标注一个类为配置类,这是广义上的配置类概念,但是这里我们只讨论@Component和@Configuration,因为与我们的开发工作关联比较紧密,那么接下来我们先讨论下一个问题,就是@Component与@Configuration有什么区别?

项目推荐:基于SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级系统架构底层框架封装,解决业务开发时常见的非功能性需求,防止重复造轮子,方便业务快速开发和企业技术栈框架统一管理。引入组件化的思想实现高内聚低耦合并且高度可配置化,做到可插拔。严格控制包依赖和统一版本管理,做到最少化依赖。注重代码规范和注释,非常适合个人学习和企业使用

Github地址https://github.com/plasticene/plasticene-boot-starter-parent

Gitee地址https://gitee.com/plasticene3/plasticene-boot-starter-parent

微信公众号Shepherd进阶笔记

2.@Component与@Configuration使用

2.1注解定义

在讨论两者区别之前,先来看看两个注解的定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
   

    /**
     * The value may indicate a suggestion for a logical component name,
     * to be turned into a Spring bean in case of an autodetected component.
     * @return the suggested component name, if any (or empty String otherwise)
     */
    String value() default "";

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
   

    @AliasFor(annotation = Component.class)
    String value() default "";

    boolean proxyBeanMethods() default true;

}

从定义来看, @Configuration 注解本质上还是 @Component,因此 @ComponentScan 能扫描到@Configuration 注解的类。

2.2注解使用

接下来看看两者在我们日常开发中的使用:以这两种注解来标注一个类为配置类

@Configuration
public class AppConfig {
   
}

@Component
public class AppConfig {
   
}

上面的程序,Spring会将其认为配置类来做处理,但是这里有一个概念需要明确一下,就是在Spring中,对于配置类来讲,其实是有分类的,大体可以分为两类,一类称为LITE模式,另一类称为FULL模式,那么对应上面的注解,@Component就是LITE类型,@Configuration就是FULL类型,如何理解这两种配置类型呢?我们先来看这个程序。

当我们使用@Component实现配置类时:

@Component
public class AppConfig {
   
    @Bean
    public Foo foo() {
   
        System.out.println("foo() invoked...");
        Foo foo = new Foo();
        System.out.println("foo() 方法的 foo hashcode: " + foo.hashCode());
        return foo;
    }

    @Bean
    public Eoo eoo() {
   
        System.out.println("eoo() invoked...");
        Foo foo = foo();
        System.out.println("eoo() 方法的 foo hashcode: "+ foo.hashCode());
        return new Eoo();
    }

    public static void main(String[] args) {
   
        AnnotationConfigApplicationContext context =
                new AnnotationConfigApplicationContext(AppConfig.class);
    }
}

执行结果如下:

foo() invoked...
foo() 方法的 foo hashcode: 815992954
eoo() invoked...
foo() invoked...
foo() 方法的 foo hashcode: 868737467
eoo() 方法的 foo hashcode: 868737467

从结果可知,foo()方法执行了两次,一次是bean方法执行的,一次是eoo()调用执行的,所以两次生成的foo对象是不一样的。很符合大家的预期,但是当我们使用@Configuration标注配置类时,执行结果如下:

foo() invoked...
foo() 方法的 foo hashcode: 849373393
eoo() invoked...
eoo() 方法的 foo hashcode: 849373393

这里可以看到foo()方法只执行了一次,同时eoo()方法调用foo()生成的foo对象是同一个。这也就是@Component@Configuration的区别现象展示,那么为什么会有这样的一个现象?我们来考虑一个问题,就是eoo()方法中调用了foo()方法,很明显这个foo()这个方法就是会形成一个新对象,假设我们调用的foo()方法不是原来的foo()方法,是不是就可能不会形成新对象?如果我们在调用foo()方法的时候去容器中获取一下foo这个Bean,是不是就可以达到这样的效果?那如何才能达到这样的效果呢?有一个方法,代理!换句话说,我们调用的eoo()和foo()方法,包括AppConfig都被Spring代理了,那么这里我们明白了@Component与@Configuration最根本的区别,那就是@Configuration标注的类会被Spring代理,其实这样描述不是非常严谨,更加准确的来说应该是如果一个类的BeanDefinition的Attribute中有Full配置属性,那么这个类就会被Spring代理

3.Spring如何实现FULL配置的代理

如果要明白这一点,那么还需要明确一个前提,就是Spring在什么时间将这些配置类转变成FULL模式或者LITE模式的,接下来我们就要介绍个人认为在Spring中非常重要的一个类,ConfigurationClassPostProcessor。

3.1ConfigurationClassPostProcessor是什么

首先来简单的看一下这个类的定义:

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

由这个类定义可知这个类的类型为BeanDefinitionRegistryPostProcessor,以及实现了众多Spring内置的Aware接口,如果了解Beanfactory的后置处理器,那应该清楚ConfigurationClassPostProcessor的执行时机,当然不了解也没有问题,我们会在后面将整个流程阐述清楚,现在需要知道的是ConfigurationClassPostProcessor这个类是在什么时间被实例化的?

3.2ConfigurationClassPostProcessor在什么时间被实例化

要回答这个问题,需要先明确一个前提,那就是ConfigurationClassPostProcessor这个类对应的BeanDefinition在什么时间注册到Spring的容器中的,因为Spring的实例化比较特殊,主要是基于BeanDefinition来处理的,那么现在这个问题就可以转变为ConfigurationClassPostProcessor这个类是在什么时间被注册为一个Beandefinition的?这个可以在源代码中找到答案,具体其实就是在初始化这个Spring容器的时候。

new AnnotationConfigApplicationContext(ConfigClass.class)
  -> new AnnotatedBeanDefinitionReader(this);
    -> AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);
      -> new RootBeanDefinition(ConfigurationClassPostProcessor.class);
        -> registerPostProcessor(BeanDefinitionRegistry registry, RootBeanDefinition definition, String beanName)

从这里可以看出,ConfigurationClassPostProcessor已经被注册为了一个BeanDefinition,上面我们讲了Spring是通过对BeanDefinition进行解析,处理,实例化,填充,初始化以及众多回调等等步骤才会形成一个Bean,那么现在ConfigurationClassPostProcessor既然已经形成了一个BeanDefinition。

3.3 @Component与@Configuration的实现区别

上面ConfigurationClassPostProcessor已经注册到BeanDefinition注册中心了,说明Spring会在某个时间点将其处理成一个Bean,那么具体的时间点就是在BeanFactory所有的后置处理器的处理过程。

AbstractApplicationContext
  -> refresh()
    -> invokeBeanFactoryPostProcessors(beanFactory);
      -> PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(beanFactory, getBeanFactoryPostProcessors());

这个处理BeanFactory的后置处理器的方法比较复杂,简单说来就是主要处理所有实现了BeanFactoryPostProcessor及BeanDefinitionRegistryPostProcessor的类,当然ConfigurationClassPostProcessor就是其中的一个,那么接下来我们看看实现的方法:

  @Override
  public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) {
   
    int registryId = System.identityHashCode(registry);
    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);
    }
    this.registriesPostProcessed.add(registryId);

    processConfigBeanDefinitions(registry);
  }

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
   
    int factoryId = System.identityHashCode(beanFactory);
    if (this.factoriesPostProcessed.contains(factoryId)) {
   
      throw new IllegalStateException(
          "postProcessBeanFactory already called on this post-processor against " + beanFactory);
    }
    this.factoriesPostProcessed.add(factoryId);
    if (!this.registriesPostProcessed.contains(factoryId)) {
   
      // BeanDefinitionRegistryPostProcessor hook apparently not supported...
      // Simply call processConfigurationClasses lazily at this point then.
      processConfigBeanDefinitions((BeanDefinitionRegistry) beanFactory);
    }

    enhanceConfigurationClasses(beanFactory);
    beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory));
  }

这两个方法应该就是ConfigurationClassPostProcessor最为关键的了,我们在这里先简单的总结一下,第一个方法主要完成了内部类,@Component,@ComponentScan,@Bean,@Configuration,@Import等等注解的处理,然后生成对应的BeanDefinition,另一个方法就是对@Configuration使用CGLIB进行增强,那我们先来看Spring是在哪里区分配置的LITE模式和FULL模式?在第一个方法中有一个checkConfigurationClassCandidate方法:

public static boolean checkConfigurationClassCandidate(
      BeanDefinition beanDef, MetadataReaderFactory metadataReaderFactory) {
   

        // ...

      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;
        }
        // ...      
      }

根据程序的判断可知,如果一个类被@Configuration标注且代理模式为true,那么这个类对应的BeanDefinition将会被Spring添加一个FULL配置模式的属性,有些同学可能对这个”属性“不太理解,这里可以简单说一下,其实这个”属性“在Spring中有一个特定的接口就是AttributeAccessor,BeanDefinition就是继承了这个接口,如何理解这个AttributeAccessor呢?其实也很简单,想想看,BeanDefinition主要是做什么的?这个主要是用来描述Class对象的,例如这个Class是不是抽象的,作用域是什么,是不是懒加载等等信息,那如果一个Class对象有一个“属性”是BeanDefinition描述不了的,那这个要如何处理呢?那这个接口AttributeAccessor又派上用场了,你可以向其中存放任何你定义的数据,可以理解为一个map,现在了解BeanDefinition的属性的含义了么?

在这里也能看到@Configuration(proxyBeanMethods = false)@Component一样效果,都是LITE模式

在这里第一步先判断出这个类是FULL模式还是LITE模式,那么下一步就需要开始执行对配置类的注解的解析了,在ConfigurationClassParser这个类有一个processConfigurationClass方法,里面有一个doProcessConfigurationClass方法,这里就是解析前文所列举的@Component等等注解的过程,解析完成之后,在ConfigurationClassPostProcessor类的方法processConfigBeanDefinitions,有一个loadBeanDefinitions方法,这个方法就是将前文解析成功的注解数据全都注册成BeanDefinition,这就是ConfigurationClassPostProcessor这个类的第一个方法所完成的任务,另外这个方法在这里是非常简单的描述了一下,实际上这个方法非常的复杂,需要慢慢的研究。

接下来再说ConfigurationClassPostProcessor类的enhanceConfigurationClasses方法,这个方法主要完成了对@Configuration注解标注的类的增强,进行CGLIB代理,代码如下:

public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
   
        Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<String, AbstractBeanDefinition>();
        for (String beanName : beanFactory.getBeanDefinitionNames()) {
   
            BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
            if (ConfigurationClassUtils.isFullConfigurationClass(beanDef)) {
   //判断是否被@Configuration标注
                if (!(beanDef instanceof AbstractBeanDefinition)) {
   
                    throw new BeanDefinitionStoreException("Cannot enhance @Configuration bean definition '" +
                            beanName + "' since it is not stored in an AbstractBeanDefinition subclass");
                }
                else if (logger.isWarnEnabled() && beanFactory.containsSingleton(beanName)) {
   
                    logger.warn("Cannot enhance @Configuration bean definition '" + beanName +
                            "' since its singleton instance has been created too early. The typical cause " +
                            "is a non-static @Bean method with a BeanDefinitionRegistryPostProcessor " +
                            "return type: Consider declaring such methods as 'static'.");
                }
                configBeanDefs.put(beanName, (AbstractBeanDefinition) beanDef);
            }
        }
        if (configBeanDefs.isEmpty()) {
   
            // nothing to enhance -> return immediately
            return;
        }
        ConfigurationClassEnhancer enhancer = new ConfigurationClassEnhancer();
        for (Map.Entry<String, AbstractBeanDefinition> entry : configBeanDefs.entrySet()) {
   
            AbstractBeanDefinition beanDef = entry.getValue();
            // If a @Configuration class gets proxied, always proxy the target class
            beanDef.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE);
            try {
   
                // Set enhanced subclass of the user-specified bean class
                Class<?> configClass = beanDef.resolveBeanClass(this.beanClassLoader);
                Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);//生成代理的class
                if (configClass != enhancedClass) {
   
                    if (logger.isDebugEnabled()) {
   
                        logger.debug(String.format("Replacing bean definition '%s' existing class '%s' with " +
                                "enhanced class '%s'", entry.getKey(), configClass.getName(), enhancedClass.getName()));
                    }
                    //替换class,将原来的替换为CGLIB代理的class
                    beanDef.setBeanClass(enhancedClass);
                }
            }
            catch (Throwable ex) {
   
                throw new IllegalStateException("Cannot load configuration class: " + beanDef.getBeanClassName(), ex);
            }
        }
    }

这里需要CGLIB的一些知识,我就简单的在这里总结一下,这个方法从所有的BeanDefinition中找到属性为FULL模式的BeanDefinition,然后对其进行代理增强,设置BeanDefinition的beanClass。然后在增强时有一些细节稍微需要明确一下,就是我们这个普通类中的方法,比如eoo(),foo()等方法,将会被MethodInterceptor所拦截,这个方法的调用将会被BeanMethodInterceptor所代理,到这里我们大家应该稍微明确了ConfigurationClassPostProcessor是在什么时间被实例化,什么时间解析注解配置,什么时间进行配置增强。如果看到这里不太明白,那欢迎与我来讨论。

4.总结

@Component在Spring中是代表LITE模式的配置注解,这种模式下的注解不会被Spring所代理,就是一个标准类,如果在这个类中有@Bean标注的方法,那么方法间的相互调用,其实就是普通Java类的方法的调用。

@Configuration在Spring中是代表FULL模式的配置注解,这种模式下的类会被Spring所代理,那么在这个类中的@Bean方法的相互调用,就相当于调用了代理方法,那么在代理方法中会判断,是否调用getBean方法还是invokeSuper方法,这里就是这两个注解的最根本的区别。

一句话概括就是 @Configuration 中所有带 @Bean 注解的方法都会被动态代理,因此调用该方法返回的都是同一个实例。

目录
相关文章
|
7天前
|
XML Java 数据格式
SpringBoot入门(8) - 开发中还有哪些常用注解
SpringBoot入门(8) - 开发中还有哪些常用注解
25 0
|
3天前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
12 0
|
4天前
|
Java 开发者 微服务
手写模拟Spring Boot自动配置功能
【11月更文挑战第19天】随着微服务架构的兴起,Spring Boot作为一种快速开发框架,因其简化了Spring应用的初始搭建和开发过程,受到了广大开发者的青睐。自动配置作为Spring Boot的核心特性之一,大大减少了手动配置的工作量,提高了开发效率。
20 0
|
14天前
|
XML JSON Java
SpringBoot必须掌握的常用注解!
SpringBoot必须掌握的常用注解!
40 4
SpringBoot必须掌握的常用注解!
|
14天前
|
Java Spring
[Spring]aop的配置与使用
本文介绍了AOP(面向切面编程)的基本概念和核心思想。AOP是Spring框架的核心功能之一,通过动态代理在不修改原代码的情况下注入新功能。文章详细解释了连接点、切入点、通知、切面等关键概念,并列举了前置通知、后置通知、最终通知、异常通知和环绕通知五种通知类型。
27 1
|
16天前
|
JSON Java 数据库
SpringBoot项目使用AOP及自定义注解保存操作日志
SpringBoot项目使用AOP及自定义注解保存操作日志
33 1
|
11天前
|
存储 安全 Java
springboot当中ConfigurationProperties注解作用跟数据库存入有啥区别
`@ConfigurationProperties`注解和数据库存储配置信息各有优劣,适用于不同的应用场景。`@ConfigurationProperties`提供了类型安全和模块化的配置管理方式,适合静态和简单配置。而数据库存储配置信息提供了动态更新和集中管理的能力,适合需要频繁变化和集中管理的配置需求。在实际项目中,可以根据具体需求选择合适的配置管理方式,或者结合使用这两种方式,实现灵活高效的配置管理。
10 0
|
2月前
|
SQL 监控 druid
springboot-druid数据源的配置方式及配置后台监控-自定义和导入stater(推荐-简单方便使用)两种方式配置druid数据源
这篇文章介绍了如何在Spring Boot项目中配置和监控Druid数据源,包括自定义配置和使用Spring Boot Starter两种方法。
|
1月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
162 2
|
8天前
|
缓存 IDE Java
SpringBoot入门(7)- 配置热部署devtools工具
SpringBoot入门(7)- 配置热部署devtools工具
20 2
 SpringBoot入门(7)- 配置热部署devtools工具