SpringBoot自动装配

清明前夕,我发表了一篇与Spring Cloud有关的文章,原计划在这篇文章之后,就梳理Eureka注册中心的相关知识。然而在跟踪之后,我才发现上来就谈Eureka组件的实现原理是不现实的,因为我根本不清楚SpringBoot是如何集成Eureka组件的。虽然周围人一再强调集成与要梳理的组件没有任何关系,但是我总觉得:不解决这个问题的梳理就像是在半空中建房一样,无处落脚。因此在这篇文章中我想梳理一下SpringBoot自动装配的过程。

1 SpringBoot中的Import注解

记得在梳理《Spring AOP》及《Spring事务》的时候,同事有跟我提过这个注解。当时他明确指出Import注解的解析起点位于ConfigurationClassPostProcessor类的processConfigBeanDefinitions(BeanDefinitionRegistry)方法中,具体如下图所示:

顺着图中红色方框标识的方法继续向下,会进入到ConfigurationClassParser类中,该类中的parser()方法的源码如下所示:

这个方法中,我们主要关注红色方框标出的地方。然后继续顺着这个方法向下看,最后会来到ConfigurationClassParser类的doProcessConfigurationClass(ConfigurationClass, SourceClass)方法中,在这个方法中我们可以看到如下信息,具体如下图所示:

从红色方框可以看出,这个方法是真正调用各注解解析逻辑的地方,这个方法可以处理的注解有:@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean注意:这里我们主要关注@Import注解的解析过程

梳理到这里,先停一下。因为在梳理过程中我发现:如果不明白这个注解的作用,就算弄清楚了它的解析流程,也就是蜻蜓点水,毫无意义。那Spring框架的设计者为什么要提供这样一个注解呢?

在Spring中@Import注解的作用是用来导入额外的配置类或者组件类,以扩展当前上下文中的Bean定义集合。这意味着当我们在一个配置类上使用@Import注解时,Spring容器会在初始化过程中处理被导入的类,并依据类的不同特性执行不同的操作

  1. 导入配置类:如果@Import的参数是一个带有@Configuration注解的类,则Spring容器会像处理其他配置类一样处理这个类,包括扫描并实例化其中通过@Bean注解的方法所定义的Bean
  2. 导入普通类:从Spring 4.2开始,@Import不仅可以导入配置类,还可以导入普通的类。这意味着即使不是配置类,只要通过@Import引入,Spring也会尝试将该类作为Bean进行实例化和管理
  3. 实现ImportSelector接口:当@Import的参数是一个实现了ImportSelector接口的类时,Spring容器会实例化该类,并调用selectImports()方法。此方法返回一个包含类全路径名的字符串数组,Spring容器会按照返回的列表加载并实例化那些类
  4. 实现DeferredImportSelector接口:类似于ImportSelector,但DeferredImportSelector的selectImports()方法调用时机更晚,确保在所有常规的@Configuration类处理完毕后才进行。这对于那些依赖于其他Bean配置完成后才能确定导入哪些类的情况非常有用
  5. 实现ImportBeanDefinitionRegistrar接口:当@Import注解导入一个实现了ImportBeanDefinitionRegistrar接口的类时,Spring容器允许软件开发者直接通过编程方式向BeanDefinitionRegistry注册自定义的Bean定义,这提供了更底层的控制机制,可以在注册Bean时设置更多的属性或执行复杂的逻辑。

总之,@Import注解提供了一种灵活的方式来聚合和整合各个模块或组件的配置,使得Spring容器能够统一管理和初始化应用程序所需的所有Bean。个人理解,@Import注解的主要作用就是向Spring容器中注入Bean(不知这个说法是否准确,若不对,欢迎大家在评论区留言)。了解了@Import注解的作用,下面就来看看其使用案例吧:

1.1 导入普通类

在本小节中我们将使用Import注解向Spring容器中导入一个普通java类,下面就一起看看这种用法的实现过程。首先定义一个普通java类,源码如下:

public class A {
}

接着再定义一个配置类ConfigA,然后在该类中定义一个方法a(),该方法上有一个@Bean注解,源码如下所示:

import org.springframework.context.annotation.Bean;

public class ConfigA {
    @Bean
    public A a() {
        return new A();
    }
}

最后再定义一个配置类ConfigB,该类上有两个注解@Configuration和@Import,其中@Import注解中指定ConfigA.class为属性,具体源码如下所示:

@Configuration
@Import(ConfigA.class)
public class ConfigB {
}

最后编写一个测试类,用于验证这种写法能否正常向Spring容器中注入相关对象(ConfigA对象及A对象)。该测试类的源码如下所示:

@SpringBootApplication
public class EurekaServiceApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(EurekaServiceApplication.class, args);
        ConfigA ca = ctx.getBean(ConfigA.class);
        A a = ctx.getBean(A.class);
        System.out.println(ca.getClass().getSimpleName());
        System.out.println(a.getClass().getSimpleName());
    }

}

