SpringBoot自动装配的原理

2023-03-21 21:24:26 浏览数 (1)

前言

自动装配是SpringBoot的核心,从Spring的起源来说,一开始是通过XML文件对Bean进行装载,后来使用JavaConfig的方式实现无配置化的解决方案,但是本质问题并没有得到解决,直到SpringBoot的问世,帮开发者省略了繁琐的基础性的工作,是开发者更加关注于逻辑本身,SpringBoot将繁琐的基础性工作交给了Starter组件和自动装配去做。

自动装配的原理

自动装配的实现

当我们新建一个SpringBoot项目时,启动类上只有一个注解(如下),我们点进去这个注解

代码语言:javascript复制
@SpringBootApplication
public class ElmSpringApplication {

    public static void main(String[] args) {
        SpringApplication.run(ElmSpringApplication.class, args);
    }

}

当我们点进来后(如下),SpringBoot官方的开发人员也对这个注解进行了说明,简单来说@SpringBootApplication就是@Configuration, @EnableAutoConfiguration@ComponentScan的集合,下面我们分别说下这几个注解的作用:

  • @Target:用来表示注解的作用范围
  • @Retention:指定它所修饰注解的保留策略,也就是该注解的生命周期
  • @Documented:标记注解,可以被例如javadoc此类的工具文档化
  • @Inherited:用于标注一个父类的注解是否可以被子类继承
  • @SpringBootConfiguration:继承自@Configuration,允许在上下文中注册额外的 bean 或导入其他配置类
  • @EnableAutoConfiguration:开启SpringBoot的自动装载机制
    • @Enable:Spring3.1就已经支持@Enable注解了,他的作用是把相关组件的Bean装配到IoC容器中,如果基于JavaConfig的形式完成Bean的装载就必须使用@Configuration@Bean,而@Enable本质上就是针对这两个注解的封装
  • @ComponentScan:扫描包下的类中添加了@Component (@Service@Controller@Repostory@RestController)注解的类 ,并添加的到spring的容器中,可以自定义不扫描某些 bean
代码语言:javascript复制
/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @author Andy Wilkinson
 * @since 1.2.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

	
	@AliasFor(annotation = EnableAutoConfiguration.class)
	Class<?>[] exclude() default {};

	
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

	
	@AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
	Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

	
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;

}

EnableAutoConfiguration

进入@EnableAutoConfiguration注解(如下),可以看到除了我们在上面所说的注解外还多出了两个注解:@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class),这两个注解在这的作用是:

  • @AutoConfigurationPackage:把使用了该注解所在包及子包下的所有组件扫描到Spring IoC容器中
  • @Import(AutoConfigurationImportSelector.class):一般情况下,@Import导入的应该是一个Configuration配置类,但在这里导入的是一个AutoConfigurationImportSelector类,我会在下文中详细介绍此类,但知道的是,无论此类是什么,它一定会实现配置类的导入
代码语言:javascript复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {


	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	
	Class<?>[] exclude() default {};

	
	String[] excludeName() default {};

}

AutoConfigurationImportSelector

AutoConfigurationImportSelector类实现了ImportSelector接口(关系如下),它只有一个selectImports抽象方法,并且返回一个String数组,在这个数组中可以指定装配到IoC容器的类,当@Import导入ImportSelector的实现类后,就会把该方法返回的String数组里的类名的类都装载到IoC容器里。和@Configuration不同的是,这种方法可以实现自动装配,也就是根据上下文来决定哪些类能够被IoC容器初始化。

代码语言:javascript复制
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
                 .............
                 .............
}
代码语言:javascript复制
public interface DeferredImportSelector extends ImportSelector {
                 .............
                 .............
}
代码语言:javascript复制
public interface ImportSelector {
    String[] selectImports(AnnotationMetadata importingClassMetadata);

    @Nullable
    default Predicate<String> getExclusionFilter() {
        return null;
    }
}

简单使用ImportSelector的例子

创建两个类Codeone.classCodetwo.class

代码语言:javascript复制
public class Codeone {
}

public class Codetwo {
}

创建ImportSelector的实现类ImportSelectorImpl.class

代码语言:javascript复制
public class ImportSelectorImpl implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{Codeone.class.getName(),Codetwo.class.getName()};
    }
}

自定义注解EnableAutoImport

代码语言:javascript复制
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(ImportSelectorImpl.class)
public @interface EnableAutoImport {
}

在启动类上添加注解并进行测试

代码语言:javascript复制
@SpringBootApplication
@EnableAutoImport
public class ElmSpringApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(ElmSpringApplication.class, args);
        Codeone codeone=run.getBean(Codeone.class);
    }

}

