计算机系统应用教程网站

网站首页 > 技术文章 正文

java面试必备:你知道spring往容器中注册Bean有哪几种方式吗?

btikc 2024-09-16 13:04:23 技术文章 23 ℃ 0 评论

现在求职面试,特别是技术面试,面试官就喜欢问些底层问题,所谓不但要“知其然”,还要“知其所以然”。

今天的题目“spring往容器中注册Bean有哪几种方式”就是经常会被问到的,这里作者总结了下几点,并会把原理讲解,不喜勿喷!

第一种:ComponentScans + @Component /@Repository/@Controller@Service

这是一套组合拳,@ComponentScan(basePackages = {"com.abc"}),然后需要注册为Bean的类上要标注@Component /@Repository/@Controller@Service等注解

例如:

@ComponentScan(basePackages = {"com.abc"})
@Configuration
public class Config{
    
}
@Controller
public class TestController {
    
}


那么问题来了?

spring是如何将这种情况下的TestController 作为Bean注入到容器中的呢?

这要从源码说起,在类ConfigurationClassParser,有个方法是用来处理注册Bean的,就是doProcessConfigurationClass,在这个方法里,就是处理了各种注册Bean的解析。

首先来看解析ComponentScans的:

// Process any @ComponentScan annotations
Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
      sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
if (!componentScans.isEmpty() &&
      !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
   for (AnnotationAttributes componentScan : componentScans) {
      // The config class is annotated with @ComponentScan -> perform the scan immediately
      Set<BeanDefinitionHolder> scannedBeanDefinitions =
            this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
      // Check the set of scanned definitions for any further config classes and parse recursively if needed
      for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
         BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
         if (bdCand == null) {
            bdCand = holder.getBeanDefinition();
         }
         if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
            parse(bdCand.getBeanClassName(), holder.getBeanName());
         }
      }
   }
}


从这代码中,可以看出解析所有与标注了componentScans的类,封装成一个扫描注解对象属性:AnnotationAttributes ,然后循环调用 componentScanParser.parse,那么再看下componentScanParser.parse这个方法,这个方法调用了一个重要的方法是

scanner.doScan(StringUtils.toStringArray(basePackages))

注意:spring 代码有个特点,就是真正干活的,它一般都喜欢起名为 do###,所以,真正处理扫描的,就是这个方法了,我看具体看下:

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
   Assert.notEmpty(basePackages, "At least one base package must be specified");
   Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
   for (String basePackage : basePackages) {
      Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
      for (BeanDefinition candidate : candidates) {
         ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
         candidate.setScope(scopeMetadata.getScopeName());
         String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
         if (candidate instanceof AbstractBeanDefinition) {
            postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
         }
         if (candidate instanceof AnnotatedBeanDefinition) {
            AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
         }
         if (checkCandidate(beanName, candidate)) {
            BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
            definitionHolder =
                  AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
            beanDefinitions.add(definitionHolder);
            registerBeanDefinition(definitionHolder, this.registry);
         }
      }
   }
   return beanDefinitions;
}


这个方法里, Set<BeanDefinition> candidates = findCandidateComponents(basePackage); 从这个方法的参数,我们就能猜出,肯定是处理我们写的扫描路径下的内容,是用来找到所有的候选组件,进去这个方法就会发现,果然如此。

那么得到候选的组件后,调用了registerBeanDefinition(definitionHolder, this.registry); 加入了容器。


第二种:@Import注解

这种其实有几种形式,比如:@Import @ImportSelector 还有,实现ImportBeanDefinitionRegistrar接口等

例如:

@Configuration
@Import(User.class)
public class MainConfig {

    
}


那么问题来了?

spring是如何将这种情况下的User 作为Bean注入到容器中的呢?

其实处理import形式的源码是和处理上面那个componentScans的,是处在一个入口里的,都是ConfigurationClassParser里的doProcessConfigurationClass方法中,

// Process any @Import annotations
processImports(configClass, sourceClass, getImports(sourceClass), true);


