文章目录
自动装配原理
1.介绍
自动装配就是Springboot在启动时会将一些默认加载项以及配置文件当中的内容自动注入到IOC容器当中,在使用时直接通过注解调用即可.
2. 源码分析
springboot 通过 @SpringBootApplication 注解来标注启动类
当我们点击进入@SpringBootApplication时,会发现里面有几个注解
@Target(ElementType.TYPE) //标注位置: 类 接口
@Retention(RetentionPolicy.RUNTIME) // 保留策略 运行时代码
@Documented // 生成JavaDoc
@Inherited //运行被继承
@SpringBootConfiguration // 相当于@Configuration 表示这是一个配置类
@EnableAutoConfiguration // 将所有符合自动配置条件的bean定义加载到IoC容器
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) // 注解扫描 扫描的是当前类所在包及其子包中类中的注解
其中我们要重点关注的有三个注解
@SpringBootConfiguration ,@EnableAutoConfiguration和 @ComponentScan
2.1 @SpringBootConfiguration
当我们点击进入@SpringBootConfiguration之后,会看到如下注解
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
其中我们只需要关注@Configuration 就可以,我们发现这是一个配置类注解,意味着我们可以在 @SpringBootApplication 标注的启动类中自定义bean.
2.2 @ComponentScan
这是一个扫描注解,它代表这会自动扫描启动类所在包以及其子包中的注解.
2.3 @EnableAutoConfiguration (重点)
当我们点击进入@EnableAutoConfiguration 注解后,我们会看到如下注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
我们所需要关注的只有两个注解@AutoConfigurationPackage 和@Import(AutoConfigurationImportSelector.class)
2.3.1 @AutoConfigurationPackage
这个注解其实也内部也使用了一个@Import 注解,它的作用是将main包下的所有组件注册到容器当中.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
2.3.2 @Import(AutoConfigurationImportSelector.class)
我们可以发现,这个注解是将AutoConfigurationImportSelector这个类导入到了容器当中.
我们点击进去会发现如下继承关系
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
我们发现它继承了 DeferredImportSelector 接口,而DeferredImportSelector又继承了ImportSelector接口
public interface DeferredImportSelector extends ImportSelector {
所以在AutoConfigurationImportSelector 中应该是重写了selectImports方法并返回了一个字符串数组.这个数组中存储的是要加载的Config配置文件的全包名列表.
我们在AutoConfigurationImportSelector中寻找,果然发现它重写了此方法.
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//判断自动装配是否打开
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
//获取所需要装配的bean
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
我们发现它先对自动装配是否开启做了一个判断
我们点击去之后可以看到
protected boolean isEnabled(AnnotationMetadata metadata) {
if (getClass() == AutoConfigurationImportSelector.class) {
return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
}
return true;
}
这意味这我们是可以手动配置是否开启自动装配的,如果不配置,则默认为开启。
返回去我们再看,在判断完是否开启自动配置之后,就需要获取所需要装配的Bean了,我们发现它是通过getAutoConfigurationEntry完成的,我们点击进去看一下是如何实现的.
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
//判断是否打开自动配置
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
//用于获取注解中的exclude和excludeName.
//获取注解
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//获取需要自动装配的配置类.
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
//符合条件加载,去除重复的配置项
//当用户与springboot配置冲突时.以用户配置为主
configurations = removeDuplicates(configurations);
//执行
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
//校验
checkExcludedClasses(configurations, exclusions);
//删除
configurations.removeAll(exclusions);
//过滤
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
//创建自动配置对象
return new AutoConfigurationEntry(configurations, exclusions);
}
通过断点执行我们可以发现,由下面这一行获取到需要自动装配的配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
通过下面这一行获取@EnableAutoConfiguration注解中的exclue和excludeName
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
我们重点关注获取自动装配的配置类
我们发现它是通过getCandidateConfigurations这个方法获取的,点击进去我们发现有如下代码
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
在这段代码中我们并没有发现传递的具体路径,但是我们发现了一个断言
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ “are using a custom packaging, make sure that file is correct.”);
即如果在这两个文件当中找不到自动配置类。如果使用的是自定义包装,请确保该文件正确无误。
我们可以推论出自动装配类就在这两个文件当中,我们在IDEA里找找这两个文件
点击进去
2.3.3 小结
我们发现这与我们写的yml文件十分的相似,于是我们就可以得出一个结论,即Springboot的自动装配就是在启动时自动读取这些配置文件当中的内容并将其放入容器当中,使用时我们就可以直接调用.
2.3.4 读取配置文件位置
我们之前是通过断言推论出了配置文件的位置,那配置文件到底是如何被找到的呢?
我们通过阅读代码发现,他通过两段代码实现了这个功能
//读取META-INF/spring.factories
List<String> configurations = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
//读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
我们先进入第一个方法
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
发现方法返回了一个字符串的集合,这个集合中存储的应该就是获取的候选配置.我们点击loadSpringFactories进去
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
在try中我们发现了一个枚举类,这会不会是我们要找的地址呢?我们点击进去一看
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
发现它果然是我们要找的文件META-INF/spring.factories,至此我们就找到了第一个文件的位置.
我们看之前的第二段代码,如果猜的没错,它会带领我们找到第二个文件的位置.
//读取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
我们点击进去看一下
private static final String LOCATION = "META-INF/spring/%s.imports";
发现了这样一个代码
它使用了模糊匹配的方式来找到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.这个文件,至此我们两个文件都全部找到了 .
2.3.5 过滤
在上面的代码分析当中,我们知道了Springboot是如何自己找到两个配置文件并完成自动装配的,但这个时候我们又有另一个疑问,这么多得配置类中的Bean都要被装入容器中吗?
为此我们要再一次进入配置文件当中查看,以gson来作为例子,我们进入它的构造方法查看
@AutoConfiguration
@ConditionalOnClass(Gson.class)
@EnableConfigurationProperties(GsonProperties.class)
public class GsonAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public GsonBuilder gsonBuilder(List<GsonBuilderCustomizer> customizers) {
GsonBuilder builder = new GsonBuilder();
customizers.forEach((c) -> c.customize(builder));
return builder;
}
@Bean
@ConditionalOnMissingBean
public Gson gson(GsonBuilder gsonBuilder) {
return gsonBuilder.create();
}
我们发现在类和构造方法上有两个注解@ConditionalOnClass(Gson.class)和@ConditionalOnMissingBean
那么这两个注解有什么作用呢
@ConditionalOnClass 标注在类上,会判断环境中是否有对应字节码文件才初始化Bean
@ConditionalOnMissingBean标注在方法上判断环境中没有对应Bean才初始化Bean
在Springboot自动装配中,提供了下面这些注解用于判断是否初始化Bean
@ConditionalOnBean:当容器里有指定 Bean 的条件下
@ConditionalOnMissingBean:当容器里没有指定 Bean 的情况下
@ConditionalOnSingleCandidate:当指定 Bean 在容器中只有一个,或者虽然有多个但是指定首选 Bean
@ConditionalOnClass:当类路径下有指定类的条件下
@ConditionalOnMissingClass:当类路径下没有指定类的条件下
@ConditionalOnProperty:指定的属性是否有指定的值
@ConditionalOnResource:类路径是否有指定的值
@ConditionalOnExpression:基于 SpEL 表达式作为判断条件
@ConditionalOnJava:基于 Java 版本作为判断条件
@ConditionalOnJndi:在 JNDI 存在的条件下差在指定的位置
@ConditionalOnNotWebApplication:当前项目不是 Web 项目的条件下
@ConditionalOnWebApplication:当前项目是 Web 项 目的条件下
3.总结
所谓的自动装配,就我个人理解是读取META-INF/spring.factories 和 METAINF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.这两个配置文件当中配置进行类的自动装配,并通过@Conditional注解进行按需加载.