通过观察控制台输出,我们可以发现ConfigA及A可以正常注入到Spring容器中。

1.2 导入ImportSelector实现类

在本小节中我们将通过在@Import注解中指定ImportSelector接口的实现类的方式向Spring容器中注入一个Bean对象,下面就一起看看这种用法的实现过程。首先定义一个普通java类,源码如下:

public class Tiger {
}

然后再定义一个配置类ZooConfig,然后在该类中定义一个方法tiger (),该方法上有一个@Bean注解,源码如下所示:

import org.springframework.context.annotation.Bean;

public class ZooConfig {

    @Bean
    public Tiger tiger() {
        return new Tiger();
    }

}

接着再定义一个ZooImportSelector类,该类实现了ImportSelector接口,并实现了该接口中的selectImports()方法,该类的源码如下所示:

public class ZooImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"org.com.chinasoft.s.ZooConfig"};
    }

}

最后再定义一个配置类ZooConfigB,该类上有两个注解@Configuration和@Import,其中@Import注解中指定ZooImportSelector.class为属性,具体源码如下所示:

@Configuration
@Import(ZooImportSelector.class)
public class ZooConfigB {
}

最后编写一个测试类,用于验证这种写法能否正常向Spring容器中注入相关对象(ZooConfig对象及Tiger对象)。该测试类的源码如下所示:

@SpringBootApplication
public class EurekaServiceApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(EurekaServiceApplication.class, args);
        ZooConfig zooConfig = ctx.getBean(ZooConfig.class);
        Tiger tiger = ctx.getBean(Tiger.class);
        System.out.println(zooConfig.getClass().getSimpleName());
        System.out.println(tiger.getClass().getSimpleName());
    }

}

通过观察控制台输出,我们可以发现ZooConfig及Tiger可以正常注入到Spring容器中。

1.3 导入ImportBeanDefinitionRegistrar实现类

在本小节中我们将通过在@Import注解中指定ImportBeanDefinitionRegistrar接口的实现类的方式向Spring容器中注入一个Bean对象,下面就一起看看这种用法的实现过程。首先定义一个普通java类,源码如下:

public class Dog {
}

然后再定义一个ZooRegistrar类,该类实现了ImportBeanDefinitionRegistrar接口,并实现了该接口中的registerBeanDefinitions ()方法,该类的源码如下所示:

import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.type.AnnotationMetadata;

public class ZooRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        GenericBeanDefinition gbd = new GenericBeanDefinition();
        gbd.setBeanClass(Dog.class);
        registry.registerBeanDefinition("dog", gbd);
    }

}

接着再定义一个配置类ZooConfigBC,该类上有两个注解@Configuration和@Import,其中@Import注解中指定ZooRegistrar.class为属性,具体源码如下所示:

@Configuration
@Import(ZooRegistrar.class)
public class ZooConfigBC {
}

最后编写一个测试类,用于验证这种写法能否正常向Spring容器中注入相关对象(Dog对象)。该测试类的源码如下所示:

@SpringBootApplication
public class EurekaServiceApplication {

    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(EurekaServiceApplication.class, args);
        Dog dog = ctx.getBean(Dog.class);
        System.out.println(dog.getClass().getSimpleName());
    }

}

通过观察控制台输出,我们可以发现Dog可以正常注入到Spring容器中。

通过前面的梳理,我们知道@Import注解的作用就是向Spring容器中注入Bean,也了解了通过@Import注解向Spring容器中注入Bean的方法。下面我们将继续梳理Spring解析@Import注解的步骤。

前面梳理到ConfigurationClassParserdoProcessConfigurationClass(ConfigurationClass, SourceClass)方法。在该方法中我们可以看到有这样一段代码,如下所示:

processImports(configClass, sourceClass, getImports(sourceClass), true);

这段代码中的getImports(sourceClass)方法的主要作用就是搜集源类上的@Import注解,这里的源类是我们项目的启动类,即EurekaServiceApplication(这个类上有两个注解,一个是@SpringBootApplication,一个是@EnableDiscoveryClient),具体情况如下所示:

图中的情况与上面的说法一致,下面先看一下getImports(sourceClass)方法,该方法及其相关方法的源码如下所示:

private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
    Set<SourceClass> imports = new LinkedHashSet<SourceClass>();
    Set<SourceClass> visited = new LinkedHashSet<SourceClass>();
    collectImports(sourceClass, imports, visited);
    return imports;
}
private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited)
       throws IOException {

    if (visited.add(sourceClass)) {
       for (SourceClass annotation : sourceClass.getAnnotations()) {
          String annName = annotation.getMetadata().getClassName();
          if (!annName.startsWith("java") && !annName.equals(Import.class.getName())) {
             collectImports(annotation, imports, visited);
          }
       }
       imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
    }
}

从CollectImports()方法的源码不难看出:这个方法采用递归调用的方式,逐步找出源类及该类上的注解通过@Import注解导入的类。具体过程如下图所示:

