Spring解读@Component和@Configuration的区别以及源码分析

 更新时间:2024年10月25日 10:38:27   作者:魔道不误砍柴功  
通过实例分析@Component和@Configuration注解的区别,核心在于@Configuration会通过CGLIB代理确保Bean的单例,而@Component不会,在Spring容器中,使用@Configuration注解的类会被CGLIB增强,保证了即使在同一个类中多次调用@Bean方法

之前一直搞不清 @Component 和 @Configuration 这两个注解到底有啥区别,一直认为被这两修饰的类可以被 Spring 实例化嘛,不,还是见识太短,直到今天才发现这两玩意有这么大区别。

很幸运能够及时发现,后面可以少走点坑,下面就直接通过最简单的案例来说明它两的区别,颠覆你的认知。

1、案例演示

定义一个 Apple 实体类,通过 @Bean 的方式交给 Spring 管理,如下:

public class Apple {

}

在定义一个 AppleFactory 工厂类也可以获取到 Apple 类实例,如下:

public class AppleFactory {

	private Apple apple;

	public Apple getApple() {
		return apple;
	}

	public void setApple(Apple apple) {
		this.apple = apple;
	}
}

在定义一个 AppleAutoConfiguration 类,此时先用 @Configuration 注解修饰该类,如下:

@Configuration
public class AppleAutoConfiguration {

	@Bean
	public Apple apple() {
		return new Apple();
	}

	@Bean
	public AppleFactory appleFactory() {
		AppleFactory appleFactory = new AppleFactory();

		appleFactory.setApple(apple());
		return appleFactory;
	}
}

先来分析下 AppleAutoConfiguration 类中的方法,apple() 方法中直接通过 new 关键字创建了一个 Apple 对象,这个没啥问题

继续看到 appleFactory() 方法内部,又调用了 apple() 方法,此时仔细想想,Spring 是不是调用了两次 apple() ,第一次是 Spring 扫描到 @Bean 注解调用一次,第二次是在 Spring 扫描到 @Bean 注解调用 appleFactory() 方法,appleFactory() 方法中又调用一次 apple() 方法,调两次 new 创建对象,必然会出现两个不一样的 Apple 对象,这样必然违背了 Spring 单例设计思想。

接下来测试下结果,看下是不是和我们想象的结果一样呢?

测试如下:

@ComponentScan
public class ComponentConfigurationTest {
	public static void main(String[] args) {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ComponentConfigurationTest.class);


		AppleFactory appleFactory = context.getBean(AppleFactory.class);
		Apple apple = appleFactory.getApple();
		System.out.println("从 AppleFactory 获取的 apple = " + apple);


		Apple bean = context.getBean(Apple.class);
		System.out.println("从 Spring 容器中获取的 apple = " + bean);
	}
}

最终输出结果如下:

从 AppleFactory 获取的 apple = com.gwm.configurationanno.Apple@2f465398
从 Spring 容器中获取的 apple = com.gwm.configurationanno.Apple@2f465398

发现这结果和我们刚刚的猜想不一样啊,其实大家隐约应该猜到,是因为这里使用的是 @Configuration 注解,所以这里最终你调用多少次最终都是同一个对象,原理稍后分析。

那么接下来肯定是要把 @Configuration 注解替换成 @Component 试试,修改之后的代码如下:

@Component
public class AppleAutoConfiguration {

	@Bean
	public Apple apple() {
		return new Apple();
	}

	@Bean
	public AppleFactory appleFactory() {
		AppleFactory appleFactory = new AppleFactory();

		appleFactory.setApple(apple());
		return appleFactory;
	}
}

​​​​​​

测试结果如下:

从 AppleFactory 获取的 apple = com.gwm.configurationanno.Apple@2f465398
从 Spring 容器中获取的 apple = com.gwm.configurationanno.Apple@2f465353

最终发现这个结果和我们在前面想象的结果一模一样,果然就创建了两个 Apple 对象,违背了 Spring 单例设计思想,这个区别非常非常的重要,因为在 SpringBoot 中为什么配置类都是使用的 @Configuration 注解,并不是直接使用 @Component 注解,这个原因想必大家应该也知道了。

上面这个例子在 SpringBoot 中有非常多的应用,在来看下另一个例子,如下:

定义一个 Orange 实体类,通过 FactoryBean 接口将 Orange 类交给 Spring 管理,如下:

public class Orange {

}

再定义个 OrangeFactoryBean 类实现 FactoryBean 接口,调用 getObject() 方法可以创建 Orange 对象,如下:

public class OrangeFactoryBean implements FactoryBean<Orange> {
	@Override
	public Orange getObject() throws Exception {
		return new Orange();
	}

	@Override
	public Class<?> getObjectType() {
		return Orange.class;
	}
}

大家都知道实现了 FactoryBean 接口的类最终会去调用 getObject() 方法然后创建对象。然后再在 AppleAutoConfiguration 类中调用 getObject() 方法,这里直接使用 @Component 注解演示,因为 @Configuration 注解肯定是没问题的,如下:

@Component
public class AppleAutoConfiguration {

	@Bean
	public Apple apple() {
		return new Apple();
	}

	@Bean
	public AppleFactory appleFactory() throws Exception {
		AppleFactory appleFactory = new AppleFactory();

		appleFactory.setApple(apple());
		
		OrangeFactoryBean orangeFactoryBean = orangeFactoryBean();
		System.out.println("appleFactory orangeFactoryBean = " + orangeFactoryBean);
		Orange orange = orangeFactoryBean.getObject();
		System.out.println("appleFactory orange="+orange);
		return appleFactory;
	}

	@Bean
	public OrangeFactoryBean orangeFactoryBean() {
		return new OrangeFactoryBean();
	}
}

这里通过 @Bean 注入 OrangeFactoryBean 实例,因为 AppleAutoConfiguration 类被 @Component 修饰,所以这里 Spring 和 手动调用两次创建的 OrangeFactoryBean 不一样,导致 getObject() 其实也是不一样的。

测试代码如下:

@ComponentScan
public class ComponentConfigurationTest {
	public static void main(String[] args) throws Exception {
		AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ComponentConfigurationTest.class);


		AppleFactory appleFactory = context.getBean(AppleFactory.class);
		Apple apple = appleFactory.getApple();
		System.out.println("从 AppleFactory 获取的 apple = " + apple);


		Apple bean = context.getBean(Apple.class);
		System.out.println("从 Spring 容器中获取的 apple = " + bean);


		Orange bean1 = context.getBean(Orange.class);
		System.out.println("从 Spring 容器中获取的 orange = "+bean1);

		Object bean2 = context.getBean("orangeFactoryBean");
		System.out.println("=>从 Spring 容器中获取的 orange = " + bean2);

		OrangeFactoryBean bean3 = context.getBean(OrangeFactoryBean.class);
		System.out.println("bean3 = " + bean3);
	}
}

输出结果如下:

appleFactory orangeFactoryBean = com.gwm.configurationanno.OrangeFactoryBean@c0c2f8d
appleFactory orange=com.gwm.configurationanno.Orange@305b7c14
从 AppleFactory 获取的 apple = com.gwm.configurationanno.Apple@484970b0
从 Spring 容器中获取的 apple = com.gwm.configurationanno.Apple@4470f8a6
从 Spring 容器中获取的 orange = com.gwm.configurationanno.Orange@7c83dc97
=>从 Spring 容器中获取的 orange = com.gwm.configurationanno.Orange@7c83dc97
Spring 容器中的 orangeFactoryBean = com.gwm.configurationanno.OrangeFactoryBean@7748410a

通过以上两个案例可以清楚的知道 @Configuration 和 @Component 注解的区别,那么这个底层是怎么实现的呢?

2、源码解析

2.1、@Configuration 简单思路分析

这里我们可以大致的思考下,产生多个实例无非就是没有从 Spring 缓存中取值嘛,如果都是从 Spring 缓存中取值,那么必然就不会出现那么多对象。

对于 apple() 方法因为被 @Bean 修饰,所以在 Spring 实例化过程中会被调用 ,然后创建完实例将其放到 Spring 单例缓冲池中,那么下次就可以直接从缓存中获取到 Apple 实例(@Bean 注入 bean 流程看另一篇文章)。

