springboot自动配置的原理和如何自定义starter

发布时间 2023-09-03 20:01:49作者: 程序晓猿

一、springboot自动配置的原理

使用springboot时的一大优点就是当需要引入一些第三方的框架时只需要引入一个对应的starter后springboot就会自动的完成配置,例如在springboot中使用mybatis只需要引入mybatis提供的starter.

那么这种便捷的配置方式是如何实现的呢,要了解其中的原理需要先从了解一个接口ImportSelector开始

1.1 ImportSelector接口的作用


public interface ImportSelector {

	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

注意这是spring提供的一个接口,当我们提供一个它的实现类并在配置类上用@Import注解导入这个实现类后,

spring就会把实现类中selectImports方法返回的数组中的元素当做类的全路径去找到对应的类然后加入到spring容器中。

举个例子说明,现在有一个AService

public class AService {
    public void methodA(){
        System.out.println("methodA");
    }
}

然后提供一个ImportSelector的实现类

public class ConfigImport implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.lyy.guanchazhe.AService"};
    }
}

然后测试

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

public class Test3 {
    public static void main(String[] args) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(MyConfig3.class);
        AService aService = context.getBean(AService.class);
        aService.methodA();
    }

    //配置类
    @Configuration
    @Import(ConfigImport.class)
    static class MyConfig3 {

    }
}

可以看到ImportSelector接口给我们提供了一种动态加载bean信息的方式,我们可以在selectImports方法中去读取外部文件中的信息来获取bean定义信息,springboot自动配置的时候就用到了这种方式。

1.2 自动配置的原理 AutoConfigurationImportSelector

在springboot中提供了一个ImportSelector接口的实现类 AutoConfigurationImportSelector,它的包路径是

org.springframework.boot.autoconfigure, 从这个包路径就能看出来它的作用和自动配置相关。

先不管这个类是怎么被导入的,我们直接看下它的selectImports方法

public class AutoConfigurationImportSelector
		implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware,
BeanFactoryAware, EnvironmentAware, Ordered {
    @Override
	public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
        //注意这里,这个方法是在获取当前项目中所有starter中的自动配置信息
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
				autoConfigurationMetadata, annotationMetadata);
        // 这里是在返回上边收集到的自动配置类的全类名数组,spring会把这些类全部加入到spring容器中
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}
    
    //详细看下getAutoConfigurationEntry方法
    protected AutoConfigurationEntry getAutoConfigurationEntry(
			AutoConfigurationMetadata autoConfigurationMetadata,
			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 = filter(configurations, autoConfigurationMetadata);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}
    
        //这个方法获取候选的自动配置类信息
    	protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
			AnnotationAttributes attributes) {
         //这里调用的这个静态方法会从所有的starter包的META-INF/spring.factories文件中
         //查找EnableAutoConfiguration这个键对应的值并返回
		List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
				getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
		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;
	}

	
	protected Class<?> getSpringFactoriesLoaderFactoryClass() {
		return EnableAutoConfiguration.class;
	}
}

注意spring.factories文件中的内容实际上是以键值对的形式存储,根properties文件一样,springboot在解析它的时候也是用properties解析的。

我们来看下spring-boot-autoconfigure-2.1.3.RELEASE.jar这个包中spring.factories文件中关于EnableAutoConfiguration这个键的配置

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
# 多个以逗号隔开,\表示换行,省略其他的,其剩下的中有一个WebMvcAutoConfiguration它是用来配置springmvc的

到这里我们就可以总结下自动配置的过程,AutoConfigurationImportSelector#selectImports方法会从每个starter包的 spring.factories文件中查找EnableAutoConfiguration键对应的值,把所有的值封装成一个数组返回,spring就会把查找到的这些类全部加入到spring容器中成为bean,而每个starter自己提供的XXXAutoConfiguration会自己去处理自身的配置逻辑,实际就是在自己的XXXAutoConfiguration中使用@Bean注解去注册自身运行所必须的一些bean。

springboot只是识别到每个starter中的这个类然后加入到容器中。

1.3 AutoConfigurationImportSelector是如何被引入的

从启动类上加的@SpringBootApplication注解开始,此注解上加了一个@EnableAutoConfiguration 注解

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

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {};

}

到这里就明白了,这个注解上用@@Import导入了我们上边分析到的这个类AutoConfigurationImportSelector

二、springboot自定义starter

从上边的分析我们知道自动配置的本质是在starter中创建一个配置类,然后在META-INF/spring.factories中配置

EnableAutoConfiguration键值对,所以我们尝试自定义一个简单的starter

第一步创建一个简单的maven工程,引入spring的依赖,并创建配置类和一个简单的service

pom文件中引入springboot的依赖

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
            <version>2.1.3.RELEASE</version>
</dependency>

注意这里是为了方便才直接引入springboot的starter,因为我们这个service不需要自己启动所以也可以只引入spring相关的依赖即可

创建HelloService

package com.hello;

public class HelloService {
    
}

创建配置类HelloConfig

package com.hello;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class HelloConfig {

    @Bean
    public HelloService helloService (){
        return new HelloService();
    }
}

在resources目录下创建META-INF/spring.factories

# 让springboot自动配置能识别到自定义starter中的配置类
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.hello.HelloConfig

然后把这个工程安装到本地的maven仓库,再创建一个测试用的springboot工程引入刚刚那个starter。

测试工程启动类

import com.hello.HelloService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class HelloStarterTest {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(HelloStarterTest.class);
        HelloService hello = context.getBean(HelloService.class);
        System.out.println(hello);
    }
}

因为在自定义starter的配置类中给容器中添加了HelloService,所以这个工程里可以直接从spring容器中获取,控制台会输出bean的地址。这样我们自定义的starter就开发成功了。

我们也可以测试把starter中的spring.factories删掉,重新安装到仓库,在测试工程中重新引入,再次启动就会抛出异常提示容器中找不到bean。