首先是先调用getImports(sourceClass) 得到所有标注@Import的组件,我们截取这个方法的一部分重要代码:

for (SourceClass candidate : importCandidates) {
   if (candidate.isAssignable(ImportSelector.class)) {
      // Candidate class is an ImportSelector -> delegate to it to determine imports
      Class<?> candidateClass = candidate.loadClass();
      ImportSelector selector = BeanUtils.instantiateClass(candidateClass, ImportSelector.class);
      ParserStrategyUtils.invokeAwareMethods(
            selector, this.environment, this.resourceLoader, this.registry);
      if (this.deferredImportSelectors != null && selector instanceof DeferredImportSelector) {
         this.deferredImportSelectors.add(
               new DeferredImportSelectorHolder(configClass, (DeferredImportSelector) selector));
      }
      else {
         String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
         Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
         processImports(configClass, currentSourceClass, importSourceClasses, false);
      }
   }
   else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
      // Candidate class is an ImportBeanDefinitionRegistrar ->
      // delegate to it to register additional bean definitions
      Class<?> candidateClass = candidate.loadClass();
      ImportBeanDefinitionRegistrar registrar =
            BeanUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class);
      ParserStrategyUtils.invokeAwareMethods(
            registrar, this.environment, this.resourceLoader, this.registry);
      configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
   }
   else {
      // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
      // process it as an @Configuration class
      this.importStack.registerImport(
            currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
      processConfigurationClass(candidate.asConfigClass(configClass));
   }
}

从这循环中,可以看出,分别处理@Import @ImportSelector 还有,实现ImportBeanDefinitionRegistrar接口的情况,注意,这里并没有立马将其放入容器中,而是放到了一个缓存中,同时,以上三种情况,放缓存还不是一种缓存。

对于@import的,是放在了一个configurationClasses缓存中;对于@ ImportSelector 还有,实现ImportBeanDefinitionRegistrar接口的,是放在了当前主配置类Mainconfig.class的对应属性列表中。

那什么时候真正放入到容器中的呢?这里先卖个关子,等会再说。


第三种:@Bean

例如:

public class MainConfig {

    @Bean
    public ShanghaiUser shanghaiUser(){
        return  new ShanghaiUser();
    }
}

那么问题来了?

spring是如何将这种情况下的User 作为Bean注入到容器中的呢?

其实处理这种形式的源码也是ConfigurationClassParser里的doProcessConfigurationClass方法中,

// Process individual @Bean methods
Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
for (MethodMetadata methodMetadata : beanMethods) {
   configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
}


类似上面的ImportBeanDefinitionRegistrar 处理,也是暂时保存在主配置类Mainconfig.class的对应属性列表中。那什么时候真正放入到容器中的呢?

连同上面的@Import形式的,和这个@Bean形式的,都是在调用this.reader.loadBeanDefinitions(configClasses);时,加入到容器的,

private void loadBeanDefinitionsForConfigurationClass(
      ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {

   if (trackedConditionEvaluator.shouldSkip(configClass)) {
      String beanName = configClass.getBeanName();
      if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
         this.registry.removeBeanDefinition(beanName);
      }
      this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
      return;
   }

   if (configClass.isImported()) {
      registerBeanDefinitionForImportedConfigurationClass(configClass);
   }
   for (BeanMethod beanMethod : configClass.getBeanMethods()) {
      loadBeanDefinitionsForBeanMethod(beanMethod);
   }

   loadBeanDefinitionsFromImportedResources(configClass.getImportedResources());
   loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars());
}


从这个方法中,就可以看出处理了@Import形式的,@Bean形式的还有实现ImportBeanDefinitionRegistrar 接口的等。

所以,想了解原理,还是要看源码啊,看源码很枯燥,但是你要是带着好奇的心情,带着想了解“为啥是这样子的?”心情去看的话,会感觉还是挺有成就感的。

同时,看源码还有一点就是:不能可能都看懂的,能看懂就看,看不懂的就先过,回头真正用到,可以debug下再研究。

想了解更多,就关注我把,持续更新您最想知道的知识!

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表