也可以在ImportSelector的实现类中把Codeone.classCodetwo.class变为xxxConfiguration.class,就是把具体的类变为Configuration类,实现批量导入

自动装配的原理分析

根据上面的分析我们已经知道了是ImportSelector通过导入Configuration类从而完成Bean的装载过程,那么这些Configuration是从哪里获取的呢?是怎样获取的呢?这是下面我们要分析的问题,上文中我们提到了AutoConfigurationImportSelector类,当时我们只分析了selectImports方法的作用,没分析具体的代码,而要想知道以上的两个问题的答案就要继续分析其代码,我把代码贴在下面:

代码语言:javascript复制
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
                               .......
                               .......
                               .......
        @Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
                               .......
                               .......
                               .......
}

我们可以看到以上selectImports方法中getAutoConfigurationEntry方法,这个方法也是在该类中,下面我们找到getAutoConfigurationEntry方法的具体实现

代码语言:javascript复制
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
                               .......
                               .......
                               .......
       protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
                //检测自动装配是否开启 可在yml中通过spring.boot.enableautoconfiguration=true开启,默认开启
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
                //获取@EnableAutoConfiguration注解中exclude和excludeName
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
                //获得所有自动装配的配置类,后面会继续分析
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
                //去除重复的配置项,防止多个项目引入同样的配置类
		configurations = removeDuplicates(configurations);
                //根据@EnableAutoConfiguration注解中exclude等属性,把不需要自动装配的配置类移除
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
                //检查被排除类是否可实例化,是否被自动注册配置所使用,不符合条件则抛出异常
		checkExcludedClasses(configurations, exclusions);
                //从自动配置类集合中取出被排除的类
		configurations.removeAll(exclusions);
                //检查配置类的注解是否符合 spring.factories 文件中 AutoConfigurationImportFilter 指定的注解检查条件
		configurations = getConfigurationClassFilter().filter(configurations);
                //广播事件,所有监听 AutoConfigurationImportEvent 事件的都会做出相关的反应
		fireAutoConfigurationImportEvents(configurations, exclusions);
                //创建自动配置的对象
		return new AutoConfigurationEntry(configurations, exclusions);
	}
                               .......
                               .......
                               .......
}

以上源码都进行了注释,我们只看其中的核心,也就是getCandidateConfigurations这个方法,他是获得配置类最核心的方法,下面我们看他的代码

代码语言:javascript复制
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
                               .......
                               .......
                               .......
        protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) 
       {
		List<String> configurations = new ArrayList<>(
				SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), 
                                getBeanClassLoader()));
		ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
		Assert.notEmpty(configurations,
				"No auto configuration classes found in META-INF/spring.factories nor in META- 
                                 INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
				   "are using a custom packaging, make sure that file is correct.");
		return configurations;
	}
                               .......
                               .......
                               .......
}

通过上面的代码,我们看到代码中用了SpringFactoriesLoader,它是Spring内部提供的一种约定俗成的加载方式,类似于JAVA中的SPI,简单来说,他会扫描classpath下的META-INF/spring.factories文件,spring.factories文件中的数据以key=value的形式存储,而SpringFactoriesLoader.loadFactoryNames会根据key得到对应的value值,因此在这个场景中key对应为EnableAutoConfiguration,value是多个配置类,也就是getCandidateConfigurations的返回值,我们通过debug获取它的返回值,如下图所示:

打开RabbitAutoConfiguration,代码如下

代码语言:javascript复制
@AutoConfiguration
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import({ RabbitAnnotationDrivenConfiguration.class, RabbitStreamConfiguration.class })
public class RabbitAutoConfiguration {
                      ....................
                      ....................
                      ....................
}

除了基本的@Configuration注解,还有一个@ConditionalOnClass注解,这个条件控制机制在这里的用途是,判断classpath下是否存在着RabbitTemplateChannel这两个类,如果是,则把当前配置类注册到IoC容器中。另外,@EnableConfigurationProperties是属性配置,也就是说我们可以按照约定在application.yml中配置RabbitMQ的参数,而这些配置会加载到RabbitProperties中。

@Conditional条件装配

@Conditional是Spring提供的一个核心注解,这个注解的作用是提供自动装配的条件约束,一般与@Configuration@Bean配合使用

简单来说,Spring在解析@Configuration配置类时,如果该配置类增加了@Conditional注解,那么就会根据该注解配置的条件来决定是否要实现Bean的装配

@Conditional的注解类声明代码如下,该注解可以接收一个Condition类型数组