从图中可以看出第一轮循环先处理启动类上的@SpringBootApplication注解,此时annName的值为org.springframework.boot.autoconfigure.SpringBootApplication,接下来用if分支中的判断条件处理后发现if分支中的逻辑可以执行,所以下面会递归调用collectImports()方法,其中sourceClass参数的值为@SpringBootApplication注解,imports参数的值为imports,visited的值为visited集合。此时可以看到下面这样一幅图片:

图中的Evaluate对话框展示的是sourceClass.getAnnotations()操作从SpringBootApplication注解类中拿到的注解信息,其中前四个是java提供的元注解,后三个则是Spring框架提供的注解。因此这轮解析中只有后三个会被处理,其中SpringBootConfiguration注解处理后,imports集合无变更(默认大小为零,处理后依旧为零)、EnableAutoConfiguration注解处理后,imports集合有变化(默认大小为零,处理后变为二)、ComponentScan注解处理后,imports集合无变化(默认大小为二,处理后依旧为二)。这里一起看一下EnableAutoConfiguration注解的详细源码,如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
/** 注意下面这个注解 */
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    /**
     * Exclude specific auto-configuration classes such that they will never be applied.
     * @return the classes to exclude
     */
    Class<?>[] exclude() default {};

    /**
     * Exclude specific auto-configuration class names such that they will never be
     * applied.
     * @return the class names to exclude
     * @since 1.3.0
     */
    String[] excludeName() default {};

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
/** 注意下面这个注解 */
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {

}

从代码可以看出,这里有两个@Import注解,所以前面递归解析后imports集合大小会变成二。由于启动类上还有一个@EnableDiscoveryClient注解,该注解的源码如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

    /**
     * If true, the ServiceRegistry will automatically register the local server.
     */
    boolean autoRegister() default true;
}

从源码可以看出这个注解上有个@Import注解,所以最终imports集合的大小为三,最终效果如下图所示:

imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"))这一句代码位于collectImports()方法中。其主要作用就是将定义在sourceClass(比如EurekaServiceApplication启动类)上的@Import注解中的值解析出来并添加到imports集合中。

接下来就可以回到processImports()方法中了,这段方法的主要作用是对import候选类进行处理。在开始梳理前,先来看一下它的源码,如下所示

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
       Collection<SourceClass> importCandidates, boolean checkForCircularImports) throws IOException {

    if (importCandidates.isEmpty()) {
       return;
    }

    if (checkForCircularImports && isChainedImportOnStack(configClass)) {
       this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
    }
    else {
       this.importStack.push(configClass);
       try {
          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));
             }
          }
       }
       catch (BeanDefinitionStoreException ex) {
          throw ex;
       }
       catch (Throwable ex) {
          throw new BeanDefinitionStoreException(
                "Failed to process import candidates for configuration class [" +
                configClass.getMetadata().getClassName() + "]", ex);
       }
       finally {
          this.importStack.pop();
       }
    }
}

为了更清楚的捋顺这个方法的处理逻辑,我们先来看一下这个方法在运行过程中的情况,具体如下图所示:

注意:configClass、currentSourceClass均为启动类,而importCandidates则是前面getImports()执行的结果集。因此图中蓝色条纹标注出来的importCandidates集合有三个值。蓝色条纹所处的代码块的处理逻辑也相对简单:

  • 判断import候选类是否是ImportSelector类型,如果是则加载这个类,然后通过反射的方式创建对象,最后反射调用这个对象上的Aware方法。接着判断这个对象是否是DeferredImportSelector,如果是则将其添加到deferedImportSelectors集合中,如果不是则直接执行这个类上的selectImports()方法,接着将返回结果转化为Collection集合,最后再次调用processImports()方法处理selectImports()方法返回的数据(这正呼应了前面说的实现ImportSelect接口的类,其selectImports()方法返回的数据会被加载到Spring容器中)

未完,请见谅

 

相关推荐

  1. 02--SpringBoot自动装配原理

    2024-04-15 03:04:02       10 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-15 03:04:02       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-15 03:04:02       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-15 03:04:02       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-15 03:04:02       20 阅读

热门阅读

  1. 贪心算法-分发饼干

    2024-04-15 03:04:02       11 阅读
  2. VirtualBox - 与 Win10 虚拟机 与 宿主机 共享文件

    2024-04-15 03:04:02       17 阅读
  3. MindSQL

    MindSQL

    2024-04-15 03:04:02      21 阅读
  4. 网络协议学习——IP协议

    2024-04-15 03:04:02       15 阅读
  5. 面试流程梳理

    2024-04-15 03:04:02       16 阅读
  6. golang context

    2024-04-15 03:04:02       17 阅读
  7. MongoDB聚合运算符:$not

    2024-04-15 03:04:02       14 阅读
  8. Android 编译C程序APP

    2024-04-15 03:04:02       15 阅读
  9. 读《股票大作手回忆录》有感

    2024-04-15 03:04:02       15 阅读
  10. Go 之常见的几种设计模式

    2024-04-15 03:04:02       15 阅读