本文是Spring IoC容器技术介绍系列文章之一。本文介绍@Configuration
的使用以及原理。
1 概念介绍
Annotating a class with @Configuration indicates that its primary purpose is as a source of bean definitions.
上述是Spring官方文档中用于介绍@Configuration
的一段话,大意是说在类上使用@Configuration
标注则表明该类的主要目的是作为bean定义的源文件。
在Spring应用中,我们一般会定义一个使用@Configuration
标注的类,然后让ApplicationContext
加载它,来启动整个容器。比如下面这样:
@Configuration
public class Application {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
System.out.println(applicationContext);
}
}
在这段示例代码中,使用了@Configuration
注解标注了Application
类。在程序入口的main
方法中,通过AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class)
将Application.class
作为构造函数的入参传递给了新创建的applicationContext
对象。这样,就完成了一个IoC容器的创建了。
然而,上面创建的IoC容器只是加载了Spring相关的内部bean。由于我们没有定义任何的@Bean
对象,所以难以看出IoC容器加载完成后的效果。
@Bean
也是Spring中定义的注解,一般会配合@Configuration
一起用,标注在@Configuration
类中的某个方法上。被@Bean
标注的方法会在IoC容器初始化时被调用,并将方法的返回结果作为一个bean对象保存在IoC容器里。
我们稍微调整下代码便于测试IoC容器的功能:
@Configuration
public class Application {
@Bean
public NumberStyleFormatter numberStyleFormatter() {
return new NumberStyleFormatter();
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
NumberStyleFormatter bean = applicationContext.getBean(NumberStyleFormatter.class);
System.out.println(bean);
}
}
在这段代码中,我们增加了一个@Bean
方法。同时,在main
方法中使用getBean(...)
获取该对象并打印。控制台上显示的结果如下图:
IoC容器的简单测试
1.1 换成@Component试试
从@Configuration
注解的源码可以发现,@Configuration
注解继承自@Component
。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {
String value() default "";
boolean proxyBeanMethods() default true;
}
那如果我直接使用@Component
,而不是@Configuration
呢,IoC容器还能正常启动吗?
事实证明是可以的,我们可以将上述测试代码中的@Configuration
改成@Component
,结果会发现依然可以从IoC容器中读取NumberStyleFormatter
类型的bean对象。
那@Configuration
和@Component
有何区别呢?
1.2 proxyBeanMethods
最开始的那段官方描述大致是说:@Configuration
表示这个类就是用来配置的,就是用来帮助IoC容器初始化的,所以大家要尽可能使用@Configuration
标注初始化的类,便于理解。这算是一个区别。
另外还有一个很大的区别是官方文档中给出的这句话:
@Configuration classes let inter-bean dependencies be defined by calling other @Bean methods in the same class.
这说的是啥!别着急,我们先写一段代码来看一个现象:
@Configuration
public class Application {
@Bean Parent parent() {
return new Parent();
}
@Bean Child child() {
return new Child(parent());
}
static class Parent {}
static class Child {
public Parent parent = null;
public Child(Parent parent) {
this.parent = parent;
}
}
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(Application.class);
Parent parent = applicationContext.getBean(Parent.class);
Child child = applicationContext.getBean(Child.class);
System.out.println(child.parent == parent);
}
}
上面这段代码中,我们定义了两个内部类。一个叫Parent
,一个叫Child
。Child
的构造方法需要传入一个Parent
对象。
同时我们定义了两个@Bean
方法,一个创建了类型为Parent
的bean,另一个创建了一个类型为Child
的bean。
在@Bean Child child()
方法里,我们调用了parent()
方法创建了一个Parent
对象作为构造器入参传入给新创建的Child
对象。
我们没有使用@Bean Child child(Parent parent)
这样的方法创建Child
类型的bean(即没有使用依赖注入的方式将Parent
类型的bean对象注入到Child
中)。因此,在IoC容器中,Parent
类型的bean应该和Child
类型的bean的parent属性是不同的对象。main
方法在控制台上打印的结果应该是false,但实际输出却是true!
不合常理的相等
这是为什么?
这是由于@Configuration
中有个proxyBeanMethods()
属性,且默认设置为true了。
现在可以来解释上面官方文档中的那段话的意思了。上面那段话的意思是说@Configuration
会给@Bean
方法设置代理,这样@Configuration
中其他的方法在调用该方法时,实际会返回IoC容器中对应bean对象,而不是创建新的对象。
因此,测试程序返回的结果是true,而非false。如果我们将注解改为@Configuration(proxyBeanMethods = false)
,则控制台上返回的就会是false,表明这两个对象是不同的对象。
2 源码解释
ApplicationContext
初始化的核心方法是refresh()
方法。
ConfigurableApplicationContext.refresh()
该方法会处理已经加载的配置对象,这里的配置对象可以简单理解为我们编写的@Configuration
对象。
在AnnotationConfigApplicationContext
初始化的最后一步,便是调用refresh
方法。在ApplicationContext
中会保存一个beanFactory属性。beanFactory在IoC容器中非常重要,其内部有一个叫beanDefinitionMap的属性,这个属性用于保存IoC容器需要加载的bean对象的所有定义信息。
下面这张是在执行refresh()
方法之前,applicationContext.beanFactory.beanDefinitionMap中存的数据。可以发现,我们定义的@Configuration
就在里面,名称为application。
refresh方法之前的内存数据
程序继续往下执行,会走到一个ConfigurationClassPostProcessor
类的processConfigBeanDefinitions()
方法。这个ConfigurationClassPostProcessor
是框架负责完成初始化和调起的,不用过多关注。
在processConfigBeanDefinitions()
方法中,会调用parser.parse(candidates)
方法,这个时候传入进来的candidates也是我们的@Configuraiton
对象。
解析方法入口处的内存数据
解析完之后,会拿到一个configClasses,这个configClasses就是所有的配置类。在这里,它还是我们的@Configuration
对象。之后会调用this.reader.loadBeanDefinitions(configClasses)
方法,在这个方法中加载所有@Configuration
对象中定义的bean。
loadBeanDefinitions入口处的内存数据
在ConfigurationClassBeanDefinitionReader
的loadBeanDefinitionsForConfigurationClass(...)
方法中,将我们在@Configuration
对象中定义的BeanDefinition写入到applicationContext.beanFactory.beanDefinitionMap。程序执行到这里,我们便可以看到applicationContext中已经有了我们定义的两个bean对象的定义信息了。
BeanDefinition对象生成后的内存信息
在beanFactory中,有一个叫singletonObjects的属性,singletonObjects保存了IoC容器中的所有bean对象。
在完成BeanDefinition构建之后,程序会执行到AbstractApplicationContext
的finishBeanFactoryInitialization(...)
中,在其中调用beanFactory.preInstantiateSingletons()
将所有bean初始化并保存到singletonObjects。
实例化bean之前的内存信息
在IoC进行Bean对象初始化时,最终会调用@Bean Child child()
方法。在调用该方法时,可以发现实际使用的对象是通过CGLIB代理的类,进而改写了默认的特性,将IoC容器中已经存在的Parent
对象注入到Child
类型的bean中。
初始化Child时的堆栈
那Spring是什么时候使用代理类替换了我们的@Configuration
呢?
首先是在ConfigurationClassPostProcessor
的方法processConfigBeanDefinitions(...)
中调用了ConfigurationClassUtils.checkConfigurationClassCandidate(...)
。在该方法中,判断proxyBeanMethods
的值为true,则在BeanDefinition中增加一个属性:
设置属性
然后在ConfigurationClassPostProcessor
的enhanceConfigurationClasses(...)
方法中根据该属性筛选出BeanDefinition对象,将所有对象存在一个叫configBeanDefs的映射表中:
找到BeanDefinition
最后将BeanClass改成CGLIB代理类的类型,这样就改变了IoC容器中最终生成的@Configuration
对象的类型了。
修改类型
3 总结
本文主要介绍Spring IoC容器中@Configuration
的使用方法,并深入分析了其底层的实现机理。
源码解析部分,主要分析了IoC容器启动过程中和@Configuration
相关的核心部分的逻辑和代码。