对于 appleFactory() 方法中去调用 apple() 方法,apple() 方法又会 new 一个新的 Apple 实例,那么怎么样避免它重复创建对象呢?是不是可以通过代理改变 apple() 方法内部的逻辑,改成让它直接从 Spring 缓冲池中获取 Apple 的实例,这样就避免了重复创建对象。

如果要把 apple() 方法改成代理方法,是不是需要将所在的类 AppleAutoConfiguration 变成代理对象即可,Spring 就是这样干的,加上 @Configuration 注解标识之后,Spring 就会通过 cglib 代理创建 AppleAutoConfiguration 实例,所以你在 Spring 容器中获取 AppleAutoConfiguration 类是一个代理类,并不是真正的实体类。

跟着上述思路再去看源码,就简单多了。

2.2、@Configuration 源码分析之普通类型方法调用

首先看 ConfigurationClassPostProcessor 类解析 @Configuration 注解的地方,会先做个 full 标记,源码如下:


标记做好了之后,后面就可以通过判断是否有这个标记,然后要不要用代理创建实例,进入到 ConfigurationClassPostProcessor 类的 postProcessBeanFactory() 方法,源码如下:

这里会去判断当前 beanClass 是否有 full 标记,有的话就加入到 configBeanDefs 容器中,准备要用代理方式生成实例 bean。

然后开始遍历 configBeanDefs 容器,通过 ConfigurationClassEnhancer 对象挨个创建代理类,这里增强的是 Class 字节码文件,其实工作中这种方法也是可以借鉴的。

其中增强逻辑都放在了拦截器中(BeanMethodInterceptorBeanFactoryAwareMethodInterceptor) 源码如下:

从这两个拦截器侧面说明 @Configuration 和 @Bean 才是老搭档,干活不累,紧密相连,相辅相成,所以你在使用 @Bean 的时候,最好在外层类上标注 @Configuration,不要使用 @Component 注解。

也就是说当你触发了目标方法的调用时,就会回调到这两个拦截器链,但是具体执行哪个拦截器是需要条件的,BeanMethodInterceptor 拦截器的条件需要,如下所示:

BeanFactoryAwareMethodInterceptor 拦截器需要条件,源码如下:

我们这里满足 BeanMethodInterceptor 拦截器的执行条件,所以 Spring 在调用 apple() 方法的时候,会触发 BeanMethodInterceptor 拦截器增强逻辑的执行,增强逻辑如下:

这里有一个判断 isCurrentlyInvokedFactoryMethod() 非常关键,因为这个开关控制着你是否会多次创建实例,进入该方法内部,源码如下:

可以发现这里有一个 ThreadLocal 类型的容器,那么这个容器什么时候会有值呢?这个就要需要你对 @Bean 的实例化流程非常了解了,这里简单摘取核心部分,源码如下:

在 @Bean 实例化流程的时候就用到了这个 currentlyInvokedFactoryMethod 容器,先把值放进去,然后反射调用完方法之后又删除。其实这只是一个标记作用。

Spring 在调用 @Bean 修饰的 apple() 方法时,currentlyInvokedFactoryMethod 容器中放的就是 apple,然后通过 set() 反射调用 apple() 方法,注意此时的 AppleAutoConfiguration 是一个代理对象,所以调用 apple() 方法就会触发走切面逻辑,因为是 @Bean 修饰的,所以走的是 BeanMethodInterceptor 这个类的增强逻辑,源码如下:

注意此时的判断逻辑 isCurrentlyInvokedFactoryMethod() 是有值的哦,存的就是 apple,所以这里就直接走 if 逻辑,直接调用目标方法逻辑,直接 new Apple() 对象,然后返回到 set() 反射调用处,在删除 currentlyInvokedFactoryMethod 容器中的值 apple,然后就是 Spring 实例化 @Bean 的后续流程,最终会将这个 new 出来的实例放到 Spring 一级缓存中,源码如下:

那么重点来了,Spring 在执行 @Bean 修饰的 appleFactory() 方法时, isCurrentlyInvokedFactoryMethod 容器中那么就是存的 appleFactory,然后通过反射 set() 方法去调用 appleFactory() 方法,然后再 appleFactory() 方法中执行逻辑时发现又调用了 apple() 方法,那么又会触发进入 apple() 方法的增强逻辑 BeanMethodInterceptor,源码如下:

注意此时的 isCurrentlyInvokedFactoryMethod() 判断逻辑,当前入参 beanMethod 是 apple,但是 isCurrentlyInvokedFactoryMethod 容器中刚刚存放的是外面方法 appleFactory() 的值,所以这里 isCurrentlyInvokedFactoryMethod() 方法判断条件不成立,走 resolveBeanReference() 逻辑,源码如下:

这里面这段逻辑会触发 getBean() 流程,此时 getBean() 流程去获取 Apple 类实例,肯定是从单例缓冲池中获取得,因为之前在执行 @Bean 修饰的 apple() 方法就已将实例存入到了 Spring 的一级缓存中,所以在 appleFactory() 方法中,不管你调用多少次,都不会重复创建 Apple 类实例,因为最终都是通过切面逻辑去调用 getBean() 从缓存中获取得,必然是同一个实例。

2.3、@Configuration 源码分析之 FactoryBean 类型方法调用

Apple 是一个普通类,上面已经解析完,现在来解析一下实现 FactoryBean 接口的 OrangeFactoryBean 特殊一点类型的看 Spring 又是如何处理的。

public class Orange {

}

public class OrangeFactoryBean implements FactoryBean<Orange> {
	@Override
	public Orange getObject() throws Exception {
		return new Orange();
	}

	@Override
	public Class<?> getObjectType() {
		return Orange.class;
	}
}


@Configuration
public class AppleAutoConfiguration {

	@Bean
	public AppleFactory appleFactory() throws Exception {
		OrangeFactoryBean orangeFactoryBean = orangeFactoryBean();
		System.out.println("appleFactory orangeFactoryBean = " + orangeFactoryBean);
		Orange orange = orangeFactoryBean.getObject();
		System.out.println("appleFactory orange="+orange);

		return appleFactory;
	}

	@Bean
	public OrangeFactoryBean orangeFactoryBean() {
		return new OrangeFactoryBean();
	}
}

其实只需要看切面逻辑就可以,AppleAutoConfiguration 类的实例是一个代理对象,在调用 appleFactory() 方法里面调用 orangeFactoryBean() 方法时会触发进入切面逻辑(BeanMethodInterceptor),因为 OrangeFactoryBean 这个类有点特殊,实现了 FactoryBean 接口,所以在切面逻辑(BeanMethodInterceptor)实现会有一点不一样,源码如下:

从上面源码分析,当在 appleFactory() 方法中调用 orangeFactoryBean() 方法时会触发进入 BeanMethodInterceptor 切面逻辑,然后在切面中会去判断是否是 FactoryBean 接口类型,恰好 OrangeFactoryBean 就是 FactoryBean 类型,所以会直接调用 getBean() 流程,此时注意,beanName 是包含了 & 符号,表示是需要实例化 OrangeFactoryBean 类,这个特别注意。因为不带 & 符号的话会调用 getObject() 方法创建 Orange 实例。注意这里是包含了 & 符号,是要去创建 OrangeFactoryBean 实例。

当调用 getBean() 去创建 OrangeFactoryBean 实例时,因为 AppleAutoConfiguration 类是一个代理类,所以在调用 AppleAutoConfiguration 类中调用 orangeFactoryBean() 方法创建 OrangeFactoryBean 实例时会又会触发切面逻辑,又会走上面的逻辑,但是注意注意注意注意此时的 beanName 是不带 & 符号的哦,此时的 beanName 就是方法名称,所以此时就会进入 else 逻辑进入 enhanceFactoryBean() 方法中,源码如下:

从源码中可以看到又是通过 cglib 创建了一个 OrangeFactoryBean 的代理对象,注意这里的拦截器逻辑,只有当你调用了 OrangeFactoryBean 类中的 getObject() 方法才会做特殊增强,去调用 getBean() 逻辑,同时 注意 beanName 是不带 & 符号的,也就是去创建 Orange 类实例,除了 getObject() 方法之外的所有方法不做任何处理,直接进回调即可。至此 OrangeFactoryBean 类实例已经创建好了,在 Spring 容器中是一个代理类。

然后再看到我们自己的代码如下:

@Configuration
public class AppleAutoConfiguration {

