乐趣区

关于spring:一道经典面试题Configuration-和-Component-有何区别

对于 @Configuration 注解有一个特地经典的面试题:

  • @Configuration 和 @Component 有什么区别?

无论小伙伴们之前是否背过相干的面试题,明天这篇文章学完之后置信大家对这个问题都会有更深一层的了解,废话不多少,咱们开始剖析。

1. 情景展示

@Configuration 和 @Component 到底有何区别呢?我先通过如下一个案例,在不剖析源码的状况下,小伙伴们先来直观感受一下这两个之间的区别。

@Configuration
public class JavaConfig01 {
}
@Component
public class JavaConfig02 {}

首先,别离向 Spring 容器中注入两个 Bean,JavaConfig01 和 JavaConfig02,其中,JavaConfig01 上增加的是 @Configuration 注解而 JavaConfig02 上增加的则是 @Component 注解。

当初,在 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());
    }
}

最终打印进去后果如下:

从下面这段代码中,咱们能够得进去两个论断:

  1. @Configuration 注解也是 Spring 组件注解的一种,通过一般的 Bean 扫描也能够扫描到 @Configuration。
  2. @Configuration 注解注册到 Spring 中的 Bean 是一个 CGLIB 代理的 Bean,而不是原始 Bean,这一点和 @Component 不一样,@Component 注册到 Spring 容器中的还是原始 Bean。

一个问题来了,@Configuration 标记的类为什么注册到 Spring 容器之后就变成了代理对象了呢?闭着眼睛大家也能猜到,必定是为了通过代理来加强其性能,那么到底加强什么性能呢?接下来咱们通过源码剖析来和小伙伴们梳理一下这里的条条框框。

2. 源码剖析

要了解这个问题,首先得联合咱们后面的文章 @Configuration 注解的 Full 模式和 Lite 模式!,在该文中,松哥提到了 @Configuration 模式分为了 Full 模式和 Lite 模式,所以,对于 @Configuration 注解的解决,在加载的时候,就须要首先辨别进去是 Full 模式还是 Lite 模式。

负责 @Configuration 注解的是 ConfigurationClassPostProcessor,这个处理器是一个 BeanFactoryPostProcessor,BeanFactoryPostProcessor 的作用就是在 Bean 定义的时候,通过批改 BeanDefinition 来从新定义 Bean 的行为,这个松哥之前有过专门的文章介绍,不相熟的小伙伴能够先看看这里:

  • Spring 中 BeanFactory 和 FactoryBean 有何区别?

同时,ConfigurationClassPostProcessor 也是 BeanDefinitionRegistryPostProcessor 的实例,BeanDefinitionRegistryPostProcessor 是干嘛的呢?

BeanDefinitionRegistryPostProcessor 是 Spring 框架中的一个接口,它的作用是在应用程序上下文启动时,对 BeanDefinitionRegistry 进行后置解决。具体来说,BeanDefinitionRegistryPostProcessor 能够用于批改或扩大应用程序上下文中的 BeanDefinition,即在 Bean 实例化之前对 BeanDefinition 进行批改。它能够增加、删除或批改 BeanDefinition 的属性,甚至能够动静地注册新的 BeanDefinition。通过实现 BeanDefinitionRegistryPostProcessor 接口,咱们能够在 Spring 容器启动过程中干涉 Bean 的定义,以满足特定的需要。这使得咱们能够在应用程序上下文加载之前对 Bean 进行一些自定义的操作,例如动静注册 Bean 或者批改 Bean 的属性。 须要留神的是,BeanDefinitionRegistryPostProcessor 在 BeanFactoryPostProcessor 之前被调用,因而它能够影响到 BeanFactoryPostProcessor 的行为。

BeanFactoryPostProcessor 中的办法是 postProcessBeanFactory,而 BeanDefinitionRegistryPostProcessor 中的办法是 postProcessBeanDefinitionRegistry,依据后面的介绍,postProcessBeanDefinitionRegistry 办法将在 postProcessBeanFactory 办法之前执行。

