SpringBoot源码解读与原理分析(二)组件装配

SpringBoot源码解读与原理分析(合集)

2.1 组件装配

2.1.1 组件

组件:IOC容器中的核心API对象
组件装配:将核心API配置到XML配置文件或注解配置类的行为
Spring Framework 只有一种组件装配方式,即手动装配;而 Spring Boot 基于原生的手动装配,通过模块装配+条件装配+SPI机制,完美实现组件的自动装配。

2.1.2 手动装配

手动装配,是指开发者在项目中通过编写XML配置文件、注解配置类、配合特定注解等方式,将所需的组件注册到IOC容器(即ApplicationContext)中。
三种手动装配方式(共性:需要手动编写配置信息):

<!-- 基于XML配置文件的手动配置 -->
<bean id="person" class="com.xiaowd.springboot.component.Person"/>

// 基于注解配置类的手动装配
@Configuration
public class ExampleConfiguration {
    @Bean
    public Person person() {
        return new Person();
    }
}

// 基于组件扫描的手动装配
@Component
public class DemoService {
}
@Configuration
@ComponentScan("com.xiaowd.springboot")
public class ExampleConfiguration {
}

2.1.3 自动装配

自动装配是 Spring Boot 的核心特性之一。
自动装配:本应该由开发者编写的配置,转为框架自动根据项目中整合的场景依赖,合理地做出判断并装配合适的Bean到IOC容器中。相比较于手动装配,自动装配关注的重点是整合的场景,而不是每个具体的场景中所需的组件。

  • 实现机制:模块装配+条件装配+SPI机制
  • 非侵入性:默认注册的组件可以被覆盖。如整个spring-jdbc时,如果项目中已经注册了JdbcTemplate,则SpringBoot提供的默认的JdbcTemplate就不会再创建。
  • 配置禁用:在@SpringBootApplication或者@EnableAutoConfiguration注解上标注exclude/excludeName属性,可以禁用默认的自动配置类;或者在全局配置文件中声明spring.autoconfigure.exclude属性。

2.2 Spring Framework的模块装配

模块装配是自动装配的核心,可以把一个模块所需的核心功能组件都装配到IOC容器中。
通过标注@EnableXXX注解,实现快速激活和装配对应的模块

2.2.1 模块

  • 独立的:一个个可以分解、组合、更换的独立单元
  • 功能高内聚:一个模块通常用于解决一个独立的问题
  • 可相互依赖:模块间
  • 目标明确

2.2.2 模块装配举例

模块装配的核心原则:自定义注解+@Import导入组件

1.模块装配场景

使用代码模拟构建一个酒馆,酒馆里有吧台、调酒师、服务员和老板4种不同的实体元素;酒馆可以看成IOC容器,4种不同的实体元素可以看成4个组件。
目的:通过一个注解,把以上元素全部填充到酒馆中。

2.声明自定义注解@EnableTavern

@Documented
@Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
@Target(ElementType.TYPE) // 该注解只能标注到类上
public @interface EnableTavern {
}

3.声明老板类Boss

public class Boss {
}

4.在@EnableTavern增加@Import注解

@Import注解源码如下:


由源码可知,@Import注解可以导入配置类、ImportSelector的实现类、ImportBeanDefinitionRegistrar的实现类,以及普通类。
接下来在@EnableTavern的@Import注解中填入Boss类,这就意味着如果一个配置类上标注了@EnableTavern注解,就会触发@Import的效果,向容器中导入一个Boss类的Bean。

@Documented
@Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import(Boss.class)
public @interface EnableTavern {

}

5.创建配置类

@Configuration
@EnableTavern
public class TavernConfiguration {
}

6.编写启动类测试

public class TavernApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
        Boss boss = ctx.getBean(Boss.class);
        System.out.println(boss);
    }

}

运行结果显示,使用getBean可以正常获取Boss对象,说明Boss类已经被注册到了IOC容器,并创建了一个对象。

2.2.3 导入配置类

1.声明调酒师类

public class Bartender {
    
    private String name;

    public Bartender(String name) {
        this.name = name;
    }

    // getter and setter
}

2.声明注解配置类

@Configuration
public class BartenderConfiguration {
    
    @Bean
    public Bartender zhangsan() {
        return new Bartender("张三");
    }

    @Bean
    public Bartender lisi() {
        return new Bartender("李四");
    }
    
}

3.在@EnableTavern注解中添加BartenderConfiguration配置类

@Documented
@Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import({Boss.class, BartenderConfiguration.class})
public @interface EnableTavern {

}

4.测试运行

public class TavernApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
        Map<String, Bartender> bartenders = ctx.getBeansOfType(Bartender.class);
        bartenders.forEach((name, bartender) -> System.out.println(name, bartender));
    }

}


运行结果显示,两个调酒师对象已经注册到了IOC容器。
注意:
配置类@Configuration还可以被组件扫描(ComponentScan)识别到,如果配置了组件扫描,不使用@Import导入配置类也可以在IOC容器中找到相应的组件。另外,本例中BartenderConfiguration本身也被注册到了IOC容器中成为一个Bean。

2.2.4 导入ImportSelector实现类

1.ImportSelector源码

Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes.
ImportSelector是一个接口,它的实现类可以根据指定的筛选标准(通常是一个或多个注解)来决定那些配置类被导入。
被ImportSelector导入的类,最终会在IOC容器中以单实例Bean的形式创建并保存。