	@Bean
	public AppleFactory appleFactory() throws Exception {
		OrangeFactoryBean orangeFactoryBean = orangeFactoryBean();
		System.out.println("appleFactory orangeFactoryBean = " + orangeFactoryBean);
		Orange orange = orangeFactoryBean.getObject();
		System.out.println("appleFactory orange="+orange);

		return appleFactory;
	}

	@Bean
	public OrangeFactoryBean orangeFactoryBean() {
		return new OrangeFactoryBean();
	}
}

刚才已执行到第一行代码,执行完后,获取到一个 OrangeFactoryBean 代理对象,然后开始执行第二行代码,注意这里隐式调用了 toString() 方法,会触发 OrangeFactoryBean 代理类的切面逻辑,而 OrangeFactoryBean 代理类只对 getObject() 方法有特殊处理,其他的方法都不做处理,就是直接回调而已,所以 toString() 的切面逻辑不用太在乎。

接下来执行第三行代码,调用 getObject() 方法,getObject() 方法是 OrangeFactoryBean 代理类非常关心的方法,在源码中写死要对 getObject() 方法进行处理。最终会调用到 getBean(orange) 流程,实例化 Orange bean,最终将 Orange 实例放入到 Spring 一级缓存。

所以最终在 appleFactory() 方法中执行 Orange orange = orangeFactoryBean.getObject() 代码和在测试类中执行 getBean(Orange.class) 或者 getBean(“orangeFactoryBean”) 代码都是从同一个地方获取到的值(Spring 中的一级缓存中),虽然多个地方调用,表面上给人的感觉是调用了多次,会出现多个实例,但是 Spring 中用代理的方式从底层帮我们解决了这个问题。但是前提是要使用 @Configuration 注解才会生效。

总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • Java常用字符串方法小结

    Java常用字符串方法小结

    字符串变量是Java与C语言的一大不同之处。Java之中的 String 类和 Stringbuffer 类提供了大量的对字符串操作的方法。String 类适合处理较小的字符串,而Stringbuffer类适合处理大量字符串
    2017-04-04
  • Java经典排序算法之插入排序代码实例

    Java经典排序算法之插入排序代码实例

    这篇文章主要介绍了Java经典排序算法之插入排序代码实例,插入排序是一种最简单直观的排序算法,它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,需要的朋友可以参考下
    2023-10-10
  • Spring:bean注入--Set方法注入

    Spring:bean注入--Set方法注入

    这篇文章主要给大家总结介绍了关于Spring注入Bean的一些方式,文中通过示例代码介绍的非常详细,对大家学习或者使用Spring具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2021-07-07
  • maven插件assembly使用及springboot启动脚本start.sh和停止脚本 stop.sh

    maven插件assembly使用及springboot启动脚本start.sh和停止脚本 stop.sh

    这篇文章主要介绍了maven插件assembly使用及springboot启动脚本start.sh和停止脚本 stop.sh的相关资料,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-08-08
  • SpringBoot中@Autowired生效方式详解

    SpringBoot中@Autowired生效方式详解

    @Autowired注解可以用在类属性,构造函数,setter方法和函数参数上,该注解可以准确地控制bean在何处如何自动装配的过程。在默认情况下,该注解是类型驱动的注入
    2022-06-06
  • Java基础元注解基本原理示例详解

    Java基础元注解基本原理示例详解

    这篇文章主要为大家介绍了Java基础元注解基本原理示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-01-01
  • 如何基于java向mysql数据库中存取图片

    如何基于java向mysql数据库中存取图片

    这篇文章主要介绍了如何基于java向mysql数据库中存取图片,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-02-02
  • IDEA实现 springmvc的简单注册登录功能的示例代码

    IDEA实现 springmvc的简单注册登录功能的示例代码

    这篇文章主要介绍了IDEA实现 springmvc的简单注册登录功能,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-06-06
  • Spring ApplicationListener监听器用法详解

    Spring ApplicationListener监听器用法详解

    这篇文章主要介绍了Spring ApplicationListener监听器用法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2019-11-11
  • Java核心教程之常见时间日期的处理方法

    Java核心教程之常见时间日期的处理方法

    这篇文章主要给大家介绍了关于Java核心教程之常见时间日期的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-02-02

最新评论