当前位置: 首页 > news >正文

SpringBoot 自动装配原理 自定义一个 starter

目录

  • 1、pom.xml 文件
    • 1.1、parent 模块
      • 1.1.1、资源文件
        • 1.1.1.1、`resources` 标签说明
        • 1.1.1.2、从 Maven 视角:资源处理全流程​
      • 1.1.2、插件
    • 1.2、dependencies 模块
  • 2、启动器
  • 3、主程序
    • 3.1、`@SpringBootApplication` 注解
    • 3.2、`@SpringBootConfiguration` 注解
      • 3.2.1、`@Configuration` 和 `@Component` 区别
    • 3.3、`@EnableAutoConfiguration` 注解
      • 3.3.1、`@AutoConfigurationPackage` 注解
        • 3.3.1.1、`AutoConfigurationPackages.Registrar#registerBeanDefinitions()` 方法:
      • 3.3.2、`@Import` 注解
      • 3.3.3、`AutoConfigurationImportSelector`
    • 3.4、自动装配原理
  • 4、自定义一个 starter
    • 4.1、基础版
      • 4.1.1、创建一个 SpringBoot 工程
      • 4.1.2、新建一个 Properteis 类
      • 4.1.3、新建一个自动配置类,关联 Properties 类
      • 4.1.4、添加一个对外服务接口
      • 4.1.5、注册自动配置类
      • 4.1.6、`mvn install`
      • 4.1.7、引用 starter
        • 4.1.7.1、pom.xml 中引入 starter
        • 4.1.7.2、使用 starter
    • 4.2、引入条件装配
      • 4.2.1、修改配置类 `ReadingAutoConfiguration`
      • 4.2.2、修改使用 starter
    • 4.3、重写 starter 的默认配置
    • 4.4、重写 starter 默认实现
    • 4.5、实现一个自己的 `@EnableXXX`
      • 4.5.1、在 starter 中添加一个 selector
      • 4.5.2、在 starter 中删除 `resources/META-INF/spring.factories` 文件
      • 4.5.3、在 starter 中添加一个注解 `@EnableReading`
      • 4.5.4、在当前项目中做出修改

1、pom.xml 文件

创建一个 SpringBoot 工程,查看 pom.xml 文件,当前工程是依赖 parent 模块。如下图:

在这里插入图片描述

1.1、parent 模块

parent 模块主要加载配置文件【yml】和插件。如下图:

在这里插入图片描述

parent 模块又依赖 dependencies 模块

1.1.1、资源文件

在这里插入图片描述

src/main/resources 目录下的 yml/yaml/properties 文件进行占位符替换【第一个 resource:启用过滤】,其它文件不进行处理【第二个 resource:防止其它文件误被修改】,最终都会打包

1.1.1.1、resources 标签说明

Maven 处理 <resources> 时采用​​顺序合并策略​​,每个 <resource> 块定义不同的过滤规则:

  • 第一个资源块(精准过滤)
    • 处理范围​​:仅 application*.yml/yaml/properties
    • 操作​​:开启过滤(filtering=true
    • 目标​​:​​动态替换​​这些配置文件中的 @variable@ 占位符
  • 第二个资源块(全量保护)
    • ​​处理范围​​:排除上述配置文件后的​​其他所有资源​
    • 操作​​:默认关闭过滤(filtering=false
    • ​​目标​​:​​原样复制​​静态资源(如图片、HTML、XML 等)

处理流程如下图:

在这里插入图片描述

1.1.1.2、从 Maven 视角:资源处理全流程​

如下图:
在这里插入图片描述
关键结论:

  1. ​​所有资源文件都会被打包​​,无论是否经过过滤
  2. 过滤(filtering)只是对文件内容的处理,与是否打包无关
  3. 两个资源块共同作用确保:
    • 配置文件:动态内容替换
    • 其他资源:保持原始状态

1.1.2、插件

在这里插入图片描述

1.2、dependencies 模块

dependencies 模块主要管理了各种依赖的版本。所以,在 SpringBoot 工程中导入依赖时,不需要指定版本,已有默认的版本。如下图:

在这里插入图片描述

2、启动器

启动器:SpringBoot 启动的场景。如:spring-boot-starter-web,会帮我们自动导入 web 环境的所有的依赖:

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>

SpringBoot 将所有的功能场景变成一个个启动器。当我们需要使用什么功能时,就只需要导入对应的启动器即可。

3、主程序

@SpringBootApplication
public class MybootApplication {public static void main(String[] args) {SpringApplication.run(MybootApplication.class, args);}}
  1. @SpringBootApplication:标注这个类是一个 SpringBoot 应用
  2. SpringApplication.run() 方法启动 SpringBoot 应用

3.1、@SpringBootApplication 注解

@SpringBootApplication 注解是一个复合注解

@SpringBootConfiguration
@EnableAutoConfiguration
public @interface SpringBootApplication {//...
}

3.2、@SpringBootConfiguration 注解

// 保证了 bean 的唯一性。@Component 无法保证
@Configuration
public @interface SpringBootConfiguration {// 默认使用 CGLIB 代理该类@AliasFor(annotation = Configuration.class)boolean proxyBeanMethods() default true;
}

@SpringBootConfiguration 注解相当于 @Configuration 注解

3.2.1、@Configuration@Component 区别

  • @Component 在 Spring 中是代表 LITE 模式的配置注解,这种模式下的注解不会被 Spring 所代理,就是一个标准类,如果在这个类中有 @Bean 标注的方法,那么方法间的相互调用,其实就是普通 Java 类的方法的调用。

  • @Configuration 在 Spring 中是代表 FULL 模式的配置注解,这种模式下的类会被 Spring 所代理,那么在这个类中的 @Bean 方法的相互调用,就相当于调用了代理方法,那么在代理方法中会判断,是否调用 getBean 方法还是 invokeSuper 方法,这里就是这两个注解的最根本的区别。@Configuration 中所有带 @Bean 注解的方法都会被动态代理,且该方法返回的都是同一个实例。且 @Configuration(proxyBeanMethods = false)@Component 作用一样

如下:

①:使用 @Configuration 注解:

@Configuration
//@Component
public class UserConfig {@Beanpublic User user() {System.out.println("user..........");return new User();}@Beanpublic Teacher teacher() {// 通过代理调用, 返回同一实例return new Teacher(user());}}

②:测试

@GetMapping("/testAuto")
public void testAuto() {ApplicationContext ctx = new AnnotationConfigApplicationContext(UserConfig.class);User configB1 = ctx.getBean(User.class);User configB2 = ctx.getBean(UserConfig.class).user();// 输出 trueSystem.out.println(configB1 == configB2);ApplicationContext ctx = new AnnotationConfigApplicationContext(UserComponent.class);User component1 = ctx.getBean(User.class);User component2 = ctx.getBean(UserConfig.class).user();// 输出 falseSystem.out.println(configB1 == configB2);
}

@Component 类中,若需要依赖其他 @Bean,应通过参数注入而非直接调用方法:

@Component
public class UserConfig {@Beanpublic User user() {System.out.println("user..........");return new User();}/*@Beanpublic Teacher teacher() {// 通过代理调用, 返回同一实例return new Teacher(user());}*/@Beanpublic Teacher teacher(User user) {return new Teacher(user);}}

3.3、@EnableAutoConfiguration 注解

@AutoConfigurationPackage
// 导入参数类到 IOC 容器中
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {//...
}

3.3.1、@AutoConfigurationPackage 注解

// 保存扫描路径,提供给 spring-data-jpa 查询【@Entity】
@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {String[] basePackages() default {};Class<?>[] basePackageClasses() default {};
}
3.3.1.1、AutoConfigurationPackages.Registrar#registerBeanDefinitions() 方法:
public abstract class AutoConfigurationPackages {public static void register(BeanDefinitionRegistry registry, String... packageNames) {if (registry.containsBeanDefinition(BEAN)) {BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));} else {GenericBeanDefinition beanDefinition = new GenericBeanDefinition();beanDefinition.setBeanClass(BasePackages.class);beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);beanDefinition.setRole(2);registry.registerBeanDefinition(BEAN, beanDefinition);}}private static String[] addBasePackages(ConstructorArgumentValues constructorArguments, String[] packageNames) {String[] existing = (String[])((String[])constructorArguments.getIndexedArgumentValue(0, String[].class).getValue());Set<String> merged = new LinkedHashSet();merged.addAll(Arrays.asList(existing));merged.addAll(Arrays.asList(packageNames));return StringUtils.toStringArray(merged);}static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {Registrar() {}public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {AutoConfigurationPackages.register(registry, (new PackageImport(metadata)).getPackageName());}}
}

3.3.2、@Import 注解

@Import 注解:SpringBoot 自动装配的核心注解。它有三种用法:

  1. 参数如果是普通类:将该类实例化并交给 IOC 容器管理
  2. 参数如果是 ImportBeanDefinitionRegistrar 的实现类,则支持手工注册 Bean
  3. 参数如果是 ImportSelector 的实现类,注册 selectImports() 方法返回的数组【类全路径】到 IOC 容器 【批量注册】

3.3.3、AutoConfigurationImportSelector

selectImports() 方法:查询导入的组件

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

getAutoConfigurationEntry() 方法:

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);}
}

