清明前夕,我发表了一篇与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 {
@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容器中。
1.4 自动导入原理
通过前面的梳理,我们知道@Import注解的作用就是向Spring容器中注入Bean,也了解了通过@Import注解向Spring容器中注入Bean的方法。前面虽然梳理了@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 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集合有三个值。蓝色条纹所处的代码块的处理逻辑也相对简单(遍历importCandidates集合并进行相应的处理,具体处理过程见下面描述):
- 判断import候选类是否是ImportSelector类型,如果是则加载这个类,然后通过反射的方式创建对象,最后反射调用这个对象上的Aware方法。接着判断这个对象是否是DeferredImportSelector,如果是则将其添加到deferedImportSelectors集合中;如果不是则直接执行这个类上的selectImports()方法,接着将该方法返回的结果转化为Collection集合,最后再次调用processImports()方法处理这个方法返回的集合(这正呼应前面讲的实现ImportSelect接口的类,其selectImports()方法返回的数据会被加载到Spring容器中)
- 判断import候选类是否是ImportBeanDefinitionRegistrar类型,如果是则加载这个类,然后通过反射的方式创建对象,最后反射调用这个对象上的Aware方法。接着调用ConfigurationClass类上的addImportBeanDefinitionRegistrar()方法将当前候选类添加到ConfigurationClass类上的importBeanDefinitionRegistrars集合中
- 如果当前import候选类既不是ImportSelector类型,也不是ImportBeanDefinitionRegistrar类型。则调用本类importStack属性上的registerImport()方法,将其注册到importStack属性中。最后调用processConfigurationClass继续解析这个类(跟踪的时候新增了一个ConfigDemo类,然后通过在启动类上的Import注解,将其导入到Spring容器中)
该方法执行完毕后,便持续向上返回,直到返回到ConfigurationClassParser类中的parse(Set<BeanDefinitionHolder>)方法时停止。由于传递到该方法中的configCandidates集合中只有一个数据,所以当调用完下级逻辑返回到本方法后,就会跳出循环,直接执行该方法中的最后一行代码:processDeferredImportSelectors()。下面看一下这个方法的源码,如下所示:
private void processDeferredImportSelectors() {
List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
this.deferredImportSelectors = null;
Collections.sort(deferredImports, DEFERRED_IMPORT_COMPARATOR);
for (DeferredImportSelectorHolder deferredImport : deferredImports) {
ConfigurationClass configClass = deferredImport.getConfigurationClass();
try {
String[] imports = deferredImport.getImportSelector().selectImports(configClass.getMetadata());
processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configClass.getMetadata().getClassName() + "]", ex);
}
}
}
这段方法的主要处理逻辑就是遍历当前对象中的deferredImportSelectors属性,这个属性经过前面的解析后,会存在两个数据,分别为:
- EnableDiscoveryClientImportSelector
- EnableAutoConfigurationImportSelector
接着for循环体会从包装类DeferredImportSelectorHolder的对象中拿到ImportSelector接口对象,然后调用该对象上的selectImports()方法。这个调用最终执行的就是上面两个类中的selectImports()方法。注意:前面在讲@Import注解时提到过,selectImports()方法会返回一个包含类全路径的字符串数组,Spring容器会加载并实例化列表中的类,所以在这个方法中可看processImports(configClass, asSourceClass(configClass), asSourceClasses(imports), false)这样一句代码。下面就以EnableDiscoveryClientImportSelector类中selectImports()方法的返回值[org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration]为例继续梳理。将该参数传递给processImport()方法后的运行时状态如下图所示:
processImports()方法的几个参数的实际值可参见上图的右下角,从图中可以知道:configClass和currentSourceClass的值是一样的,即org.com.chinasoft.EurekaServiceApplication(就是项目的启动类);checkForCircularImports是一个布尔值false; importCandidates是一个数组,即[org.springframework.cloud.client.serviceregistry.AutoServiceRegistrationConfiguration]。由于此方法会遍历importCandidates对象,并且AutoServiceRegistrationConfiguration既不是ImportSelector类型,也不是ImportBeanDefinitionRegistrar,所以最终会执行循环体中的else分支,最终又会调用ConfigurationClassParser类中processConfigurationClass(ConfigurationClass)方法,此时这个方法接收的参数如下图所示:
接着processConfigurationClass()方法又会调用ConfigurationClassParser类中的doProcessConfigurationClass(ConfigurationClass, SourceClass)方法。然后再次调用ConfigurationClassParser类中的processImports(ConfigurationClass configClass, SourceClass currentSourceClass, Collection<SourceClass> importCandidates, boolean checkForCircularImports)方法,调用这个方法的运行时信息如下所示:
从图中可以知道通过getImports(sourceClass)方法获取到的Import类为org.springframework.boot.context.properties.EnableConfigurationPropertiesImportSelector。这个类是在EnableConfigurationProperties接口上通过@Import注解引入的,具体如下图所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(EnableConfigurationPropertiesImportSelector.class)
public @interface EnableConfigurationProperties {
/**
* Convenient way to quickly register {@link ConfigurationProperties} annotated beans
* with Spring. Standard Spring Beans will also be scanned regardless of this value.
* @return {@link ConfigurationProperties} annotated beans to register
*/
Class<?>[] value() default {};
}
注意这个注解是配置在AutoServiceRegistrationConfiguration类上的,具体的配置代码如下所示:
@Configuration
@EnableConfigurationProperties(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public class AutoServiceRegistrationConfiguration {
}
上面调完getImports(sourceClass)方法后,会继续调用processImports()方法,这个方法的处理逻辑,前面已经梳理过了,这里不再叙述。这里主要看最终引入的EnableConfigurationPropertiesImportSelector类的selectImports()方法的执行逻辑,这个方法的源码如下所示:
public String[] selectImports(AnnotationMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata.getAllAnnotationAttributes(
EnableConfigurationProperties.class.getName(), false);
Object[] type = attributes == null ? null
: (Object[]) attributes.getFirst("value");
if (type == null || type.length == 0) {
return new String[] {
ConfigurationPropertiesBindingPostProcessorRegistrar.class
.getName() };
}
return new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(),
ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() };
}
这个方法首先会拿到@EnableConfigurationProperties注解中的value值,这里拿到的便是AutoServiceRegistrationProperties。见AutoServiceRegistrationConfiguration类上的注解。这个方法最终会返回一个数组,数组中的值为:new String[] { ConfigurationPropertiesBeanRegistrar.class.getName(), ConfigurationPropertiesBindingPostProcessorRegistrar.class.getName() }。这个结果会返回给上级调用者,即ConfigurationClassParser#processImports()方法,具体如下图所示:
最终会开始新一轮的processImports()处理。关于这部分处理,这里就不再过多强调了。因为这个过程和前面的梳理过程一样。
下面让我们再次回到ConfigurationClassParser类的processDeferredImportSelectors()方法中的for循环体。经过第一轮循环代码处理了启动类上的@EnableDiscoveryClient注解通过@Import注解导入的信息。接下来开启第二轮循环处理启动类上的@SpringBootApplication注解通过@Import注解导入的EnableAutoConfigurationImportSelector类。
通过梳理,我大概明白了@Import注解的解析过程,但有一点我非常困惑,通过这个注解导入相关的类究竟有什么用呢?在回答这个问题前,我想总结一下@Import注解的解析过程。不过在开始前先看一下Selector体系的继承结构,具体如下所示:
通过上面这幅图可以看到:AutoConfigurationImportSelector实现了DeferredImportSelector、BeanClassLoaderAware、ResourceLoaderAware、BeanFactoryAware、EnvironmentAware、Ordered等接口;SpringFactoryImportSelector实现了DeferredImportSelector、BeanClassLoaderAware、EnvironmentAware等接口;ManagementContextConfigurationsImportSelector实现了DeferredImportSelector、BeanClassLoaderAware等接口。下面就对Spring容器解析@Import注解的具体过程做一个总结,大致是这样的:
- ConfigurationClassPostProcessor#postProcessBeanDefinitionRegistry(BeanDefinitionRegistry)
- ConfigurationClassPostProcessor#processConfigBeanDefinitions(BeanDefinitionRegistry)
- ConfigurationClassParser#parse(Set<BeanDefinitionHolder>):这个地方要重点关注。在这个地方我主要关注自己定义的启动类EurekaServiceApplication
- ConfigurationClassParser#parse(AnnotationMetadata, String):这个方法非常简单,仅仅一行代码——调用本类的processConfigurationClass()方法
- ConfigurationClassParser#processConfigurationClass(ConfigurationClass):该方法的主要作用是调用同类中的doProcessConfigurationClass()方法
- ConfigurationClassParser#doProcessConfigurationClass(ConfigurationClass, SourceClass):该类是@Import注解解析的调用入口。在调用processImports()方法之前,会先调用同类中的getImports()方法解析待解析的启动类上通过@Import注解导入的类【这里会递归调用,直至解析完启动类上所有通过@Import注解导入的类为止】。注意这个方法中还会调用@PropertySource、@ComponentScan、@ImportResource、@Bean等注解的解析方法。其中前两个在调用@Import注解解析方法前调用,后面两个在@Import注解解析方法后调用。其中调用@ComponentScan注解解析方法时,会回调ConfigurationClassParser类中的parse(String, String)方法,然后继续调用ConfigurationClassParser类中的processConfigurationClass(ConfigurationClass)方法,也即回调上一步中提到的方法
- ConfigurationClassParser#processImports(ConfigurationClass, SourceClass, Collection<SourceClass>, boolean):真正解析@Import注解的地方。注意第三个参数就是上一步通过getImports()方法解析出的通过@Import注解导入的类的集合。这个方法主要作用就是循环遍历第三个参数集合中的元素,并根据元素的实际类型做进一步处理:如果元素的实际类型为ImportSelector,则创建对象,然后回调其中的aware()方法,接着判断该对象是否是DeferredImportSelector类型,如果不是则直接调用这个对象上的selectImports()方法,最后继调processImports()方法解析ImportSelector实现类通过selectImports()方法导入的类;如果元素的实际类型为ImportBeanDefinitionRegistrar,则创建对象,然后回调其中的aware()方法,接着调用ConfigurationClass对象上的addImportBeanDefinitionRegistrar()方法,具体代码可以翻阅源码;如果不是上面两种类型,则直接当作普通的Configuration配置类进行解析,回调ConfigurationClassParser类上的processConfigurationClass(ConfigurationClass)方法,即回调第5步中的方法
- 上述过程执行完毕后,代码会回到ConfigurationClassParser类中的parse(Set<BeanDefinitionHolder>)方法。这一步骤主要关注这个方法中的最后一行代码:processDeferredImportSelectors()方法。这个方法的主要作用就是遍历deferredImports集合,然后调用集合元素上的selectImports()方法,并调用ConfigurationClassParser类中的processImports()方法继续解析(解析通过@Import注解导入的类)
2 关于第一小节问题的梳理
在上一节中,我们主要梳理了SpringBoot对@Import注解修饰的类的处理过程。在梳理过程中有很多困惑,譬如前一小节提到的一个问题:通过这个注解(@Import)导入相关的类究竟有什么用呢?这一小节我们将围绕这个问题进行拓展,以便让自己知其然而知其所以然。在梳理这个问题前,让我们先来回顾一下这个问题是怎么引发的。
首先通过前面的源码,我们知道EnableDiscoveryClient注解上有一个@Import注解,这个注解是这样写的:@Import(EnableDiscoveryClientImportSelector.class)。根据前面对@Import注解的梳理,我们知道这个注解的主要作用就是向Spring容器中导入一些类,而这些要导入的类是通过EnableDiscoveryClientImportSelector类中的selectImports()方法确定的,譬如这个类的selectImports()方法向容器中注入了一个AutoServiceRegistrationConfiguration类。
然后通过观察AutoServiceRegistrationConfiguration类的源码,我们知道这个类上有这样一些注解:@Configuration、@EnableConfigurationProperties、@ConditionalOnProperty,其具体写法为:
@Configuration
@EnableConfigurationProperties(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public class AutoServiceRegistrationConfiguration {
}
通过这个源码我们又认识了一个名字叫做AutoServiceRegistrationProperties的类,出于好奇我们又看了它详细的源码:
@ConfigurationProperties("spring.cloud.service-registry.auto-registration")
public class AutoServiceRegistrationProperties {
/** If Auto-Service Registration is enabled, default to true. */
private boolean enabled = true;
/** Whether to register the management as a service, defaults to true */
private boolean registerManagement = true;
/** Should startup fail if there is no AutoServiceRegistration, default to false. */
private boolean failFast = false;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public boolean isRegisterManagement() {
return registerManagement;
}
@Deprecated
public boolean shouldRegisterManagement() {
return registerManagement;
}
public void setRegisterManagement(boolean registerManagement) {
this.registerManagement = registerManagement;
}
public boolean isFailFast() {
return failFast;
}
public void setFailFast(boolean failFast) {
this.failFast = failFast;
}
}
在这个类上有一个@ConfigurationProperties的注解,这个注解自定义了一个value值,其名字为:spring.cloud.service-registry.auto-registration。看到这里我不禁问出了本小节开头的那个问题:通过这个注解(@Import)导入相关的类究竟有什么用呢?我觉得要得到这个问题的答案,我们必须要弄明白这样几个问题:
- @Configuration、@EnableConfigurationProperties、@ConditionalOnProperty注解究竟有什么作用?
- AutoServiceRegistrationProperties类究竟被谁使用了,这个使用者用它干了什么,最终得到了什么效果?
首先来看第一个问题中提到的@Configuration注解:@Configuration注解是Spring框架中的一个核心注解,主要用于标记类是一个配置类。这个注解隶属于Spring的Java配置方式,它是Spring基于Java类进行配置的重要组成部分。使用Java配置替代传统的XML配置,可以让项目更加简洁、类型安全,并且能够充分利用IDE的代码提示和检查功能。
当一个类被@Configuration注解时,这个类就会被Spring容器识别为配置类,其内部可以包含bean定义的方法。这些方法通常使用@Bean注解来标记,表示它们将返回一个由Spring管理的bean实例。配置类可以相互引用,形成一个配置类的层级结构,从而组织和模块化配置代码。例如:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
@Bean
public MyRepository myRepository() {
return new MyRepositoryImpl();
}
}
在上面的例子中,AppConfig类被标记为配置类,它定义了两个bean:myService和myRepository。Spring容器会调用这两个方法来创建和初始化对应的bean实例,并管理它们的生命周期。
此外,配置类之间可以通过@Import注解来导入其他配置类,实现配置的模块化和复用。还可以通过@PropertySource注解来指定外部属性文件,或者使用@Value注解来注入外部配置值,增强配置的灵活性。
总之,@Configuration注解是Spring框架用于实现基于Java类的配置的核心组件,它极大地提高了应用的可读性、维护性和灵活性。
然后再来看第一个问题中提到的@ConditionalOnProperty注解:它是Spring框架提供的一个条件注解。关于条件注解:Spring框架提供了条件注解(Conditional Annotations),这些注解允许基于特定条件来决定是否创建一个Bean或者启用某个配置。这一特性增强了应用的可配置性和环境适应性,使得开发者能够编写更加灵活的代码,特别是在需要根据不同环境或条件配置Bean的情况下。以下是一些常用的Spring条件注解:
- @ConditionalOnClass:当classpath中存在指定的类时,条件成立。这常用于检查某个库是否存在,从而决定是否配置相关Bean。譬如这段代码:@Configuration @ConditionalOnClass(DataSource.class) public class DatabaseConfig {}
- @ConditionalOnMissingClass:与`@ConditionalOnClass`相反,当classpath中不存在指定的类时,条件成立
- @ConditionalOnProperty:根据配置属性的值决定是否加载配置。可以检查属性是否存在,或者其值是否匹配某个预期值。譬如这段代码:@Configuration @ConditionalOnProperty(name = "feature.flag", havingValue = "true") public class FeatureFlagConfig { }
- @ConditionalOnExpression:基于SpEL表达式的评估结果决定。如果表达式求值为true,则条件成立。譬如下面这段代码:@Configuration @ConditionalOnExpression("${app.env} == 'prod'") public class ProductionConfig { }
- @ConditionalOnBean:当容器中存在指定类型的Bean时,条件满足。譬如下面这段代码:@Configuration @ConditionalOnBean(TransactionalService.class) public class ServiceConfig { }
- @ConditionalOnMissingBean:当容器中不存在指定类型的Bean时,条件满足。这常用于避免Bean的重复定义
- @ConditionalOnWebApplication和@ConditionalOnNotWebApplication:分别用于web应用和非web应用环境下激活配置。
- @ConditionalOnJava:基于Java运行时版本决定。譬如下面这段代码所表示的含义就是只有在Java 8环境下才会加载配置:@ConditionalOnJava(JavaVersion.EIGHT)
- 自定义条件注解:通过实现Condition接口并使用@Conditional注解自己的类,可以创建自定义的条件判断逻辑
总之,通过使用这些条件注解,Spring应用可以更智能地适应不同的部署环境和配置需求,提高代码的复用性和灵活性。
梳理完条件注解,我们不得不再来研究一下@ConditionalOnProperty注解,这个注解的源码如下所示:
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Documented
@Conditional(OnPropertyCondition.class)
public @interface ConditionalOnProperty {
String[] value() default {};
String prefix() default "";
String[] name() default {};
String havingValue() default "";
boolean matchIfMissing() default false;
boolean relaxedNames() default true;
}
通过源码可以知道这个注解有这样几个属性:value、name、prefix、havingValue、matchIfMissing、relaxedNames,其中几个属性的作用如下所示:
- prefix:配置文件中的前缀。
- name:配置的名字。
- havingValue:它的值与配置文件的值对比,当两个值相同,类会被加载到spring的IOC容器中。
- matchIfMissing:缺少该配置时是否可以加载,如果为true,没有该配置属性时也会正常加载,反之则不会生效。【网络博文描述:matchIfMissing属性用来指定如果配置文件中未进行对应属性配置时的默认处理:默认情况下matchIfMissing为false,也就是说如果未进行属性配置,则自动配置不生效。如果matchIfMissing为true,则表示如果没有对应的属性配置,则自动配置默认生效】
关于条件注解,这里有一篇博文,如果有兴趣,大家可以浏览一番:《条件注解之@ConditionalOnProperty注解:通过配置文件的配置来控制配置类是否加入spring的IOC容器》。
接着再来看第一个问题中提到的@EnableConfigurationProperties注解:这也是Spring框架提供的一个注解,用于启用配置属性的绑定功能。这个注解主要用于简化配置文件中的属性与Java Bean之间的映射过程,使得开发者能够更方便地在应用中使用这些配置属性。当在Spring Boot应用中定义了一个或多个带有@ConfigurationProperties注解的类来封装配置文件中的属性时,使用@EnableConfigurationProperties可以告诉Spring去扫描并自动装配这些配置类。例如,有一个配置类如下:
@ConfigurationProperties(prefix = "app")
public class AppProperties {
private String name;
private int port;
// Getter and Setter methods...
}
为了使Spring Boot应用能够识别并加载这个配置类,你可以在配置类所在的包或者启动类上添加@EnableConfigurationProperties注解:
@SpringBootApplication
@EnableConfigurationProperties({AppProperties.class})
public class MyAppApplication {
public static void main(String[] args) {
SpringApplication.run(MyAppApplication.class, args);
}
}
或者,如果希望Spring自动发现所有带有@ConfigurationProperties注解的类,可以不指定具体类,而是使用其默认行为:
@SpringBootApplication
@EnableConfigurationProperties
public class MyAppApplication {
}
这样,Spring Boot在启动时会自动查找并绑定配置文件中app.name和app.port等前缀对应的属性到AppProperties类的相应字段上,使得你可以在应用的其他部分通过依赖注入的方式来使用这些配置值。
再次一起看下与@EnableConfigurationProperties相关的注解@ConfigurationProperties:它是Spring框架中的一个注解,用于将外部配置文件中的属性绑定到一个Bean的字段上。这个注解通常与@Component、@Configuration或者@Service等注解一起使用,以使得Spring能够自动检测并管理这些配置属性的Bean。
使用@ConfigurationProperties可以简化配置管理,让我们能够以类型安全的方式访问配置值,而不是直接使用@Value注解逐个注入每个属性。其基本用法如下:
- 定义配置类:首先,创建一个类来持有配置属性,并使用@ConfigurationProperties注解。你可以指定配置属性的前缀,这样Spring会自动将对应前缀的配置项与该类的字段匹配并赋值。譬如:@ConfigurationProperties(prefix = "myapp") @Component public class MyAppProperties { private String url; private int timeout; /* getters and setters... */ }。在这个例子中,Spring会尝试从配置源(如application.properties或application.yml)中读取myapp.url和myapp.timeout的值,并注入到相应的字段。
- 启用配置绑定:为了让Spring知道需要处理这些配置属性,你需要在你的配置类或主启动类上使用@EnableConfigurationProperties,或者如果你使用的是Spring Boot,通常只需要将配置类放在特定的包下或直接使用Spring Boot的自动配置特性,它会自动扫描并绑定配置。譬如:@SpringBootApplication @EnableConfigurationProperties(MyAppProperties.class) public class MyAppApplication { public static void main(String[] args) { SpringApplication.run(MyAppApplication.class, args); } }。或者,如之前提到的,对于Spring Boot应用,可以不显式指定类,让Spring自动发现:@SpringBootApplication public class MyAppApplication { }。
- 注入并使用配置:最后,在需要使用这些配置的地方,简单地通过@Autowired注入配置类的实例即可。譬如:@Service public class MyService { @Autowired private MyAppProperties appProps; public void doSomething() { String url = appProps.getUrl(); int timeout = appProps.getTimeout(); } }
通过这种方式,我们可以轻松地管理和使用应用程序的各种配置属性,同时保持代码的清晰和可维护性。在实践中通常这个注解经常使用的方式有这样几种:@ConfigurationProperties配合@Component或者被@Component注解修饰的注解(@Controller、@Service、@Repository、@Configuration)使用;@ConfigurationProperties配合@Bean注解在配置类的Bean定义方法上使用;@ConfigurationProperties配合@EnableConfigurationProperties使用。
最后,通过前面的梳理可以看出通过@EnableDiscoveryClient注解,我们向容器中注入了一个名为AutoServiceRegistrationConfiguration的类。然后通过它又向容器中引入了名为AutoServiceRegistrationProperties的类(这一步是通过@EnableConfigurationProperties注解完成的)。这个类是一个配置类,它通过@@ConfigurationProperties注解为其属性指定了通过配置文件配置属性值时的前缀:spring.cloud.service-registry.auto-registration。通过这个类可以设定的属性有:enabled、registerManagement、failFast。注意这些属性值都有一个默认值,前两者的默认值为true,后一个的默认值为false。再回过头看AutoServiceRegistrationAutoConfiguration类,这个类上有一个名为ConditionalOnProperty的注解,其配置详情为:
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
根据前面的梳理,我们可以明白这里的意思是当配置项spring.cloud.service-registry.auto-registration.enabled缺失时,AutoServiceRegistrationAutoConfiguration类依然要注入到Spring容器中(由于matchIfMissing的值被配置为true)。因此简单理解的话,本小节开头提出的问题的答案就是“通过@Import注解,我们向容器中导入了一些前缀为spring.cloud.service-registry.auto-registration的配置项”。
下面再来看一下第二个问题,这个问题中首先问道:AutoServiceRegistrationProperties类究竟被谁使用了?为了回答这个问题,我们可以使用集成工具Idea的搜索功能,通过搜索可以发现使用这个类的有这样几个:AbstractAutoServiceRegistration<R extends Registration>、AutoServiceRegistrationAutoConfiguration、EurekaClientAutoConfiguration、spring-configuration-metadata.json,具体如下图所示:
AutoServiceRegistrationAutoConfiguration,这个不就是前面梳理的哪个类吗?这里还有一个相对熟悉的类EurekaClientAutoConfiguration,在梳理这个类的时候我曾有过这样的疑问:AutoServiceRegistrationProperties和EurekaClientAutoConfiguration是怎么关联到一起的,如今这个问题基本解决了。EurekaClientAutoConfiguration类使用AutoServiceRegistrationProperties类的代码如下所示:
@Bean
@ConditionalOnBean(AutoServiceRegistrationProperties.class)
@ConditionalOnProperty(value = "spring.cloud.service-registry.auto-registration.enabled", matchIfMissing = true)
public EurekaAutoServiceRegistration eurekaAutoServiceRegistration(ApplicationContext context, EurekaServiceRegistry registry,
EurekaRegistration registration) {
return new EurekaAutoServiceRegistration(context, registry, registration);
}
这段代码表示当容器中存在AutoServiceRegistrationProperties时,才会想容器中注入EurekaAutoServiceRegistration对象。这就解释了第二个问题中的第二部分:怎么用的。关于使用这个类之后的效果,可以看后面梳理Eureka原理的内容。
3 SpringBoot自动装配原理
通过前面两小节的梳理,我知道如何通过@Import注解向容器导入一些自定义的数据信息,也弄清楚了Spring框架解析@Import注解的基本流程。不过,即使这样,我依旧搞不明白为什么redis、kafka、mybatis、eureka等组件只通过一些简单的配置就能注入到Spring容器中。为了解决这个问题我看了很多博客,也搜了很多视频,但都没有找到一个令我满意的回答。无奈,我只能循着这些大佬给出的线索自行摸索。
前面在梳理@Import注解解析过程的时候,提到过在@SpringBootApplication注解上有一个@EnableAutoConfiguration注解,这个注解的源码如下所示:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
从源码可以看出,这个[U1] 注解上有个@Import注解,这个注解中的value是一个class对象——EnableAutoConfigurationImportSelector.class。从这个类的命名不难看出,它实现了ImportSelector接口,其源码如下所示:
[U1]指@EnableAutoConfiguration
public class EnableAutoConfigurationImportSelector
extends AutoConfigurationImportSelector {
@Override
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass().equals(EnableAutoConfigurationImportSelector.class)) {
return getEnvironment().getProperty(
EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class,
true);
}
return true;
}
}
从源码可知EnableAutoConfigurationImportSelector类继承了AutoConfigurationImportSelector类,这个类实现了DeferredimportSelector和Aware类接口,具体继承结构可以参看下面这幅图:
第二小节开头有提到过“当@Import注解的参数是一个实现了ImportSelector接口的类时,Spring容器会实例化该类,并调用实现类中的selectImports()方法。此方法返回一个包含类全路径名的字符串数组,Spring容器会按照返回的列表加载并实例化那些类”。从上面这这幅图可以看出EnableAutoConfigurationImportSelector类最终实现了ImportSelector接口,而这个类又是@Import注解的参数,所以根据本段前半部分的描述:Spring容器启动时,会调用这个类中的selectImport()方法,并会实例化这个方法返回的类。通过源码可以知道类EnableAutoConfigurationImportSelector的selectImport()方法并不在本类上,而位于其继承的父类AutoConfigurationImportSelector类中,这个方法的源码如下所示(源码中每步操作的具体含义参见注释):
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// annotationMetadata,该对象有三个属性:introspectedClass、annotations、nestedAnnotationsAsMap,其中第一个属性值为启动类、第二个属性值为启动类上的两个注解
// 下面调用isEnabled()方法的意义是什么呢?根据前面的继承关系和源码可以知道,这里最终调用的是EnableAutoConfigurationImportSelector类中的isEnabled()方法,关于这个方法的详细信息可以参看源码。这个方法调用自身类上的getClass()方法,然后判断这个值是否跟类名.class的值是否一致,如果一致,就走if分支,否则不走。if分支的处理也很简单,就是从当前环境变量中拿到key为spring.boot.enableautoconfiguration的值,源码是这样写的:getEnvironment().getProperty( EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true),其含义就是从配置环境中拿spring.boot.enableautoconfiguration的配置值,如果没有,就给个默认值true【事实也确实如此,系统中没有,最终返回了defaultValue即true】
// 这么分析下来,这个调用的最终返回结果为true,所以后继续if()分支后面的代码
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
try {
// 读取spring-boot-autoconfigure-1.5.7.RELEASE.jar包META-INF目录下的spring-autoconfigure-metadata.properties文件(这仅仅是文件之一),并将最终结果包装到AutoConfigurationMetadata对象中。反正最终会读取所有jar包下所有位于META-INF目录下的spring-autoconfigure-metadata.properties文件
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
// 读取启动类上的注解中的属性值,比如:excludeName、exclude
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 读取所有jar包中META-INF目录下名为spring.factories文件中的值【这个文件中配置的就是自动配置类,比如常见的kafka自动配置类-KafkaAutoConfiguration,或者本文要梳理的Eureka客户端自动配置类-EurekaClientAutoConfiguration】。注意在测试案例中这一步读出来了128个数据【个人理解这一步的主要作用就是读出所有的候选自动配置类。测试时在项目资源路径中新建了一个META-INF目录,然后创建了一个spring.factories文件,并将Eureka相关jar包中的自动配置类写到这个文件中,最终读出了133个数据】
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
// 移除重名的自动配置类,说白了就是缩小上一步获取的候选自动配置类的范围【上面读出的133个数据因为有重复的,所以经过下面这个步骤集合中只剩下128个数据】
configurations = removeDuplicates(configurations);
// 对集合中的数据进行排序
configurations = sort(configurations, autoConfigurationMetadata);
//如果在启动类上的@SpringBootApplication注解上添加一个属性exclude,并指定其值为KafkaAutoConfiguration.class,下面这个操作会读出这个类的全路径,并包装成Set集合。那么读取AnnotationAttributes对象那一步会读出这个值,并且前面读取出来的候选可配置类集合中包含了KafkaAutoConfiguration类
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 下面这一步做了上面目前不确定
checkExcludedClasses(configurations, exclusions);
// 经过下面的步骤后,@SpringBootApplication注解中属性exclude指定的值KafkaAutoConfiguration会被移除,最终configurations集合的大小会从128变为127
configurations.removeAll(exclusions);
// 通过第一步读取的集合对候选自动配置类集合进行过滤【经过这一步,最终的数据量大小由127个变为43个】
configurations = filter(configurations, autoConfigurationMetadata);
// 触发AutoConfigurationImportEvents事件
fireAutoConfigurationImportEvents(configurations, exclusions);
// 将自动配置类集合返回,并添加到Spring容器中
return configurations.toArray(new String[configurations.size()]);
}
catch (IOException ex) {
throw new IllegalStateException(ex);
}
}
下面这幅图片展示了上面方法中configurations = removeDuplicates(configurations)一句代码执行前,configurations集合的情况(注意集合中包含KafkaAutoConfiguration类):
下面这幅图片则展示了上面方法中configurations = removeDuplicates(configurations)一句代码执行后configurations集合的情况(注意集合中已经不包含KafkaAutoConfiguration类了):
通过上面的梳理我了解了第三方组件的自动配置类是如何加载到Spring容器中的,但这些配置类的解析过程是怎样的?另外这个配置类是否需要加载的具体判断逻辑究竟是怎样的呢?下篇文章我们会对这写问题进行梳理。