spring boot原理-自动装配

spring boot原理-自动装配

什么是spring boot?

服务于spring框架的框架,约定由于配置,提供各种开箱即用的组件,内置Tomcat、模板引擎、提供默认application.properties。可以快速构建一个web应用。

我们如何在spring boot中动态装配bean?

基于注解驱动实现

先定义一个Bean:

@Configuration
public class HelloWorldConfiguration {
 
    @Bean
    public String helloWorld() { // 方法名即 Bean 名称
        return "Hello,World";
    }
}

然后再定义一个注解,使用@Import导入刚刚定义的bean:

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

完成上面两步之后,我们只要用@EnableHelloWorld标注在某个类上时,这个Bean就会加载到Spring容器中。

@EnableHelloWorld
public class EnableHelloWorldBootstrap {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHelloWorldBootstrap.class)
                .web(WebApplicationType.NONE)
                .run(args);
        // helloWorld Bean 是否存在
        String helloWorld =
                context.getBean("helloWorld", String.class);
        System.out.println("获取到的bean: " + hello);
        // 关闭上下文
        context.close();
    }
}

基于接口驱动实现

  1. 实现ImportSelector接口,实现它的selectImports方法,返回的是一个string类型的数组,数组里面存放的类名:
public class HelloWorldImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {HelloWorldConfiguration.class.getName()
        };   
  }

然后将上面定义的注解改一下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//@Import({HelloWorldConfiguration.class})  enable注解驱动的方式
@Import({HelloWorldImportSelector.class})  //接口编程的方式
public @interface EnableHelloWorld {
}

可以看到,基于接口的方式实现更加灵活,我们可以在selectImports中做一些判断,根据需要返回不同的类名数组,然后再根据类名进行装配。

  1. 实现 ImportBeanDefinitionRegistrar接口,重写 registerBeanDefinitions

Spring Boot自动装配核心

我们再看一下@EnableAutoConfiguration这个注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
}

再点进@AutoConfigurationPackage看一下

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

使用@Import来给Spring容器中导入一个组件 ,这里导入的是Registrar.class

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    Registrar() {
    }

    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
    }

    public Set<Object> determineImports(AnnotationMetadata metadata) {
        return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
    }
}

运行springboot项目,debug看一下,可以发现metadata是被@SpringBootApplication标注的类,再看看new PackageImport(metadata).getPackageName()的值:这个值就是扫描的包路径,也就是说,默认扫描的包路径是引导类所在的包以及子包。

接着我们再看EnableAutoConfiguration也使用了@import,导入了 AutoConfigurationImportSelector.class,从这个类的名字我们就可以知道,它指定是实现了ImportSelector接口,重写了selectImports方法,所以我们直接看selectImports:

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());
    }
}

可以看到返回结果是通过autoConfigurationEntrygetConfigurations()获取的,所以我们直接看getAutoConfigurationEntry方法:

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);
    }
}

同理,这里我们进入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;
}

最终加载的配置类会从META-INF/spring.factories中获取,也就是说,Spring Boot在启动的时候会从类路径下的META-INF/spring.factories文件中将指定的值作为配置类导入到容器中,也就实现了自动配置。通过META-INF/spring.factoriesspring boot还实现了SPI(Service Provide Interface)的机制。

条件装配

条件装配注解有两种:

Spring注解 场景说明 起始版本
@Profile 配置化条件装配 3.1
@Conditional 编程条件装配 4.0

在4.0之后@profile也变成了@Conditional来实现

应用场景:一般用于生产环境和开发环境之间的切换,就是在类或者方法上添加注解并设置环境标识比如java7java8,我们以一个简单的多整数求和来演示如何使用

@Profile实现条件装配

首先定义一个接口:

public interface CalCulateService {
    /**
     * 整数求和
     * @param args
     * @return
     */
    Integer sum(Integer... args);
}
复制代码

然后分别实现一个dev和test的求和方法。

首先是dev的实现:

/**
 * dev的方式实现求和
 */
@Profile("dev")
@Service
public class DevCalCulateServiceImpl implements CalCulateService {
    @Override
    public Integer sum(Integer... args) {
        int sum = 0;
        for (int i = 0; i < args.length; i++) {
            sum+=args[i];
        }
        return sum;
    }
}

然后是test的实现:

@Profile("test")
@Service
public class TestCalCulateServiceImpl implements CalCulateService {
    @Override
    public Integer sum(Integer... args) {
        int sum = Stream.of(args).reduce(0,Integer::sum);
        return sum;
    }
}
复制代码

然后创建一个启动类,在启动容器的时候,使用.profiles("test")来指定使用哪个版本来计算:

@SpringBootApplication(scanBasePackages = "com.lingxiao.springboot.service")
public class CalCulateServiceBootstrap {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext =
                new SpringApplicationBuilder(CalCulateServiceBootstrap.class)
                        .web(WebApplicationType.NONE)
                        .profiles("test")
                        .run(args);
        CalCulateService calCulateService = applicationContext
                .getBean(CalCulateService.class);
        applicationContext.close();
    }
}

@Conditional实现条件装配

梳理一下整个流程:

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

推荐阅读更多精彩内容