Spring Boot 自动装配的原理

Spring Boot 自动装配的原理,第1张

在Spring Boot中,不得不说的一个点是自动装配,它是Starter的基础,也是Spring Boot的核心,那么什么叫自动装配呢?或者说什么叫装配呢?

简单来说,就是自动将Bean装配到IoC容器中,接下来,我们通过一个Spring Boot整合Redis的例子来了解一下自动装配。

  • 添加Starter依赖:

    org.springframework.boot
    spring-boot-starter-data-redis
  • 在application.properties中配置Redis的数据源:

spring.redis.host=localhost

spring.redis.port=6379

  • 在HelloController中使用RedisTemplate实现Redis的 *** 作:
@RestController
public class HelloController {
    @Autowired
    RedisTemplate redisTemplate;

    @GetMapping("/hello")
    public String hello() {
        redisTemplate.opsFousValue().set("key", "value");
        return "Hello World";
    }

}

在这个案例中,我们并没有通过XML形式或者注解形式把RedisTemplate注入IoC容器中,但是在HelloController中却可以直接使用@Autowired来注入redisTemplate实例,这就说明,IoC容器中已经存在RedisTemplate。这就是SpringBoot的自动配置机制。

在往下探究其原理前,可以大胆猜测一下,如何只添加一个Starter依赖,就能完成该依赖组件相关Bean的自动注入?不难猜出,这个机制的实现一定基于某种约定或者规范,只要Starter组件符合SpringBoot中自动装配约定的规范,就能实现自动装配。

自动装配的实现

自动装配在SpringBoot中是通过@EnableAutoConfiguration注解来开启的,这个注解的声明在启动类注解@SpringBootApplication内。

@SpringBootApplication
public class SpringBootDemoApplication {
    public static void main(Sring[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

进入@SpringBootApplication注解,可以看到@EnableAutoConfiguration注解的声明。

@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 {
}

这里简单讲解一下@Enable注解。其实Spring3.1版本已经支持@Enable注解了,它的主要作用把相关组件Bean装配到IoC容器中。@Enable注解对JavaConfg 的进一步完善,为使用Spring Framework的开发者减少配置代码量,降低了使用的难度,比如场景的@Enable注解有@EnableWebMvc、@EnableScheduling等。

如果基于JavaConfig的形式来完成Bean的装载,则必须要使用@Configuration注解及@Bean注解。而@Enable本质上就是针对这两个注解的封装,所以大家如果仔细关注这些注解,就不难发现这些注解中都会携带一个@Import注解,比如@EnableScheduling

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({SchedulingConfiguration.class})
@Documented
public @interface EnableScheduling {
}

因此,使用@Enable注解后,Spring会解析到@Import导入的配置类,从而根据这个配置类中的描述来实现Bean的装配。

思考一下,@EnableAutoConfiguration的实现原理是不是也一样呢?

EnableAutoConfiguration

进入@EnalbeAutoConfiguration注解里,可以看到除@Import注解之外,还多了一个@AutoConfigurationPackage注解(它的作用是把使用了该注解的类所在的包及子包下所有的组件扫描到Spring IoC容器中)。并且,@Import注解中导入的并不是一个Configuration的配置类,而是一个AutoConfigurationSelector类。从一点来看,它就和其他的@Enable注解有很大的不同。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration

不过,不管AutoConfigurationImportSelector是什么,它一定会实现配置类的导入,至于导入的方式和普通的@Configuration有什么区别,这就是我们需要去分析的

AutoConfigurationImportSelector

AutoConfigurationImportSelector实现了ImportSelector,它只有一个selectImports抽象方法,并且返回一个String数组,在这个数组中可以指定需要装配到IoC容器的类,当在@Import中导入一个ImportSelector的实现类之后,会把该实现类返回的class名称都装载到IoC容器中。

public interface ImportSelector {
    Sring[] selectImports(AnnotationMetadata var1);
}

和@Configuration不同的是,ImportSelector可以实现批量装配,并且还可以通过逻辑处理来实现Bean的选择性装配,也就是可以根据上下文来觉得哪些类能够被IoC容器初始化。接下来通过一个简单例子带大家了解ImportSelector的使用。

  • 首先创建两个类,我们需要把这两个类装配到IoC容器中。

        public class FirstClass {

        }

        public class SecondClass {

        }

  • 创建一个ImportSelector的实现类,在实现类中把定义的两个Bean加入Spring数组,这意味着两个Bean会装配到IoC容器中。

        public class GpImportSelector implements ImportSelector {

                @Override

                public String[] selectImports(AnnotationMetadata importClassMetadata ) {

                        return new String[] {FirstClass.class.getName(),SecondClass.class.getName()};

                }

        }

  • 为了模拟EnableAutoConfiguration,我们可以自定义一个类似的注解,通过@Import导入GpImportSelector
@Target()
@Retention()
@Documented
@Inherited
@AutoConfigurationPackage
@Import(GpImportSelector.class)
public @interface EnableAutoImport {

}

  • 创建一个启动类,在启动类上使用@EnableAutoImport注解后,即可通过ca.getBean从IoC容器中得到FirstClass对象实例。
@SpringBootApplication
@EnableAutoImport
public class ImportSelectorMain(String[] args) {
    public void static main(String[] args) {
        ConfigurableApplicationContext ca = SpringApplication.run(ImportSelectorMain.class, args);
        FirstClass fc = ca.getBean(FirstClass.class);
    }
}

这种实现方式相比@Import(*Configuration.class)的好处在于装配的灵活性,还可以实现批量,比如GpImportSelector还可以直接在Spring数组中定义多个Configuration类,由于一个配置类代表的是某一个技术组件中批量的Bean声明,所以在自动装配这个过程中只需要扫描到指定路径下对应的配置类即可。

public class GpImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMeatadata) {
        return new String[] {FirstConfiguration.class.getName(), SecondConfiguration.class.getName()};
    }
}

自动装配原理分析

基于前面章节的分析可以猜想到,自动装配的核心是扫描约定目录下的文件进行解析,解析完成之后把得到的Configuration配置类通过ImportSelector进行导入,从而完成Bean的自动装配过程,那么接下来我们通过分析AutoConfigurationImportSelector的实现来求证这个猜想是否正确。

定位到AutoConfigurationImportSelector中的selectImports方法,它是ImportSelector接口的实现,这个方法中主要是有两个功能:

