5

一道经典面试题:@Configuration 和 @Component 有何区别?

 9 months ago
source link: http://www.javaboy.org/2023/0829/spring-configuration-vs-component.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
3 个月前 22 分钟 读完 (大约 3362 个字)

一道经典面试题:@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 的行为,这个松哥之前有过专门的文章介绍,不熟悉的小伙伴可以先看看这里:

同时,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 模式! 一起食用效果更佳哦~

# Spring

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

javaboy.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK