SpringBoot 自动配置原理

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容器中。

 
 <beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans
 https://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 {
    //等价于 
    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 {
    @Bean
    public 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 {
    @Bean
    User 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,并将其填充为从注解属性中获取的基础包名称。		
    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属性也就是启动类所在的包。

SpringBoot 自动配置原理_第1张图片

我们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 这个集合里面都是什么

SpringBoot 自动配置原理_第2张图片

全都是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 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;
	}

我们俩看一下 result里面是啥

SpringBoot 自动配置原理_第3张图片

该方法作用是加载所有依赖的路径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
------------------------------>
    @Override
    public 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指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效;

SpringBoot 自动配置原理_第4张图片

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

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

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

#开启springboot的调试类
debug=true

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

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

Unconditional classes: (没有条件的类)

你可能感兴趣的:(spring,boot,java,后端)