  • AutoConfigurationMetadataLoader.loadMetadata从META-INF/spring-autoconfigure-metadata.properties中加载自动装配的条件元数据,简单来说就是只有满足条件的Bean才能够进行装配。
  • 收集所有符合条件的配置类 autoConfigurationEntry.getConfigurations(),完成自动装配
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMedata)) {
        return NO_IMPORTS;
    }
    AntoConfigurationMetadata autoConfigurationMedadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
    AutoConfigurationEntry autoConfiguration = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

需要注意的是,在AutoConfigurationImportSelector中不执行selectImports方法,而是通过ConfigurationClassPostProcessor中processConfigBeanDefinitions方法来扫描和注册所有配置类的Bean,最终还是会调用getAutoConfigurationEntry方法获得所有需要自动装配的配置类。

我们重点分析一下配置类的收集方法getAutoConfigurationEntry,结合之后Starter的作用不难猜测到,这个方法应该会扫描指定路径下文件解析得到需要装配的装配类,而这里面用到了SpringFactoriesLoader,这块内容后续随着代码的展开再来讲解。简单分析一下这段代码,它主要是做几件事情:

  • getAttributes获得@EnableAutoConfiguration注解中的属性exclude、excludeName等等
  • getCandidateConfigurations获得所有自动装配的配置类,后续会重点分析
  • removeDuplicates去除重复的配置项
  • getExclusions根据@EnableAutoConfiguration注解中配置的exclude等属性,把不需要自动装配的配置类移除
  • fireAutoConfigurationImportEvents广播事件
  • 最后返回经过多层判断和过滤之后的配置类集合
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMedadata, AnnotationMetadata annotationMetadata) {
    if(!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    List configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = filter(configurations, antoConfigurationsMetadata);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

总的来说,它先获得所有的配置类,通过去重,exclude排除等 *** 作,得到最终需要实现自动装配的配置类。这里需要重点关注的是getCandidateConfigurations,它是获得配置类最核心的方法。

protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    List configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
    Asset.notEmpty(configurations, "No auto configuration class found in META-INF/spring.factories. 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形式存储,而SprinFactoriesLoader.loadFactoryNames会根据Key得到对应的Value值。因此,在这个场景中,Key对应为EnableAutoConfigurations,Value是多个配置类,也就是getCandidateConfigurations方法所返回的值。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
//省略

打开RabbitAutoConfiguration,可以看出,它就是一个基于JavaConfig形式的配置类。

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration

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

至此,自动装配的原理基本上就分析完了,简单来总结一下核心过程:

  • 通过@Import(AutoConfigurationImportSelector)实现配置类的导入,但是这里并不是传统意义上的单个配置类装配。
  • AutoConfigurationImportSelector类实现了ImportSelector接口,重写了方法selectImports,它用于实现选择性批量配置类的装配
  • 通过Spring提供的SpringFactoriesLoader机制,扫描classpath路径下的META-INF/spring.factories,读取需要实现自动配置的配置类。
  • 通过条件筛选的方式,把不符合条件的配置类移除,最终完成自动装配。

@Conditional条件装配

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

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

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

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Document
public @interface Conditional {
    Class[] value();
}

Condition是一个函数接口,提供了matches方法,它主要提供一个条件匹配规则,返回true

表示可以注入Bean,反之则不注入。

@FunctionInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

接下来基于@Conditional实现一个条件装配的案例:

  • 自定义一个Condition,逻辑很简单,如果当前 *** 作系统是Windows,则返回true,否则返回false
public class GpCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
        //此处进行条件判断,如果返回true,表示需要加载该配置类的或者Bean
        //否则,表示不加载
        String os = conditionContext.getEnvironment().getProperty("os.name");
        if(os.contains("Windows")) {
            return true;
        }
        return false;
    }
}
  • 创建一个配置类,装载一个BeanClass(自定义一个类)
@Configuration
public class ConditionConfig {
    
    @Bean
    @Conditional(GpCondition.class)
    public BeanClass beanClass(){
        return new BeanClass();
    }
}

        在BeanClass的bean声明方法增加@Condition(GpCondition.class),其中具体的条件是我们自定义的GpCondition类。

        上述代码所表达的意思是,如果GpCondition类的matches返回true,则将BeanClass装载到Spring IoC 容器中

  • 运行测试方法
public class ConditionMain {
    
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(ConditionConfig.class);
        BeanClass beanClass = context.getBean(BeanClass.class);
        System.out.println(breanClass);
    }


}

在Windows环境运行,将会输出BeanClass这个对象实际。在Linux环境中,会出现如下错误

Exception is thread "main" org.springfreamework.beans.factory.NoSuchBeanDefintionException;
No qualifying bean of type 'com.xxxxxx.springboot.condition.BeanClass' available

以上就说@conditional注解的使用方法,为Bean的装载提供了上下文的判断

Spring Boot 中的@Conditional

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

  • ConditionalOnBean / ConditionalOnMissingBean :容器中存在某个类或者不存在某在Bean时进行Bean装载
  • ConditionOnClass / ConditionalOnMissingClass:classpath 下存在指定类或者不存在指定类进行Bean装载
  • ConditionalOnCloudPlatform:只有运行在指定的云平台上才加载指定的Bean
  • ConditionalOnExpression:基于SpEl 表达式的条件判断
  • ConditionalOnJava:只有指定的版本的Java才会在Bean
  • ConditionalOnJndi:只有指定的资源通过JNDI加载后加载Bean
  • ConditionalOnWebApplication/ConditionalOnNotWebApplication:如果是Web应用或者不是Web应用,才加载指定的Bean
  • ConditionalOnProperty:系统中指定的对应的属性是否有对应的值
  • ConditionalOnResource:要加载的Bean依赖指定资源是否存在classpath中
  • ConditionalOnSingleCandidate:只有在确定了给定Bean类的单个选项才会加载Bean

这些注解只需要添加到@Configuration配置类的类级别或者方法级别,然后根据每个注解的作用传参就行。下面演示几种注解类的使用。

@ConditionalOnProperty
@Condiguration
@ConditionalOnProperty(value="gp.bean.enable",havingValue="true",matchIfMissing=true)
public class ConditionConfig {
    
}

在application.properties 或 application.yml 文件中当gp.bean.enable=true时才会加载ConditionConfig这个Bean,如果没有匹配上也会加载,因为matchIfMissing = true ,默认是false

@ConditionalOnBean 和 ConditionalOnMissingBean

@Configuration
@ConfigurationOnBean(GpBean.class)
public class ConditionConfig {

}

        当Spring IoC 容器中存在GpBean时,才会加载ConditionConfig

@ConditionalOnResource
@Configuration
@ConditionalOnResource(resource="/gp.properties")
public class ConditionConfig {

}

        在classpath 中如果存在gp.properties,则会加载ConditionConfig

        在这些条件配置在 Spring Boot的自动装配配置类中出现的频率非常高,它能够很好地自动装配提供上下文条件判断,来让Spring决定是否装载该配置类

Spring-autoconfigure-metadata

除了@Conditional 注解类,在Spring Boot 中还提供了spring-autoconfigure-metadata.properties文件来实现批量自动装配条件配置

它的作用和@Conditional是一样的,只是将这些条件配置放在了配置文件中,下面这段配置来自spring-boot-autoconfigure.jar包中的/META-INF/spring-autoconfigure-metadata.properties文件

org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration.AutoConfigurationAfter=org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration.ConditionalOnClass=com.datastax.driver.core.Cluter,reactor.core.publishes.Flux,org.springframework.data.cassandra.core.ReactiveCassandraTemplate
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration.ConditionalOnClass=org.apache.solr.client.solrj.SolrClient,org.springframework.data.solr.repository.SolrRepository

要遵循两个条件:

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

这种配置方法的好处在于,它可以有效地降低Spring Boot 的启动时间,通过这种过滤方式可以减少配置类的加载数量,因为这个过滤发生在配置类的装载之前,所以它可以降低Spring Boot 启动时装载 Bean 的耗时

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/720817.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-25
下一篇 2022-04-25

发表评论

登录后才能评论

评论列表(0条)

保存