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

前言

  • 为什么导入WebMvc场景启动器后,即使没有编写任何配置代码,应用也可以正常启动?
  • Spring Boot 如何确定引入的技术场景中需要哪些重要组件?
  • 为什么项目在没有配置任何Web容器的情况下也可以正常启动Web服务?

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 导入配置类

@Import注解导入普通类是最简单的方式,还可以导入配置类。

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源码解读与原理分析

  • 32
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值