SpringBoot 自动配置原理

news/2025/2/22 1:51:48/

SpringBoot 自动配置原理

注: 本文使用的springboot版本为 2.7.11

1、@SpringBootApplication

字面分析,这个注解是标注一个Spring Boot应用。

@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 {}

其中,我们需要着重分析三个注解:@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan

我们依次来进行分析

2、@SpringBootConfiguration

我们进入这个注解里面

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {}

我们可以看到 @SpringBootConfiguration 其实是 @Configuration 的派生注解,那么他们的功能就是一样了,标注这个类是一个配置类。只不过@SpringBootConfiguration是springboot的注解,而@Configuration是spring的注解。那么 @Configuration又是个什么呢?

2.1、 @Configuration

我们知道,spring之前的配置方式是通过xml文件,就像这样,将我们需要的对象以 标签的形式注入到Spring容器中。

 <?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttps://www.springframework.org/schema/beans/spring-beans.xsd"><bean id="student" class="com.x.pojo.Student"></bean></beans>

这样需要我们一个一个的写,当类很多的时候,会很麻烦。这时我们就可以用注解的方式去实现。使用注解的时候,要在配置文件中打开注解支持

<!--指定要扫描的包,这个包下的注解就会生效-->
<context:component-scan base-package="com.x"/>
<!--开启注解配置-->
<context:annotation-config/> 

我们在类上添加一个 @Component 注解,就可以将类标记为Spring中的bean了

@Component //组件 
public class User {//等价于 <property name="name" value="嘿嘿"/>public String name;@Value("嘿嘿")public void setName(String name) {this.name = name;}
}

基于 @Component 扩展的注解还有三个: @controller、@service、@repository

1@controller:   controller控制器层(注入服务)
2@service :      service服务层(注入dao)
3@repository :  dao持久层(实现dao访问)
4@component:  标注一个类为Spring容器的Bean,(把普通pojo实例化到spring容器中,相当于配置文件中的<bean id="" class=""/>

这里不具体讲四者有什么区别,只是希望大家简单的知道 @service 标记的类,说说这个类时service层的实现就够了。

回归正题:

Spring 3.0 起,支持了另外一种方式,基于Java类的配置方式。

@Configuration
public class AppConfig {@Beanpublic User user(){return new User();}
}

@Configuration 就代表这是一个 配置类。 AppConfig 就相当于我们的 applicationContext.xml

@Bean
User getUser(){return new User();//返回要注入bean的对象
}
// getUser 相当于 id
// 返回值就相当于 class 属性
<bean id="user" class="com.x.pojo.User"></bean>

这时候我们发现一个问题 @Bean 和 @Component 都可以将对象交给SPring 托管,那二者有啥区别呢

2.2、@Bean 和 @Component 的区别

@Component(和@Service和@Repository)用于自动检测和使用类路径扫描自动配置bean。注释类和bean之间存在隐式的一对一映射(即每个类一个bean)。
@Bean用于显式声明单个bean,而不是让Spring像上面那样自动执行它。它将bean的声明与类定义分离,并允许您精确地创建和配置bean

该怎么理解这段话呢?

首先 我们知道 @Component 是放在类上的,这样就可以将这个类标记为bean,并交给spring托管。我们完完全全就是在 这个类的 “源码”上进行的配置。那如果我们引入的是第三方的类,不知道源码,那我们怎么用@Component注释呢?这个时候就要用 @Bean了。也就是所谓的声明与类定义分离。我们不需要知道类是怎么定义的。我们只需要声明一个这个类的对象?

那可能又有疑问了,既然是声明对象为什么不这样写呢?

@Configuration
public class AppConfig {@BeanUser user;
}

现在我们去看@Bean的源码:这是源码的第一句话

Indicates that a method produces a bean to be managed by the Spring container.

中文意思是:指示方法生成要由 Spring 容器管理的 Bean。这样就应该能明白为啥放在方法上了吧。

关于@SpringBootConfiguration 就先到这里,它就是将这个类标记为了配置类。

3、@EnableAutoConfiguration

这个注解是自动配置的核心!!!也是重点和难点

//@EnableAutoConfiguration 源码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {...}

这里面我们需要关注两个注解 @AutoConfigurationPackage@Import({AutoConfigurationImportSelector.class})

3.1、@AutoConfigurationPackage

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {...}

从注解来看,@AutoConfigurationPackage中使用注解@Import导入了AutoConfigurationPackages.Registrar.class到容器中。

首先我们先来了解一下 @Import 这个注解

@Import

@Import注解用来帮助我们把一些需要定义为Bean的类导入到IOC容器里面,@Import可以添加在@SpringBootApplication(启动类)、@Configuration(配置类)、@Component(组件类)对应的类上。

1@Import引入普通的类可以帮助我们把普通的类定义为Bean@SpringBootApplication
// 通过@Import注解把ImportBean添加到IOC容器里面去(跟直接在bean类上加@Component一样)
@Import(ImportBean.class) 
public class MyBatisApplication {public static void main(String[] args) {SpringApplication.run(MyBatisApplication.class, args);}
}2@Import还可以引入一个@Configuration修饰的类(引入配置类),从而把让配置类生效(配置类下的所有Bean添加到IOC容器里面去)。在自定义starter的时候经常会用到。这个后面会讲到,先知道就行
注意:如果配置类在标准的SpringBoot包结构下(SpringBootApplication启动类包的根目录下)。是不需要@Import导入配置类的,SpringBoot自动帮做了。上面的情况一般用于@Configuration配置类不在标准的SpringBoot包结构下面。所以一般在自定义starter的时候用到。

现在,我们继续,我们进入 AutoConfigurationPackages.Registrar 这个类中,这个类是AutoConfigurationPackages的内部类

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {Registrar() {}public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));}public Set<Object> determineImports(AnnotationMetadata metadata) {return Collections.singleton(new PackageImports(metadata));}
}