所以,咱们就从 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);
}

这个方面后面的代码次要是为了确保该办法执行一次,咱们就不多说了。关键在于最初的 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));
        }
    }
    // 省略。。。}

我省略了其余代码,大家看,这个办法中,会首先依据 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 就是说当初也存在 @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);
}

这个办法的判断逻辑是这样:

  1. 首先注解要是标记的是接口,那就不能算是 Lite 模式。
  2. 遍历 candidateIndicators,判断以后类上是否蕴含这个 Set 汇合中的注解,这个 Set 汇合中的注解有四个,别离是 @Component、@ComponentScan、@Import、@ImportResource 四个,也就是,如果类上标记的是这四个注解的话,那么也依照 Lite 模式解决。
  3. 判断以后类中是否有 @Bean 标记的办法,如果有则依照 Lite 模式解决,否则就不是 Lite 模式。

如果小伙伴们看过松哥之前的 @Configuration 注解的 Full 模式和 Lite 模式!一文,那么下面这些代码应该都很好了解,跟松哥在该文章中的介绍都是统一的。

好了,通过下面的解决,当初就曾经标 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 办法曾经解决过了,如果没有解决的话,则会在该办法中调用 processConfigBeanDefinitions 去确认 Bean 应用的是哪种模式。

该办法的关键在于 enhanceConfigurationClasses,这个就是用来通过动静代理加强配置类的,当然这个办法也是比拟长的,我这里列出来一些要害的逻辑:

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 汇合中。

具体寻找的逻辑就是依据配置类的模式去寻找,如果配置类是 Full 模式,就将之存入到 configBeanDefs 中。如果配置类是 Lite 模式,且里边没有 @Bean 标记的办法,那就阐明这可能并不是一个配置类,就是一个一般 Bean,那么就在这里加载类就行了。

第二步则是遍历 configBeanDefs 汇合,加强配置类。

这个如果大家理解 CGLIB 动静代理的话,这个就很好懂了,对于 CGLIB 动静代理松哥这里不啰嗦,最近更新的 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,这个里边蕴含了几个办法拦截器,跟咱们相干的是 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 动静代理的小伙伴都晓得这里 intercept 办法的含意,这就是真正的拦挡办法了,也就是说,如果咱们的配置类是 Full 模式的话,那么未来调用 @Bean 注解标记的办法的时候,调用的其实是这里的 intercept 办法。

下面办法,首先会判断以后代理是否为作用域代理,咱们这里当然不是。

接下来判断申请的 Bean 是否是一个 FactoryBean,如果是,则须要去代理其 getObject 办法,当执行到 getObject 办法的时候,就去 Spring 容器中查找须要的 Bean,当然,咱们这里也不属于这种状况。

接下来判断以后正在执行的办法,是否为容器中正在调用的工厂办法。

例如我有如下代码:

@Configuration
public class JavaConfig {

    @Bean
    User user() {User user = new User();
        user.setDog(dog());
        return user;
    }

    @Bean
    Dog dog() {return new Dog();
    }
}

那么如果是间接调用 dog() 办法,则 isCurrentlyInvokedFactoryMethod 返回 true,如果是在 user() 办法中调用的 dog() 办法,则 isCurrentlyInvokedFactoryMethod 返回 false。

当 isCurrentlyInvokedFactoryMethod 返回 true 的时候,就执行 invokeSuper 办法,也就是真正的触发 dog() 办法的执行。

当 isCurrentlyInvokedFactoryMethod 返回 false 的时候,则执行上面的 resolveBeanReference 办法,这个办法会先去 Spring 容器中查找相应的 Bean,如果 Spring 容器中不存在该 Bean,则会触发 Bean 的创立流程。

当初,小伙伴们应该明确了为什么 Full 模式下,调用 @Bean 注解标记的办法并不会导致 Bean 的反复创立了吧~

好啦,本文联合上文 @Configuration 注解的 Full 模式和 Lite 模式!一起食用成果更佳哦~

退出移动版