清明前夕,我发表了一篇与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容器会在初始化过程中处理被导入的类,并依据类的不同特性执行不同的操作:
- 导入配置类:如果@Import的参数是一个带有@Configuration注解的类,则Spring容器会像处理其他配置类一样处理这个类,包括扫描并实例化其中通过@Bean注解的方法所定义的Bean
- 导入普通类:从Spring 4.2开始,@Import不仅可以导入配置类,还可以导入普通的类。这意味着即使不是配置类,只要通过@Import引入,Spring也会尝试将该类作为Bean进行实例化和管理
- 实现ImportSelector接口:当@Import的参数是一个实现了ImportSelector接口的类时,Spring容器会实例化该类,并调用selectImports()方法。此方法返回一个包含类全路径名的字符串数组,Spring容器会按照返回的列表加载并实例化那些类
- 实现DeferredImportSelector接口:类似于ImportSelector,但DeferredImportSelector的selectImports()方法调用时机更晚,确保在所有常规的@Configuration类处理完毕后才进行。这对于那些依赖于其他Bean配置完成后才能确定导入哪些类的情况非常有用
- 实现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 {@Beanpublic 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 {@Beanpublic Tiger tiger() {return new Tiger();}}
接着再定义一个ZooImportSelector类,该类实现了ImportSelector接口,并实现了该接口中的selectImports()方法,该类的源码如下所示:
public class ZooImportSelector implements ImportSelector {@Overridepublic 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 {@Overridepublic 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注解的步骤。
前面梳理到ConfigurationClassParser类doProcessConfigurationClass(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 importsClass<?> 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 definitionsClass<?> 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 classthis.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容器中)
未完,请见谅