该类的重点方法为registerBeanDefinitions()

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(
registry, 
(String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0])
);}

我们来分析一下AutoConfigurationPackages.register()的第二个参数

(String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0])

大概意思就是 一个PackageImports对象,获得了 所在的包名

我们进入类 PackageImports的源码中分析

PackageImports(AnnotationMetadata metadata) {//从提供的 AnnotationMetadata 对象中获取 AutoConfigurationPackage 注解的属性。//创建一个 AnnotationAttributes 对象,使用从注解属性中获取的值。AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(AutoConfigurationPackage.class.getName(), false));//初始化一个名为 packageNames 的 List<String>,并将其填充为从注解属性中获取的基础包名称。		List<String> packageNames = new ArrayList<>(Arrays.asList(attributes.getStringArray("basePackages")));//遍历注解属性中指定的 basePackageClasses,并将它们的包名添加到 packageNames 列表中。		for (Class<?> basePackageClass : attributes.getClassArray("basePackageClasses")) {packageNames.add(basePackageClass.getPackage().getName());}//如果没有指定基础包,就将提供的 metadata 所代表的类的包名添加到 packageNames 列表中。if (packageNames.isEmpty()) {packageNames.add(ClassUtils.getPackageName(metadata.getClassName()));}//最后,创建一个从收集到的包名构建的不可修改的列表(通过 Collections.unmodifiableList 方法),并将其赋值给类的 packageNames 字段。this.packageNames = Collections.unmodifiableList(packageNames);}

在这个构造方法中将元数据即启动类AnnotationMetadata metadata经过处理

获取标签注解信息,注解信息里面的 basePackages 和 basePackageClasses是否有数据。
basePackages、 basePackageClasses为注解@AutoConfigurationPackage中的属性。
如果没有数据则获取注解所在的类的名字目录,放到List中
获得packageNames属性也就是启动类所在的包。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们debug一下,发现确实如此

(String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0])

就是返回了 启动类所在的包名

然后回到这个方法,第一个参数是个注册表,第二个参数是 启动类所在的包

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(
registry, 
(String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0])
);}

我们进 register()的源码中看一下

public static void register(BeanDefinitionRegistry registry, String... packageNames) {if (registry.containsBeanDefinition(BEAN)) {BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition)registry.getBeanDefinition(BEAN);beanDefinition.addBasePackages(packageNames);} else {registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));}
}

这个方法的if语句为判断registry这个参数中是否已经注册了AutoConfigurationPackages的类路径所对应的bean(AutoConfigurationPackages)。如若已经被注册,则把上面分析的第二个参数所获取的包(启动类所在的包的名称)添加到这个bean的定义中。如若没有,则注册这个bean并且把包名设置到该bean的定义中。

总结:@AutoConfigurationPackage就是添加该注解的类所在的包作为自动配置包进行管理。他的实现就是依赖于工具类AutoConfigurationPackages中的内部类Registrar对所标注的包进行注册

