一道經(jīng)典面試題:@Configuration 和 @Component 有何區(qū)別?
關(guān)于 @Configuration 注解有一個特別經(jīng)典的面試題:
- @Configuration 和 @Component 有什么區(qū)別?
無論小伙伴們之前是否背過相關(guān)的面試題,今天這篇文章學(xué)完之后相信大家對這個問題都會有更深一層的理解,廢話不多少,咱們開始分析。
1. 情景展現(xiàn)
@Configuration 和 @Component 到底有何區(qū)別呢?我先通過如下一個案例,在不分析源碼的情況下,小伙伴們先來直觀感受一下這兩個之間的區(qū)別。
@Configuration
public class JavaConfig01 {
}
@Component
public class JavaConfig02 {
}
首先,分別向 Spring 容器中注入兩個 Bean,JavaConfig01 和 JavaConfig02,其中,JavaConfig01 上添加的是 @Configuration 注解而 JavaConfig02 上添加的則是 @Component 注解。
現(xiàn)在,在 XML 文件中配置包掃描:
<context:component-scan
base-package="org.javaboy.demo.p6"/>
最后,加載 XML 配置文件,初始化容器:
public class Demo {
public static void main(String[] args) {
ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("beans_demo.xml");
JavaConfig01 config01 = ctx.getBean(JavaConfig01.class);
JavaConfig02 config02 = ctx.getBean(JavaConfig02.class);
System.out.println("config01.getClass() = " + config01.getClass());
System.out.println("config02.getClass() = " + config02.getClass());
}
}
最終打印出來結(jié)果如下:
從上面這段代碼中,我們可以得出來兩個結(jié)論:
- @Configuration 注解也是 Spring 組件注解的一種,通過普通的 Bean 掃描也可以掃描到 @Configuration。
- @Configuration 注解注冊到 Spring 中的 Bean 是一個 CGLIB 代理的 Bean,而不是原始 Bean,這一點和 @Component 不一樣,@Component 注冊到 Spring 容器中的還是原始 Bean。
一個問題來了,@Configuration 標記的類為什么注冊到 Spring 容器之后就變成了代理對象了呢?閉著眼睛大家也能猜到,肯定是為了通過代理來增強其功能,那么究竟增強什么功能呢?接下來我們通過源碼分析來和小伙伴們梳理一下這里的條條框框。
2. 源碼分析
要理解這個問題,首先得結(jié)合我們前面的文章@Configuration 注解的 Full 模式和 Lite 模式!,在該文中,松哥提到了 @Configuration 模式分為了 Full 模式和 Lite 模式,所以,對于 @Configuration 注解的處理,在加載的時候,就需要首先區(qū)分出來是 Full 模式還是 Lite 模式。
負責(zé) @Configuration 注解的是 ConfigurationClassPostProcessor,這個處理器是一個 BeanFactoryPostProcessor,BeanFactoryPostProcessor 的作用就是在 Bean 定義的時候,通過修改 BeanDefinition 來重新定義 Bean 的行為,這個松哥之前有過專門的文章介紹,不熟悉的小伙伴可以先看看這里:
同時,ConfigurationClassPostProcessor 也是 BeanDefinitionRegistryPostProcessor 的實例,BeanDefinitionRegistryPostProcessor 是干嘛的呢?
BeanDefinitionRegistryPostProcessor 是 Spring 框架中的一個接口,它的作用是在應(yīng)用程序上下文啟動時,對 BeanDefinitionRegistry 進行后置處理。具體來說,BeanDefinitionRegistryPostProcessor 可以用于修改或擴展應(yīng)用程序上下文中的 BeanDefinition,即在 Bean 實例化之前對 BeanDefinition 進行修改。它可以添加、刪除或修改 BeanDefinition 的屬性,甚至可以動態(tài)地注冊新的 BeanDefinition。通過實現(xiàn) BeanDefinitionRegistryPostProcessor 接口,我們可以在 Spring 容器啟動過程中干預(yù) Bean 的定義,以滿足特定的需求。這使得我們可以在應(yīng)用程序上下文加載之前對 Bean 進行一些自定義的操作,例如動態(tài)注冊 Bean 或者修改 Bean 的屬性。需要注意的是,BeanDefinitionRegistryPostProcessor 在 BeanFactoryPostProcessor 之前被調(diào)用,因此它可以影響到 BeanFactoryPostProcessor 的行為。
BeanFactoryPostProcessor 中的方法是 postProcessBeanFactory,而 BeanDefinitionRegistryPostProcessor 中的方法是 postProcessBeanDefinitionRegistry,根據(jù)前面的介紹,postProcessBeanDefinitionRegistry 方法將在 postProcessBeanFactory 方法之前執(zhí)行。
所以,我們就從 postProcessBeanDefinitionRegistry 方法開始看起吧~
2.1 postProcessBeanDefinitionRegistry
@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);
}
這個方面前面的代碼主要是為了確保該方法執(zhí)行一次,我們就不多說了。關(guān)鍵在于最后的 processConfigBeanDefinitions 方法,這個方法就是用來決策配置類是 Full 模式還是 Lite 模式的。
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
List<BeanDefinitionHolder> configCandidates = new ArrayList<>();
String[] candidateNames = registry.getBeanDefinitionNames();
for (String beanName : candidateNames) {
BeanDefinition beanDef = registry.getBeanDefinition(beanName);
if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
if (logger.isDebugEnabled()) {
logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
}
}
else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}
}
//省略。。。
}
我省略了其他代碼,大家看,這個方法中,會首先根據(jù) beanName 取出來 BeanDefinition,然后判斷 BeanDefinition 中是否包含 ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE 屬性,這個屬性上記錄了當前配置類是 Full 模式還是 Lite 模式,不同模式將來的處理方案肯定也是不同的。如果是第一次處理,顯然 BeanDefinition 中并不包含該屬性,因此就會進入到 ConfigurationClassUtils.checkConfigurationClassCandidate 方法中,正是在該方法中,判斷當前配置類是 Full 模式還是 Lite 模式,并進行標記,checkConfigurationClassCandidate 方法的邏輯也挺長的,我這里挑出來跟我們感興趣的部分:
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;
}
//省略
}
Full 模式情況很簡單,就是如果配置類上存在 @Configuration 注解,并且該注解的 proxyBeanMethods 屬性值不為 false,那么就是 Full 模式,這個跟松哥在 @Configuration 注解的 Full 模式和 Lite 模式!一文中的介紹是一致的。
Lite 模式就情況多一些,首先 config!=null 就是說現(xiàn)在也存在 @Configuration 注解,但是 proxyBeanMethods 屬性值此時為 false,那么就是 Lite 模式(proxyBeanMethods 屬性值為 true 的話就進入到 if 分支中了)。
另外就是在 isConfigurationCandidate 方法中有一些判斷邏輯去鎖定是否為 Lite 模式:
static boolean isConfigurationCandidate(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...
return hasBeanMethods(metadata);
}
這個方法的判斷邏輯是這樣:
- 首先注解要是標記的是接口,那就不能算是 Lite 模式。
- 遍歷 candidateIndicators,判斷當前類上是否包含這個 Set 集合中的注解,這個 Set 集合中的注解有四個,分別是 @Component、@ComponentScan、@Import、@ImportResource 四個,也就是,如果類上標記的是這四個注解的話,那么也按照 Lite 模式處理。
- 判斷當前類中是否有 @Bean 標記的方法,如果有則按照 Lite 模式處理,否則就不是 Lite 模式。
如果小伙伴們看過松哥之前的 @Configuration 注解的 Full 模式和 Lite 模式!一文,那么上面這些代碼應(yīng)該都很好理解,跟松哥在該文章中的介紹都是一致的。
好了,經(jīng)過上面的處理,現(xiàn)在就已經(jīng)標 BeanDefinition 中標記了這個配置類到底是 Full 模式還是 Lite 模式了。
2.2 postProcessBeanFactory
接下來我們就來看 postProcessBeanFactory 方法。
/**
* Prepare the Configuration classes for servicing bean requests at runtime
* by replacing them with CGLIB-enhanced subclasses.
*/
@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));
}
首先大家看一下這個方法的注釋,注釋說的很明確了,將 Configuration 類通過 CGLIB 進行增強,以便在運行時較好的處理 Bean 請求。
這個方法中還會再次確認一下 postProcessBeanDefinitionRegistry 方法已經(jīng)處理過了,如果沒有處理的話,則會在該方法中調(diào)用 processConfigBeanDefinitions 去確認 Bean 使用的是哪種模式。
該方法的關(guān)鍵在于 enhanceConfigurationClasses,這個就是用來通過動態(tài)代理增強配置類的,當然這個方法也是比較長的,我這里列出來一些關(guān)鍵的邏輯:
public void enhanceConfigurationClasses(ConfigurableListableBeanFactory beanFactory) {
StartupStep enhanceConfigClasses = this.applicationStartup.start("spring.context.config-classes.enhance");
Map<String, AbstractBeanDefinition> configBeanDefs = new LinkedHashMap<>();
for (String beanName : beanFactory.getBeanDefinitionNames()) {
BeanDefinition beanDef = beanFactory.getBeanDefinition(beanName);
Object configClassAttr = beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE);
if ((configClassAttr != null || methodMetadata != null) &&
(beanDef instanceof AbstractBeanDefinition abd) && !abd.hasBeanClass()) {
// Configuration class (full or lite) or a configuration-derived @Bean method
// -> eagerly resolve bean class at this point, unless it's a 'lite' configuration
// or component class without @Bean methods.
boolean liteConfigurationCandidateWithoutBeanMethods =
(ConfigurationClassUtils.CONFIGURATION_CLASS_LITE.equals(configClassAttr) &&
annotationMetadata != null && !ConfigurationClassUtils.hasBeanMethods(annotationMetadata));
if (!liteConfigurationCandidateWithoutBeanMethods) {
try {
abd.resolveBeanClass(this.beanClassLoader);
}
}
}
if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) {
configBeanDefs.put(beanName, abd);
}
}
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);
// Set enhanced subclass of the user-specified bean class
Class<?> configClass = beanDef.getBeanClass();
Class<?> enhancedClass = enhancer.enhance(configClass, this.beanClassLoader);
if (configClass != enhancedClass) {
beanDef.setBeanClass(enhancedClass);
}
}
enhanceConfigClasses.tag("classCount", () -> String.valueOf(configBeanDefs.keySet().size())).end();
}
這個方法的邏輯,我整體上將之分為兩部分:
第一部分就是先找到 Full 模式的配置類的名稱,存入到 configBeanDefs 集合中。
具體尋找的邏輯就是根據(jù)配置類的模式去尋找,如果配置類是 Full 模式,就將之存入到 configBeanDefs 中。如果配置類是 Lite 模式,且里邊沒有 @Bean 標記的方法,那就說明這可能并不是一個配置類,就是一個普通 Bean,那么就在這里加載類就行了。
第二步則是遍歷 configBeanDefs 集合,增強配置類。
這個如果大家了解 CGLIB 動態(tài)代理的話,這個就很好懂了,關(guān)于 CGLIB 動態(tài)代理松哥這里不啰嗦,最近更新的 Spring 源碼視頻中都有詳細講到。那么這里主要是通過 enhancer.enhance 方法來生成代理類的,如下:
public Class<?> enhance(Class<?> configClass, @Nullable ClassLoader classLoader) {
if (EnhancedConfiguration.class.isAssignableFrom(configClass)) {
return configClass;
}
Class<?> enhancedClass = createClass(newEnhancer(configClass, classLoader));
return enhancedClass;
}
private Enhancer newEnhancer(Class<?> configSuperClass, @Nullable ClassLoader classLoader) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(configSuperClass);
enhancer.setInterfaces(new Class<?>[] {EnhancedConfiguration.class});
enhancer.setUseFactory(false);
enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE);
enhancer.setAttemptLoad(true);
enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader));
enhancer.setCallbackFilter(CALLBACK_FILTER);
enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes());
return enhancer;
}
小伙伴們看到,增強類中的 setCallbackFilter 是 CALLBACK_FILTER,這個里邊包含了幾個方法攔截器,跟我們相關(guān)的是 BeanMethodInterceptor,我們來看下:
private static class BeanMethodInterceptor implements MethodInterceptor, ConditionalCallback {
@Override
@Nullable
public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs,
MethodProxy cglibMethodProxy) throws Throwable {
ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance);
String beanName = BeanAnnotationHelper.determineBeanNameFor(beanMethod);
// Determine whether this bean is a scoped-proxy
if (BeanAnnotationHelper.isScopedProxy(beanMethod)) {
String scopedBeanName = ScopedProxyCreator.getTargetBeanName(beanName);
if (beanFactory.isCurrentlyInCreation(scopedBeanName)) {
beanName = scopedBeanName;
}
}
// To handle the case of an inter-bean method reference, we must explicitly check the
// container for already cached instances.
// First, check to see if the requested bean is a FactoryBean. If so, create a subclass
// proxy that intercepts calls to getObject() and returns any cached bean instance.
// This ensures that the semantics of calling a FactoryBean from within @Bean methods
// is the same as that of referring to a FactoryBean within XML. See SPR-6602.
if (factoryContainsBean(beanFactory, BeanFactory.FACTORY_BEAN_PREFIX + beanName) &&
factoryContainsBean(beanFactory, beanName)) {
Object factoryBean = beanFactory.getBean(BeanFactory.FACTORY_BEAN_PREFIX + beanName);
if (factoryBean instanceof ScopedProxyFactoryBean) {
// Scoped proxy factory beans are a special case and should not be further proxied
}
else {
// It is a candidate FactoryBean - go ahead with enhancement
return enhanceFactoryBean(factoryBean, beanMethod.getReturnType(), beanFactory, beanName);
}
}
if (isCurrentlyInvokedFactoryMethod(beanMethod)) {
return cglibMethodProxy.invokeSuper(enhancedConfigInstance, beanMethodArgs);
}
return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName);
}
}
自己寫過 CGLIB 動態(tài)代理的小伙伴都知道這里 intercept 方法的含義,這就是真正的攔截方法了,也就是說,如果我們的配置類是 Full 模式的話,那么將來調(diào)用 @Bean 注解標記的方法的時候,調(diào)用的其實是這里的 intercept 方法。
上面方法,首先會判斷當前代理是否為作用域代理,我們這里當然不是。
接下來判斷請求的 Bean 是否是一個 FactoryBean,如果是,則需要去代理其 getObject 方法,當執(zhí)行到 getObject 方法的時候,就去 Spring 容器中查找需要的 Bean,當然,我們這里也不屬于這種情況。
接下來判斷當前正在執(zhí)行的方法,是否為容器中正在調(diào)用的工廠方法。
例如我有如下代碼:
@Configuration
public class JavaConfig {
@Bean
User user() {
User user = new User();
user.setDog(dog());
return user;
}
@Bean
Dog dog() {
return new Dog();
}
}
那么如果是直接調(diào)用 dog() 方法,則 isCurrentlyInvokedFactoryMethod 返回 true,如果是在 user() 方法中調(diào)用的 dog() 方法,則 isCurrentlyInvokedFactoryMethod 返回 false。
當 isCurrentlyInvokedFactoryMethod 返回 true 的時候,就執(zhí)行 invokeSuper 方法,也就是真正的觸發(fā) dog() 方法的執(zhí)行。
當 isCurrentlyInvokedFactoryMethod 返回 false 的時候,則執(zhí)行下面的 resolveBeanReference 方法,這個方法會先去 Spring 容器中查找相應(yīng)的 Bean,如果 Spring 容器中不存在該 Bean,則會觸發(fā) Bean 的創(chuàng)建流程。
現(xiàn)在,小伙伴們應(yīng)該明白了為什么 Full 模式下,調(diào)用 @Bean 注解標記的方法并不會導(dǎo)致 Bean 的重復(fù)創(chuàng)建了吧~
好啦,本文結(jié)合上文 @Configuration 注解的 Full 模式和 Lite 模式! 一起食用效果更佳哦~
推薦閱讀:
