文章目录
一、SpringBoot项目
// 这个注解实现了SpringBoot的包扫描及自动装配
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
// 这个run方法点进去会发现里面执行到的是spring中的refresh()方法
// 刷新ioc容器,进行bean的初始化
SpringApplication.run(DemoApplication.class, args);
}
}
二、@SpringBootApplication注解
点进@SpringBootApplication后,会发现这个注解上面是这样的。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
}
前四个注解是java中的四种元注解,主要作用如下:
@Target表示该注解用于什么地方,可以在枚举类 ElemenetType 中,包括:
ElemenetType.CONSTRUCTOR ------------------- 构造器声明
ElemenetType.FIELD ------------------------- 域声明(包括 enum 实例)
ElemenetType.LOCAL_VARIABLE ---------------- 局部变量声明
ElemenetType.METHOD ------------------------ 方法声明
ElemenetType.PACKAGE ---------------------- 包声明
ElemenetType.PARAMETER --------------------- 参数声明
ElemenetType.TYPE ---------------------------类,接口(包括注解类型)或enum声明
@Retention 表示在什么级别保存该注解信息。可选的参数值在枚举类型 RetentionPolicy 中,包括:
RetentionPolicy.SOURCE----------------------注解将被编译器丢弃
RetentionPolicy.CLASS ----------------------注解在class文件中可用,但会被VM丢弃
RetentionPolicy.RUNTIME --------------------VM将在运行期也保留注释,因此可以通过反射机制读取注解的信息。
@Documented 将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。
@Inherited 允许子类继承父类中的注解。
本节主要分析后三个注解@SpringBootConfiguration,@EnableAutoConfiguration与@ComponentScan。
1.@SpringBootConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
点进来发现它又是个组合注解,由@Configuration与@Indexed组成。
@Configuration用于定义配置类,替换了spring中的xml配置文件,详细请见日后spring源码中分析。
@Indexed是Spring 5.0版本新加入的功能,在很多应用中,随着应用变得越来越大,就会出现启动变得非常慢的问题,可以通过@Indexed来提高启动效率。
在项目中使用了@Indexed之后,编译打包的时候会在项目中自动生成META-INT/spring.components文件。
当Spring应用上下文执行ComponentScan扫描时,META-INT/spring.components将会被CandidateComponentsIndexLoader 读取并加载,转换为CandidateComponentsIndex对象,这样的话@ComponentScan不在扫描指定的package,而是读取CandidateComponentsIndex对象,从而达到提升性能的目的。
引入依赖即可使用。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-indexer</artifactId>
<version>5.1.12.RELEASE</version>
<optional>true</optional>
</dependency>
</dependencies>
此时读取文件是只需要读一次这个文件后即可加载完成,不需要多次IO操作,由此提升了程序的启动速度。
@Indexed 并非可以任意使用。在没有其他模块依赖或者所依赖的模块都生成了 spring.components 文件时不会存在问题,然而如果依赖的模块只有部分模块存在 spring.components 文件,则其他模块的 bean 也不会被扫描,为避免这种问题,需要在类路径下 spring.properties 文件中或系统属性中的 spring.index.ignore 参数设置为 true,这样就会跳过 spring.components 文件的扫描,而转为重新扫描类路径下的 bean 。
2.@EnableAutoConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
}
点进@EnableAutoConfiguration发现它由@AutoConfigurationPackage与@Import组成。
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}
再点进去@AutoConfigurationPackage发现它是由@Import组成,所以问题来到了@Import上面。
我们先打开AutoConfigurationImportSelector.class这个类,看看里面的实现是什么样子的。
它主要实现了Aware接口和ImoprtSelector接口,Aware部分不是此文章关键,在之后的spring源码中会提到。
那么重点来到了ImportSelecer接口的实现。
// 这个方法返回的就是需要注册到IoC容器中的对象对应的类型的全类路径名称的字符串数组
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
那么这个数据又是从哪来的呢,点进this.getAutoConfigurationEntry(annotationMetadata),
protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
// 获取注解的属性信息
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 获取候选配置信息,加载的是当前项目的classpath目录下的所有的META-INF/spring.factories文件中的key为
// org.springframework.boot.autoconfigure.EnableAutoConfiguration的信息
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// 移除集合中重复的元素,因为会加载多个路径下的spring.factories所以可能会重复
configurations = this.removeDuplicates(configurations);
// 将不需要配置的信息拿出来@SpringBootApplication(exclude = {xxx.class})
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 将这些不用的配置信息过滤,那么怎么确定哪些配置是不需要加载的呢?就需要打开filter方法看看
configurations = this.getConfigurationClassFilter().filter(configurations);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
// 这里写明数据是从 META-INF/spring.factories取得,或者从上面的loadFactoryNames()方法进去也可找到获取数据的位置
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;
}
打开filter()方法之后,会看到如下
打开autoConfigurationMetadata会发现它的数据来自于META-INF/spring-autoconfigure-metadata.properties。
那我们打开/spring-autoconfigure-metadata.properties,拿出一个未过滤前的类org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
执行完filter方法后我们会发现这个配置被过滤掉了。
通过上面的配置文件,我们发现RedisAutoConfiguration 被注入到IOC容器中的条件是系统中要存在 org.springframework.data.redis.core.RedisOperations 这个class文件。
然而现在我们的项目里并没有这个文件。那我们就那这个配置文件里的路径创建一个空的class文件出来,试试还会不会被过滤掉
答案是不会被过滤掉了。那么到此,这个自动装配的流程大概就清楚了。
3.@ComponentScan
@ComponentScan注解,该注解的作用是用来指定扫描路径的,如果不指定特定的扫描路径的话,扫描的路径是当前修饰的类所在的包及其子包。
三、手写Starter
我们如果想自己实现Starter呢,可以新创建一个Maven项目,创建配置类及Bean,进行打包。
然后可以通过Maven依赖导入,让SpringBoot在启动容器时自动装配它。
我们在原项目上引入依赖,启动run方法后会发现报错了?找不到MyService类,为什么呢?
错误原因是因为SpringBoot项目的包路径与自己写的Starter路径不同
那我们就在自己写的Starter里面配置一下
这样SpringBoot启动时,就会自动装配我们的Bean啦。
总结
本文主要分析了SpringBoot中用到的注解及SpringBoot自动装配的原理,还实现了自己的Starter。