3.2、@Import({AutoConfigurationImportSelector.class})

中文意思就是自动配置导入选择器,它会导入哪些组件的选择器呢

类中有两个方法:

selectImports()
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

它调用了另外一个方法:

getAutoConfigurationEntry()
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}AnnotationAttributes attributes = getAttributes(annotationMetadata);List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);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);
}

我们先debug一下,看看 configurations 这个集合里面都是什么

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

全都是xxxAutoConfig类,但是我们并不是全都要

configurations = removeDuplicates(configurations);Set<String> exclusions = getExclusions(annotationMetadata, attributes);checkExcludedClasses(configurations, exclusions);configurations.removeAll(exclusions);configurations = getConfigurationClassFilter().filter(configurations);fireAutoConfigurationImportEvents(configurations, exclusions);

还进行了一系列的检查啥的,去重,去掉不要的,被过滤掉的

它又调用了另外一个方法。

getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = new ArrayList(SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader()));ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader()).forEach(configurations::add);//如果是空的,则提示 在META-INF/spring.factories中没有自动配置类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;
}

这里有句断言: 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.”);

意思是:“在 META-INF/spring.factories 中没有找到自动配置类。如果您使用自定义包装,请确保该文件是正确的。“

这个方法中的返回内容是通过类SpringFactoriesLoader中的静态方法loadFactoryNames()进行获取的。那么就继续进入loadFactoryNames()

loadFactoryNames()
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {ClassLoader classLoaderToUse = classLoader;if (classLoader == null) {classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();}String factoryTypeName = factoryType.getName();return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

在这个方法中classLoaderToUse是一个关键变量,在这个方法中通过所传进来的参数获得,如果为空则直接获取SpringFactoriesLoader的ClassLoader。然后将第二个参数factoryType的类名传入变量factoryTypeName,最后放入loadSpringFactories(),那么我们还需要知道这个方法实现了什么:

loadSpringFactories()
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 {//public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);while (urls.hasMoreElements()) {//获取META-INF/spring.factories的详细地址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 elementsresult.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;}

我们俩看一下 result里面是啥

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

该方法作用是加载所有依赖的路径META-INF/spring.factories文件,通过map结构保存,key为文件中定义的一些标识工厂类,value就是能自动配置的一些工厂实现的类,value用list保存并去重。

------------------------------------------>public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader)
{...return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
//已经得知loadSpringFactories根据一个类加载器获得了所有的配置文件,所获得的所有配置进行getOrDefault()处理,如果存在与factoryTypeName相对应的List则返回,如果没有则获取一个空List,由loadSpringFactories可知只要classLoader不为空,则可获取所有配置文件。因此可以判断返回了foryType类名所对应的所有配置-------------------------------------->protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes){List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());...return configurations;
}
//再来分析参数一getSpringFactoriesLoaderFactoryClass()
protected Class<?> getSpringFactoriesLoaderFactoryClass() {return EnableAutoConfiguration.class;
}
//获取到了类EnableAutoConfiguration。参数二就是当前类中的全局变量beanClassLoader。
//通过这两个参数可以获得配置文件中key为EnableAutoConfiguration的所有value。因此可以判断当前方法返回的是以List的配置文件中所有的自动配置的类。---------------------------------->protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) 
{...return new AutoConfigurationEntry(configurations, exclusions);
}
//这个方法是将获取的所有自动配置类进行去重、排除、过滤等一系列的操作然后返回处理完成后的配置,并将其包装为AutoConfigurationEntry
------------------------------>@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) 
{...AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
//最后回到梦开始的地方,这一步是将所有的配置类转成字符串数组,然后通过@Import的使用方法————将数组内的所有配置导入到容器中。

总结:@EnableAutoConfiguration的实现方式是导入配置文件META-INF/spring.factories中EnableAutoConfiguration所对应的所有的类

4、@ComponentScan

@ComponentScan主要就是定义扫描的路径从中找出标识了需要装配的类自动装配到spring的bean容器中。部分源码如下:我们先部了解其他的东西,先知道它的基本作用

/*** 控制符合组件检测条件的类文件   默认是包扫描下的  **/*.class* @return*/
String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;
/*** 是否对带有@Component @Repository @Service @Controller注解的类开启检测,默认是开启的* @return*/boolean useDefaultFilters() default true;

有源码可知

SpringBoot默认包扫描机制: 从启动类所在包开始,扫描当前包及其子级包下的所有文件。这也就是为什么我们在创建 package时,要在启动类的同级目录了,只有这样,我们定义的class才能被扫描到(当然,比在统一目录也可以,这是就需要我们手动指定扫描路径了,比较麻烦)。

在一个springboot项目中,我们发现没有applicationContext.xml文件,那我们的类是怎么交给 spring托管呢。就是@SpringBootApplication,它的存在使得我们启动类本身就是一个配置类(相当于配置文件了),同时 @ComponentScan 也会去扫描所有的类,将加有相应注解的类交给spring托管。

5、@Conditional

了解完自动装配的原理后,我们来关注一个细节问题,自动配置类必须在一定的条件下才能生效;

@Conditional派生注解(Spring注解版原生的@Conditional作用)

作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

那么多的自动配置类,必须在一定的条件下才能生效;也就是说,我们加载了这么多的配置类,但不是 所有的都生效了。

我们怎么知道哪些自动配置类生效?

我们可以通过启用 debug=true属性;来让控制台打印自动配置报告,这样我们就可以很方便的知道哪 些自动配置类生效;

#开启springboot的调试类
debug=true

Positive matches:(自动配置类启用的:正匹配)

Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配)

