Spring Boot自动装配的原理

Spring Boot不得不说的一个特点就是自动装配,它是starter的基础,也是spring boot的核心,那到底什么是自动装配呢?

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

  1. 添加starter
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 在application.properties中添加Redis的配置
spring.redis.host=localhost
spring.redis.port=6379
  1. 在Controller中使用redisTemplate对Redis进行操作
@RestController
public class RedisController {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @GetMapping("/test")
    public String test() {
        redisTemplate.opsForValue().set("test", "test demo");
        return "Test Demo";
    }
}

在上面例子中,我们并没有通过XML形式或者注解形式把RedisTemplate注入到IoC容器中,但是在RedisController中却可以直接使用@Autowired来注入redisTemplate实例,这就表明IoC容器中已经存在RedisTemplate实例了,这就是Spring Boot自动装配机制。

自动装配的实现

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

@SpringBootApplication
public class SpringBootDemoApplication {
    public static void main(String[] 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{

其实spring 3.1版本就已经开始支持@Enable注解了,它的主要作用是把相关组件的Bean装配到IoC容器中。@Enable注解对JavaConfig的进一步完善,使开发者减少了配置代码量,降低了使用难度,比较常见的Enable注解有@EnableWebMvc,@EnableScheduling等。

如果我们要基于JavaConfig的形式来完成Bean的装载,则必须要使用@注解及@Bean注解。@Enable注解本质上就是对这两种注解的封装,在@Enable注解中,一般都会带有一个@Import注解,比如@EnableScheduling注解:

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

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

EnableAutoConfiguration注解

当我们查看@EnableAutoConfiguration这个注解的时候,可以看到除了@Import注解之外,还有一个@AutoConfigurationPackage注解,而且@Import注解导入的并不是一个Configuration的配置类,而是AutoConfigurationImportSelector类,那AutoConfigurationImportSelector里面包含什么东西呢?

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

AutoConfigurationImportSelector

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

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

和@Confifguration不同的是,ImportSelector可以实现批量装配,而且可以通过逻辑处理来实现Bean的选择性装配,也就是可以根据上下文来决定哪些类可以被装配到IoC容器中,下面通过一个例子介绍下ImportSelector的使用:

  1. 首先创建两个类,我们要把这两个类装配到IoC容器中
public class FirstClass {
...
}
public class SecondClass {
...
}
  1. 创建ImportSelector的实现类,在实现类中把上面定义的两个类加入到数组中
public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {FirstClass.class.getName(), SecondClass.class.getName()}
    }
}
  1. 创建一个类似EnableAutoConfiguration的注解,通过@Import导入MyImportSelector
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({MyImportSelector.class})
public @interface MyAutoImport {
}
  1. 创建启动类,在启动类上使用MyAutoImport注解,然后通过ConfigurableApplicationContext获取FirstClass
@SpringBootApplication
@MyAutoImport
public class ImportSelectorDemo {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringBootApplication.run(ImportSelectorDemo.class);
        FirstClass fc = context.getBean(FirstClass.class);
    }
}

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

自动装配原理

自动装配的核心是扫描约定目录下的文件进行解析,解析完成之后把得到的Configuration配置类通过ImportSelector进行导入,进而完成Bean的自动装配过程。

我们看一下AutoConfigurationImportSelector中的selectImports方法,它是ImportSelector接口的实现,该方法中主要做了两件事:

  • AutoConfigurationMetadataLoader.loadMetadata方法从META-INF/spring-autoconfigure-metadata.properties文件中加载自动装配的条件元数据,也就是只有满足条件的Bean才会被装配
  • autoConfigurationEntry.getConfigurations()方法收集所有符合条件的配置类,进行自动装配
public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }

接下来我们了解下getAutoConfigurationEntry这个方法,这个方法会扫描指定路径下的文件进行解析,从而得到所需要装配的配置类,它主要做了下面几件事:

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

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

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
        Assert.notEmpty(configurations, "No auto configuration classes 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的形式存储,SpringFactoriesLoader.loadFactoryNames()会根据key的到对应的value值,因此,在自动装配这个场景中,key对应为EnableAutoConfiguration,value是多个配置类,也就是getCandidateConfigurations方法的返回值。

# Auto Configure
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,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
......

如果我们打开RabbitAutoConfiguration,可以看到它就是一个基于JavaConfig形式的配置类:

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

    @Configuration
    @ConditionalOnClass({RabbitMessagingTemplate.class})
    @ConditionalOnMissingBean({RabbitMessagingTemplate.class})
    @Import({RabbitAutoConfiguration.RabbitTemplateConfiguration.class})
    protected static class MessagingTemplateConfiguration {
        protected MessagingTemplateConfiguration() {
        }

        @Bean
        @ConditionalOnSingleCandidate(RabbitTemplate.class)
        public RabbitMessagingTemplate rabbitMessagingTemplate(RabbitTemplate rabbitTemplate) {
            return new RabbitMessagingTemplate(rabbitTemplate);
        }
   ......
 }

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

到此处,自动装配的工作流程就结束了,其实主要的核心过程是如下几点:

  • 通过@Import(AutoConfigurationImportSelector)实现配置类的导入
  • AutoConfigurationImportSelector类实现了ImportSelector接口,重写了方法selectImports,用于实现批量配置类的装载。
  • 通过spring提供的SpringFactoriesLoader机制扫描classpath下META-INF/spring.factories文件,读取需要实现自动装配的配置类。
  • 通过条件筛选,把不符合条件的配置类移除,最终完成自动装配。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345