getCandidateConfigurations() 方法:获取候选的配置

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
}

SpringFactoriesLoader#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());
}

loadSpringFactories() 方法:

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {Map<String, List<String>> result = (Map)cache.get(classLoader);if (result != null) {return result;} else {Map<String, List<String>> result = new HashMap();try {Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");while(urls.hasMoreElements()) {URL url = (URL)urls.nextElement();UrlResource resource = new UrlResource(url);Properties properties = PropertiesLoaderUtils.loadProperties(resource);Iterator var6 = properties.entrySet().iterator();while(var6.hasNext()) {Map.Entry<?, ?> entry = (Map.Entry)var6.next();String factoryTypeName = ((String)entry.getKey()).trim();String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());String[] var10 = factoryImplementationNames;int var11 = factoryImplementationNames.length;for(int var12 = 0; var12 < var11; ++var12) {String factoryImplementationName = var10[var12];((List)result.computeIfAbsent(factoryTypeName, (key) -> {return new ArrayList();})).add(factoryImplementationName.trim());}}}result.replaceAll((factoryType, implementations) -> {return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));});cache.put(classLoader, result);return result;} catch (IOException var14) {throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);}}
}

加载 autoconfigure 包下的 META-INF/spring.factories 自动配置类到 IOC 容器中去:

在这里插入图片描述

3.4、自动装配原理

SpringBoot 启动的时候会通过 @EnableAutoConfiguration 注解找到 META-INF/spring.factories 配置文件中的所有自动配置类,并对其进行加载【但不一定生效,要判断条件是否成立】。只要导入了对应的 starter,就有对应的启动器,自动配置类就会生效,然后就配置成功。

  • 这些自动配置类都是以 AutoConfiguration 结尾来命名的,它实际上就是一个 Java 配置类形式的 Spring 容器配置类,它能通过以 Properties 结尾命名的类中取得在全局配置文件中配置的属性如:server.port
  • XxxxProperties 类是通过 @ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

AutoConfigurationImportSelector#selectImports() 方法通过 SpringLoaderFactories#loadFactories() 方法扫描所有具有 META-INFO/spring.factories 文件的 jar 包(spring-boot-autoconfigure.jar 就有)。这个spring.factories 文件也是一组一组的 key=value 的形式,其中一个 key 是 EnableAutoConfiguration 类的全类名,而它的 value 是一个 xxxxAutoConfiguration 的类名的列表,这些类名以逗号分隔。在SpringApplication.run(...) 的内部就会执行 selectImports()方法,找到所有 JavaConfig 自动配置类的全限定名对应的 class,然后将所有自动配置类加载到 Spring 容器中

4、自定义一个 starter

4.1、基础版

结构图如下:

在这里插入图片描述

4.1.1、创建一个 SpringBoot 工程

创建一个 SpringBoot 工程:read-spring-boot-starter【官方推荐命名:××-spring-boot-starter】,其 pom.xml 如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.14</version><relativePath/></parent><groupId>com.zzc</groupId><artifactId>read-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version><name>read-spring-boot-starter</name><description>read-spring-boot-starter</description><properties><java.version>1.8</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.7.6</spring-boot.version></properties><dependencies><!--自动配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-autoconfigure</artifactId></dependency><dependency><groupId>org.slf4j</groupId><artifactId>slf4j-api</artifactId><version>2.0.9</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.30</version></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>1.8</source><target>1.8</target><encoding>UTF-8</encoding></configuration></plugin></plugins></build></project>

4.1.2、新建一个 Properteis 类

@Data
@ConfigurationProperties(prefix = "reading")
public class ReadingProperties {// 类型private String type = "txt";
}

4.1.3、新建一个自动配置类,关联 Properties 类

@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
public class ReadingAutoConfiguration {@Autowiredprivate ReadingProperties readingProperties;// 若当前IOC容器中没有ReadingService时,提供一个默认的ReadingService实现@Bean@ConditionalOnMissingBean(ReadingService.class)public ReadingService readingService() {return new ReadingServiceImpl(readingProperties);}}

4.1.4、添加一个对外服务接口

@Slf4j
@NoArgsConstructor
@AllArgsConstructor
public class ReadingServiceImpl implements ReadingService {private ReadingProperties readingProperties;@Overridepublic void reading() {log.info("start reading..., type is {}", readingProperties.getType());}
}

4.1.5、注册自动配置类

添加文件 resources/META-INF/spring.factories,其内容为:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.zzc.read.auto.ReadingAutoConfiguration

key 为 EnableAutoConfiguration 类的全限定名,value 为配置类的全限定名

4.1.6、mvn install

执行 mvn install 命令进行打包

4.1.7、引用 starter

4.1.7.1、pom.xml 中引入 starter
<dependency><groupId>com.zzc</groupId><artifactId>read-spring-boot-starter</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>
4.1.7.2、使用 starter
@Slf4j
@RestController
public class TestController {@Autowiredprivate ReadingService readingService;@GetMapping("/testAuto")public void testAuto() {readingService.reading();}
}

结果如下:

在这里插入图片描述

4.2、引入条件装配

4.2.1、修改配置类 ReadingAutoConfiguration

修改配置类 ReadingAutoConfiguration

@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
// 当存在reading.enable属性,且其值为true时,才初始化该Bean
@ConditionalOnProperty(name = "reading.enable", havingValue = "true")
public class ReadingAutoConfiguration {@Autowiredprivate ReadingProperties readingProperties;// 若当前IOC容器中没有ReadingService时,提供一个默认的ReadingService实现@Bean@ConditionalOnMissingBean(ReadingService.class)public ReadingService readingService() {return new ReadingServiceImpl(readingProperties);}}

再重新 mvn install

4.2.2、修改使用 starter

yml 文件添加配置:

reading:enable: true
@RestController
public class TestController {@Autowired(required = false)private ReadingService readingService;@GetMapping("/testAuto")public void testAuto() {readingService.reading();}
}

4.3、重写 starter 的默认配置

之前创建 starter 时,reading.type 默认配置为 reading.type=txt,想重写默认配置也是很简单的,只需要在当前项目 application.yml 中稍作添加:

reading:enable: truetype: json

再次调用,结果如下:

在这里插入图片描述

4.4、重写 starter 默认实现

如果觉得 starter 默认的 ReadingService 实现不够好,那么也可以自定义 ReadingService 实现。因为 starter 构造 ReadingService 实现那里加上了 @ConditionalOnMissingBean(ReadingService.class) 。所以,只需要在当前工程自行实现 ReadingService 接口,并将其注册到 SpringIOC 中,则 starter 中默认 ReadingService 实现将不会被初始化。

添加类 MyReadingServiceImpl

@Slf4j
@Service
public class MyReadingServiceImpl implements ReadingService {@Autowiredprivate ReadingProperties readingProperties;@Overridepublic void reading() {log.info("My reading start reading..., type is {}", readingProperties.getType());}
}

重启项目后,结果如下:

在这里插入图片描述

4.5、实现一个自己的 @EnableXXX

application.yml 配置 reading.enable=true 这样的方式让自动装配生效,其实不够优雅。那么我们也可以像别的 starter 那样提供 @EnableXXX@EnableScheduling@EnableAsync@EnableCaching…)注解,然后在 SpringApplication 启动类加上 @EnableXXX,让我们的自动装配生效

4.5.1、在 starter 中添加一个 selector

创建一个 ReadingSelector,用来替换之前的 ReadingAutoConfiguration

@Configuration
@EnableConfigurationProperties(ReadingProperties.class)
public class ReadingSelector {@Autowiredprivate ReadingProperties readingProperties;@Bean@ConditionalOnMissingBean(ReadingService.class)public ReadingService readingService() {return new ReadingServiceImpl(readingProperties);}}

4.5.2、在 starter 中删除 resources/META-INF/spring.factories 文件

4.5.3、在 starter 中添加一个注解 @EnableReading

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Import: 往Spring IOC中导入一个类
@Import(ReadingSelector.class)
public @interface EnableReading {}

再重新 mvn install

4.5.4、在当前项目中做出修改

①:修改 yml 文件:

reading:type: json

②:修改主启动类,添加注解 @EnableReading

@EnableReading
@SpringBootApplication
public class TestApplication {public static void main(String[] args) {SpringApplication.run(TestApplication.class, args);}}

③:删除类 MyReadingServiceImpl

④:测试接口

@RestController
public class TestController {@Autowiredprivate ReadingService readingService;@GetMapping("/testAuto")public void testAuto() {readingService.reading();}
}

结果如下:

在这里插入图片描述

相关文章:

  • 【并发编程】Redisson 的分布式锁
  • 设计模式系列(1):总览与引导
  • 使用PHP对接印度股票市场API
  • 沐言智语开源Muyan-TTS模型,词错率、语音质量评分都处于开源模型的一线水平,推理速度相当快~
  • 【ns3】安装(包括无网安装)
  • Lua再学习
  • GTS-400 系列运动控制器板卡介绍(二十)---PT 动态FIFO
  • GitHub 趋势日报 (2025年05月10日)
  • 线程池使用ThreadLocal注意事项
  • docker安装superset实践
  • 极新携手火山引擎,共探AI时代生态共建的破局点与增长引擎
  • Linux511SSH连接 禁止root登录 服务任务解决方案 scp Vmware三种模式回顾
  • Kids A-Z安卓版:儿童英语启蒙的优质选择
  • 《异常链机制详解:如何优雅地传递Java中的错误信息?》
  • 嵌入式中屏幕的通信方式
  • LVGL(lv_label实战)
  • 2025御网杯wp(web,misc,crypto)
  • Python异常处理全解析:从基础到高级应用实战
  • 天授强化学习库了解
  • openai接口参数max_tokens改名max-completion-tokens?
  • 继71路之后,上海中心城区将迎来第二条中运量公交
  • 习近平在中拉论坛第四届部长级会议开幕式的主旨讲话(全文)
  • 93岁南开退休教授陈生玺逝世,代表作《明清易代史独见》多次再版
  • 中共中央、国务院印发《生态环境保护督察工作条例》
  • 中国科学院院士徐春明不再担任山东石油化工学院校长
  • 非洲雕刻艺术有着怎样的“变形之美”