Unconditional classes: (没有条件的类)


http://www.ppmy.cn/news/1214952.html

相关文章

22.能被7整除,并且求和。

#include<stdio.h>int main(){int i ,sum0;printf("1-1000能被7整除的数字有&#xff1a;\n");for(i1;i<1000;i){if(i%70){printf("%d ",i);sumsumi;} }printf("\n");printf("能被7整除的数字的和是&#xff1a;%d ",sum);re…

大数据开发数据治理方向

模型合规&#xff08;包括了元数据治理&#xff0c;原来只到了元数据层次&#xff09;&#xff1a;1.数据标准重制定及修复&#xff0c;包括对原来数据域重构&#xff0c;表字段命名体系重构&#xff0c;并对原来模型按照新标准合规改造 2.元数据补充 owner、使用说明、字段中文…

VS Code设置技巧

基础设置 中文界面 安装扩展&#xff1a;Chinese(Simplified) Language Pack 自动换行 文件 - 首选项 - 设置&#xff0c;搜索wrap&#xff0c;找到Editor: Word Wrap&#xff0c;将其更改为on。

js运算,笔试踩坑知识点

文章目录 前端面试系列运算符记住口诀先计算 后 赋值赋值从右向左 和 - -计算从左向右括号里的加减优先于括号外的乘除交换俩数的值答案 前端面试系列 js运算 笔试踩坑知识点 前端js面试题 &#xff08;三&#xff09; 前端js面试题&#xff08;二&#xff09; 前端js面试题 (…

day54 django中orm数据库增删改查

昨日内容回顾 三板斧问题 HttpResponse # 返回的是字符串 render # 渲染一个HTML静态文件&#xff0c;模板文件 redirect # 重定向的 ​ """在视图文件中得视图函数必须要接收一个形参request&#xff0c;并且&#xff0c;视图…

web网站 固定的邀请码字符 能被爬虫爬取吗?动态改变邀请码的字符是不是可以避免爬虫爬取或数据泄露

无论邀请码字符是固定的还是动态改变的&#xff0c;都无法完全避免爬虫爬取或数据泄露的风险。以下是一些要考虑的因素&#xff1a; 爬虫技术的发展&#xff1a;爬虫技术不断发展&#xff0c;可以智能地解析和获取网页内容。即使邀请码字符是固定的&#xff0c;高级爬虫仍然可以…

CCF ChinaSoft 2023 论坛巡礼 | 泛在计算时代的操作系统论坛

2023年CCF中国软件大会&#xff08;CCF ChinaSoft 2023&#xff09;由CCF主办&#xff0c;CCF系统软件专委会、形式化方法专委会、软件工程专委会以及复旦大学联合承办&#xff0c;将于2023年12月1-3日在上海国际会议中心举行。 本次大会主题是“智能化软件创新推动数字经济与社…

ElasticSearch搜索详细讲解与操作

全文检索基础 全文检索流程 流程&#xff1a; 创建索引 返回结果 查询索引 原始文档 创建索引 索引库 查询索引 创建索引&#xff1a; 获取文档 构建文档对象 分析文档分词 创建索引 查询索引&#xff1a; 用户查询结构 创建查询 执行查询 渲染结果 相关概念 索引库 索引库就…