代码语言:javascript复制
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

Condition是一个函数式接口,提供了matchs方法,它主要提供一个匹配规则,返回true则装载Bean反之不装载

代码语言:javascript复制
@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);
}

基于@Conditional实现一个条件装配案例

自定义一个Condition,规则是如果当前是windows系统则返回true,反之则返回false

代码语言:javascript复制
public class ConditionImpl implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String os=context.getEnvironment().getProperty("os.name");
        if(os.contains("Windows")){
            return true;
        }
        return false;
    }
}

创建一个配置类,装载一个自定义类Codeone,在Codeone的Bean声明方法中增加@Conditional(ConditionImpl.class)注解,如果ConditionImpl.class中的matchs方法返回true则Codeone装载到Spring IoC容器中,反之则不装载

代码语言:javascript复制
@Configurable
public class ConditionConfig {

    @Bean
    @Conditional(ConditionImpl.class)
    public Codeone codeone(){
        return new Codeone();
    }
}

运行测试,输出结果

代码语言:javascript复制
@SpringBootTest
class ElmSpringApplicationTests {

    @Test
    void contextLoads() {
        AnnotationConfigApplicationContext annotationConfigApplicationContext=new AnnotationConfigApplicationContext(ConditionConfig.class);
        Codeone codeone=annotationConfigApplicationContext.getBean(Codeone.class);
        System.out.println(codeone);
    }

}

SpringBoot中的@Conditional

在SpringBoot中,针对@Conditional做了扩展,提供了更简单的使用形式,扩展注解如下:

  • @ConditionOnBean/@ConditionOnMissingBean:容器中存在某个类或者不存在某个Bean时进行Bean的装载
  • @ConditionOnClass/@ConditionOnMissingClass:classpath下存在指定类或者不存在指定类时进行Bean的装载
  • @ConditionOnCloudPlatform:只用运行在指定的云平台上才加载指定的Bean
  • @ConditionOnExpression:基于SpEI表达式的条件判断
  • @ConditionOnJava:只用运行指定版本的Java才会加载Bean
  • @ConditionOnJndi:只有指定的资源通过JNDI加载后才加载Bean
  • @ConditionOnWebApplication/@ConditionOnNotWebApplication:如果是Web应用或者不是Web应用,才加载指定的Bean
  • @ConditionOnProperty:系统中指定的对应的属性是否有对应的值
  • @ConditionOnResource:classpath中是否存在要加载的Bean依赖的资源
  • @ConditionOnSingleCandidate:只有在确定了给定Bean类的单个候选项时才会加载Bean

spring-autoconfigure-metadata

它的作用和@Conditional是一样的,只是将这些条件配置放进了配置文件里

代码语言:javascript复制
org.springframework.boot.autoconfigure.AutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitAnnotationDrivenConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.annotation.EnableRabbit
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$MessagingTemplateConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration$MessagingTemplateConfiguration.ConditionalOnClass=org.springframework.amqp.rabbit.core.RabbitMessagingTemplate
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration.ConditionalOnClass=com.rabbitmq.client.Channel,org.springframework.amqp.rabbit.core.RabbitTemplate
org.springframework.boot.autoconfigure.amqp.RabbitStreamConfiguration=
org.springframework.boot.autoconfigure.amqp.RabbitStreamConfiguration.ConditionalOnClass=org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration.ConditionalOnClass=org.aspectj.weaver.Advice
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration=
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration=
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration$DataSourceInitializerConfiguration=
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration$DataSourceInitializerConfiguration.ConditionalOnBean=javax.sql.DataSource
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration$DataSourceInitializerConfiguration.ConditionalOnClass=org.springframework.jdbc.datasource.init.DatabasePopulator
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration.AutoConfigureAfter=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration.ConditionalOnBean=org.springframework.batch.core.launch.JobLauncher
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration.ConditionalOnClass=javax.sql.DataSource,org.springframework.batch.core.launch.JobLauncher
org.springframework.boot.autoconfigure.batch.BatchConfigurerConfiguration=

这种形式也是约定大于配置的体现,通过这种配置实现条件过滤必须要遵循两个条件:

  • 配置文件的路径和名称必须是META-INFspring-autoconfigure-metadata.properties
  • 配置文件中key的配置格式:自动配置类的类全路径名.条件=值

这种配置方式有效的降低了SpringBoot的启动时间,减少配置类的加载数量

温馨提示:本文最后更新于2023-03-14,若文件或内容有错误或已失效,请在下方留言。

0 人点赞