SpringBoot Starter的用法以及原理

7 阅读4分钟

示例:hello-spring-boot-starter

为了理解SpringBoot Starter的写法,以hello-spring-boot-starter作为示例来讲解

创建父模块hello-spring-boot-starter-project

hello-spring-boot-starter-project将作为整个项目的父模块,其pom.xml文件如下:

<!--添加子模块-->  
<modules>  
<module>hello-spring-boot-starter</module>  
<module>hello-spring-boot-starter-autoconfigure</module>  
</modules>  
<!--定义的参数-->  
<properties>  
<maven.compiler.source>17</maven.compiler.source>  
<maven.compiler.target>17</maven.compiler.target>  
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
<spring-boot.version>3.0.7</spring-boot.version>  
</properties>  
<!--依赖的配置约定-->  
<dependencyManagement>  
<dependencies>  
<dependency>  
<groupId>org.springframework.boot</groupId>  
<artifactId>spring-boot-autoconfigure</artifactId>  
<version>${spring-boot.version}</version>  
</dependency>  
<dependency>  
<groupId>org.springframework.boot</groupId>  
<artifactId>spring-boot-configuration-processor</artifactId>  
<version>3.5.11</version>  
</dependency>  
</dependencies>  
</dependencyManagement> 

创建子模块hello-spring-boot-starter-autoconfigure

hello-spring-boot-starter-autoconfigure作为父模块hello-spring-boot-starter-project的一个子模块,其pom.xml关键配置如下:

<!--设置父模块-->  
<parent>  
<groupId>edu.whut</groupId>  
<artifactId>hello-spring-boot-starter-project</artifactId>  
<version>1.0-SNAPSHOT</version>  
</parent>
<dependencies>  
<dependency>  
<groupId>org.springframework.boot</groupId>  
<artifactId>spring-boot-autoconfigure</artifactId>  
</dependency>  
<dependency>  
<groupId>org.springframework.boot</groupId>  
<artifactId>spring-boot-configuration-processor</artifactId>  
</dependency>  
</dependencies>

在其模块内部创建三个文件
HelloService.java:

public class HelloService {  
private final HelloProperties properties;  
  
public HelloService(HelloProperties properties) {  
this.properties = properties;  
}  
  
public void sayHello() {  
System.out.println("Hello,"+properties.getObject());  
}  
}

HelloProperties.java:

@ConfigurationProperties(prefix = "hello")  
public class HelloProperties {  
private String object;  
  
public String getObject() {  
return object;  
}  
  
public void setObject(String object) {  
this.object = object;  
}  
}

HelloAutoConfiguration.java:

//标识自动配置类  
@AutoConfiguration  
@EnableConfigurationProperties(HelloProperties.class)  
public class HelloAutoConfiguration {  
@Bean  
//在application文件中配置了hello.object配置才会构造这个bean  
@ConditionalOnProperty(prefix = "hello", name = "object")  
public HelloService helloService(HelloProperties properties) {  
return new HelloService(properties);  
}  
}

hello-spring-boot-starter-autoconfigure模块中的resources下的META-INF下的spring目录下创建一个名为org.springframework.boot.autoconfigure.AutoConfiguration.imports的文件,文件中写入:

edu.whut.HelloAutoConfiguration

创建hello-spring-boot-starter子模块

hello-spring-boot-starter作为父模块hello-spring-boot-starter-project的一个子模块,其pom.xml关键配置如下:

<!--指定父模块-->  
<parent>  
<groupId>edu.whut</groupId>  
<artifactId>hello-spring-boot-starter-project</artifactId>  
<version>1.0-SNAPSHOT</version>  
</parent>
<dependencies>  
<dependency>  
<groupId>edu.whut</groupId>  
<artifactId>hello-spring-boot-starter-autoconfigure</artifactId>  
<version>1.0-SNAPSHOT</version>  
</dependency>  
</dependencies>

这样这个模块就完成了

使用hello-spring-boot-starter

随意创建一个springboot项目,在main方法中查找对应的名为helloServicebean

public static void main(String[] args) {  
ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class, args);  
Object bean = applicationContext.getBean("helloService");  
System.out.println(bean);  
}

结果为:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'helloService' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:978)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1381)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:202)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1296)
	at com.example.demo.DemoApplication.main(DemoApplication.java:12)

这是正常的,因为现在还未设置配置,更改aplication.properties,写入:

hello.object="world"

再次运行程序,现在就可以获取到starter中配置的bean

edu.whut.HelloService@4463d9d3

为什么要使用springboot starter?

有人可能好奇,这么大费周章,就是为了把一个对象注入到IOC容器中?那为什么不直接在项目里注入?
因为每个Starter是一个高内聚的功能模块,通过依赖传递条件化配置@ConditionalOnClass等)实现“智能装配”,避免冗余代码。

为什么不直接写starter,反而需要一个autoconfigure?

这是为了遵循Spring Boot官方提倡的“关注点分离”原则,将自动配置逻辑依赖管理解耦,让架构更清晰、更易维护。
autoconfigure包含条件化配置类@ConfigurationPropertiesMETA-INF/spring.factories(或org.springframework.boot.autoconfigure.AutoConfiguration.imports

starter仅一个pom.xml,聚合autoconfigure模块​ + 该功能所需的所有第三方依赖

springboot starter自动装配的原理

@SpringBootApplciation注解是一个组合注解,这个注解被一个@EnableAutoConfiguration所注解。

@EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) // 核心关键
public @interface EnableAutoConfiguration {
    // ...
}

ImportSelector是一个用于动态、编程式地选择要导入的配置类的核心接口,它是一个函数式接口,核心方法是:

String[] selectImports(AnnotationMetadata importingClassMetadata);

这个方法根据给定的注解元数据(被@Import注解的类的信息),返回一个由全限定类名组成的字符串数组。这些返回的类名会在运行时被Spring容器处理,就像它们原本就被@Import注解直接引用一样,其内部的@Configuration@Bean等注解会被正常解析。

AutoConfigurationImportSelector

AutoConfigurationImportSelector实现了ImportSelector接口,实现了selectImports方法。

public String[] selectImports(AnnotationMetadata annotationMetadata) {  
// 1. 检查是否开启了自动装配(默认是开启的)
if (!this.isEnabled(annotationMetadata)) {  
return NO_IMPORTS;  
} else {  
AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);  
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());  
}  
}

因此核心的逻辑是getAutoConfigurationEntry(annotationMetadata)

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {  
if (!this.isEnabled(annotationMetadata)) {  
return EMPTY_ENTRY;  
} else {  
AnnotationAttributes attributes = this.getAttributes(annotationMetadata); 
//加载所有候选配置类
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);  //移除重复和显式排除的类
configurations = this.removeDuplicates(configurations);  
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);  
this.checkExcludedClasses(configurations, exclusions);  
configurations.removeAll(exclusions);  
//进行条件注解的筛选
configurations = this.getConfigurationClassFilter().filter(configurations);  
this.fireAutoConfigurationImportEvents(configurations, exclusions);  
return new AutoConfigurationEntry(configurations, exclusions);  
}  
}

在这个方法中,核心逻辑只有三步:

  1. 加载所有的候选配置类
  2. 移除重复和显式排除的类
  3. 进行“条件注解”筛选

加载候选配置类

调用 getCandidateConfigurations()。这个方法会去约定好的位置,读取一个文件,这个文件里列出了所有可能被加载的自动配置类
这个约定好的位置,在springboot3META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports,这个文件的每一行是一个全类名,在springboot2.x中是META-INF/spring.factoriesorg.springframework.boot.autoconfigure.EnableAutoConfiguration为键的值就是一系列全类名。
无论哪种格式,此时得到的都是一个巨大的List,包含了Spring Boot所有内置的(如DataSourceAutoConfiguration, WebMvcAutoConfiguration)以及第三方starter提供的自动配置类。因此要进行后续的排除和筛选。

总结

  1. 启动:执行SpringApplication.run(),启动Spring容器
  2. 触发:容器解析主类上的@SpringBootApplication->@EnableAutoConfiguration
  3. 决策:@EnableAutoConfiguration导入AutoConfigurationImportSelector
  4. 扫描:AutoConfigurationImportSelectorMETA-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(Spring Boot 3)中读取所有候选自动配置类
  5. 排除和筛选:对候选列表进行层层筛选
  6. 导入:将最终满足所有条件的自动配置类的全限定名数组返回给容器。
  7. 解析与注册:容器将这些自动配置类当作普通的 @Configuration类进行解析,将其内部符合条件的 @Bean方法注册为Bean定义。