文章目录
前言
- 为什么导入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源码解读与原理分析