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();
}
}
基于接口驱动实现
- 实现
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
中做一些判断,根据需要返回不同的类名数组,然后再根据类名进行装配。
- 实现 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());
}
}
可以看到返回结果是通过autoConfigurationEntry
的getConfigurations()
获取的,所以我们直接看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.factories
spring boot还实现了SPI(Service Provide Interface)的机制。
条件装配
条件装配注解有两种:
Spring注解 | 场景说明 | 起始版本 |
---|---|---|
@Profile | 配置化条件装配 | 3.1 |
@Conditional | 编程条件装配 | 4.0 |
在4.0之后@profile也变成了@Conditional来实现
应用场景:一般用于生产环境和开发环境之间的切换,就是在类或者方法上添加注解并设置环境标识比如java7
、java8
,我们以一个简单的多整数求和来演示如何使用
@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实现条件装配
梳理一下整个流程:
- 使用
@EnableAutoConfiguration
激活自动装配,Spring Boot会去spring.factories
文件中解析需要自动装配的类HelloWorldAutoConfiguration
- 装配
HelloWorldAutoConfiguration
的时候会去判断是否满足装配要求,这里是jdk1.8的环境,所以是满足要求的 -
HelloWorldAutoConfiguration
是被@EnableHelloWorld
标注了,@EnableHelloWorld
通过@Import({HelloWorldImportSelector.class})
的方式引入了HelloWorldImportSelector
- 在
HelloWorldImportSelector
的selectImports
方法中返回了HelloWorldConfiguration
的类名 - 在
HelloWorldConfiguration
中加载helloWorld