2.声明吧台类

public class Bar {
}

3.声明配置类

@Configuration
public class BarConfiguration {
    @Bean
    public Bar bar() {
        return new Bar();
    }
}

4.编写ImportSelector的实现类

public class BarImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
    }

}

selectImports方法源码:

Select and return the names of which class(es) should be imported based on the AnnotationMetadata of the importing @Configuration class.
Returns: the class names, or an empty array if none
根据导入的@Configuration类的注解元数据AnnotationMetadata选择并返回要导入的类的类名。
注意:返回的一组类名一定是全限定类名(可直接定位)

5.在@EnableTavern注解中添加BarImportSelector

@Documented
@Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class})
public @interface EnableTavern {

}

6.测试运行

public class TavernApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
        Map<String, Bar> bars = ctx.getBeansOfType(Bar.class);
        bars.forEach((name, bar) -> System.out.println(name));
        System.out.println("=======");
        Map<String, BarConfiguration> barConfigurations = ctx.getBeansOfType(BarConfiguration.class);
        barConfigurations.forEach((name, barConfiguration) -> System.out.println(name));
        System.out.println("=======");
        Map<String, BarImportSelector> barImportSelectors = ctx.getBeansOfType(BarImportSelector.class);
        barImportSelectors.forEach((name, barImportSelector) -> System.out.println(name));
        System.out.println("=======");
    }

}

运行结果显示:
ImportSelector可以导入普通类(Bar),可以导入配置类(BarConfiguration),但没有导入BarImportSelector。

7.ImportSelector的灵活性

  • ImportSelector的核心是可以使开发者采用更灵活的声明式向IOC容器注册Bean,其重点是可以灵活地注定要注册的Bean的类。
  • 如果传入的全限定名以配置文件的形式存放在项目可以读取的位置,则可以避免组件导入的硬编码问题。
  • 在SpringBoot的自动装配中,底层就是利用了ImportSelector,实现从spring.factories文件中读取自动配置类。

2.2.5 导入ImportBeanDefinitionRegistrar

以编程式向IOC容器中注册bean对象

1.声明服务员类

public class Waiter {
}

2.编写ImportBeanDefinitionRegistrar的实现类

public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        registry.registerBeanDefinition("waiter222", new RootBeanDefinition(Waiter.class));
    }
    
}

第一个参数是Bean的名称(即ID)
第二个参数传入的RootBeanDefinition要指定Bean的字节码
这种方式相当于向IOC容器注册了一个普通的单实例bean(最终效果与组件扫描、@Bean注解的效果相同)

3.在@EnableTavern注解中添加WaiterRegistrar

@Documented
@Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class})
public @interface EnableTavern {

}

4.测试运行

public class TavernApplication {

    public static void main(String[] args) {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(TavernConfiguration.class);
        Map<String, Waiter> waiters = ctx.getBeansOfType(Waiter.class);
        waiters.forEach((name, waiter) -> System.out.println(name));
        System.out.println("=======");
        Map<String, WaiterRegistrar> waiterRegistrars = ctx.getBeansOfType(WaiterRegistrar.class);
        waiterRegistrars.forEach((name, waiterRegistrar) -> System.out.println(name));
        System.out.println("=======");
    }

}

结果显示:服务员对象成功注册,WaiterRegistrar不会注册。

2.2.6 扩展:DeferredImportSelector

ImportSelector的子接口DeferredImportSelector,类似于ImportSelector,但执行时机比ImportSelector晚。
ImportSelector:在注解配置类的解析期间,此时配置类中的Bean方法还没有被解析
DeferredImportSelector:在注解配置类的解析完成之后
目的:配合条件装配(后面再深入)

1.编写WaiterDeferredImportSelector类

public class WaiterDeferredImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("DeferredImportSelector执行了...");
        return new String[] {Waiter.class.getName()};
    }
    
}

2.ImportSelector和ImportBeanDefinitionRegistrar也加上执行提示语

public class BarImportSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("ImportSelector执行了...");
        return new String[] {Bar.class.getName(), BarConfiguration.class.getName()};
    }

}
public class WaiterRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        System.out.println("ImportBeanDefinitionRegistrar执行了...");
        registry.registerBeanDefinition("waiter222", new RootBeanDefinition(Waiter.class));
    }

}

3.在@EnableTavern注解中添加WaiterDeferredImportSelector

@Documented
@Retention(RetentionPolicy.RUNTIME) //该注解在运行时起效
@Target(ElementType.TYPE) // 该注解只能标注到类上
@Import({Boss.class, BartenderConfiguration.class, BarImportSelector.class, WaiterRegistrar.class, WaiterDeferredImportSelector.class})
public @interface EnableTavern {

}

4.运行测试


DeferredImportSelector的运行时机比ImportSelector晚,但比ImportBeanDefinitionRegistrar早(这样设计的原理放到后面)。
另外,DeferredImportSelector还有分组的概念(DeferredImportSelector有一个方法getImportGroup),可以对不同的DeferredImportSelector加以区分(SpringBoot使用非常少,知道即可)。
SpringBoot源码解读与原理分析(合集)

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

推荐阅读更多精彩内容