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: (没有条件的类)