SpringBoot——SpringBoot starter 原理及自定义 starter

前言

项目的开发要求是不断进化的,而随着时间以及技术的推移,在项目中除了基本的编程语言外,还需要进行大量的应用服务整合。例如,在项目中使用 MySQL 数据库进行持久化存储,同时会利用 Redis 作为缓存存储,以及使用 RocketMQ 实现异构系统整合服务等。 但在早先使用 Spring 开发的时候,如果想要进行某些服务的整合,常规的做法是引入对应服务的依赖,而后进行一些 XML 的配置和一些组件的定义。而如今在 SpringBoot 中往往各个组件都是以 starter 形式出现的,并且简化了很多配置吗,如 spring-boot-starter-data-redis、rocketmq-spring-boot-starter等。

一、SpringBoot Starter 机制

SpringBoot 中的 starter 是一种非常重要的机制,能够抛弃以前在 Spring 中的繁杂配置,将其统一集成进 starter,应用者只需要在 maven 中引入 starter 依赖,SpringBoot 就能自动扫描到要加载的信息并启动相应的默认配置。starter 让我们摆脱了各种依赖库的处理以及需要配置各种信息的困扰。SpringBoot 会自动通过 classpath 路径下的类发现需要的 Bean,并注册进 IOC 容器。SpringBoot 提供了针对日常企业应用研发各种场景的 spring-boot-starter 依赖模块。所有这些依赖模块都遵循着约定成俗的默认配置,并允许我们调整这些配置,即遵循“约定大于配置”的理念。

Spring Boot Starter本质上是一种依赖管理和自动配置机制。通过引入一个Starter依赖,可以自动引入并配置一系列相关组件,极大地简化开发流程。比如使用spring-boot-starter-web,可以自动引入Spring MVC、Tomcat服务器、JSON解析器等,无需手动逐个配置。

二、Starter的自动配置原理

Spring Boot 会扫描所有 classpath 下 JAR 包中的 META-INF/spring.factories 文件,并按照其中的配置进行自动加载。

2.1 扫描机制

  • Spring Boot 使用 SpringFactoriesLoader 类来加载 spring.factories 文件中的配置。
  • 它会扫描 所有 依赖(包括 libs/ 和 dependencies 目录中的 JAR 包),寻找 META-INF/spring.factories,然后合并所有的配置项。

2.2 合并机制

  • 如果多个 JAR 包(如 spring-boot-starter-web、spring-boot-starter-data-jpa 等)都定义了 spring.factories,Spring Boot 会把 所有 JAR 包中的配置合并,然后执行加载。

示例:

  • spring-boot-autoconfigure.jarMETA-INF/spring.factories 可能包含:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration
  • 另一个 JAR 包 my-custom-starter.jar 可能包含:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.custom.MyCustomAutoConfiguration
  • Spring Boot 会把这两部分合并,最终加载:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
com.example.custom.MyCustomAutoConfiguration

2.3 加载顺序

  • Spring Boot 不保证 spring.factories 配置的执行顺序,如果多个 JAR 包中存在相同的 Key(比如 EnableAutoConfiguration),它们都会被加载,但执行顺序不可预测。
  • 如果需要控制加载顺序,可以使用 @AutoConfigureBefore 和 @AutoConfigureAfter。

2.4 META-INF/spring.factories 文件的作用

META-INF/spring.factories 是 Spring Boot 的自动配置机制 之一,它的主要作用是:

  • 启用 Spring Boot 的自动配置(AutoConfiguration)
  • 自动注册 Spring 组件(如监听器、工厂类等)
  • 扩展 Spring Boot 功能

2.5 spring.factories 作用范围

类型 作用
EnableAutoConfiguration 自动配置 Spring Boot 组件(最常见)
ApplicationContextInitializer Spring 上下文初始化时执行
ApplicationListener 监听 Spring 事件
FailureAnalyzer 处理异常分析,提供更友好的错误信息
EnvironmentPostProcessor 修改 Spring Environment,在启动时修改配置

2.6 SpringFactoriesLoader 如何扫描?

Spring Boot 使用 SpringFactoriesLoader.loadFactoryNames(Class<?> factoryType, ClassLoader classLoader) 方法来扫描所有 META-INF/spring.factories:

public static List<String> loadFactoryNames(Class<?> factoryType, ClassLoader classLoader) {
    String factoryTypeName = factoryType.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
  • loadSpringFactories(classLoader) 方法会 遍历所有 JAR 包,加载 spring.factories。
  • 所有 JAR 包的 spring.factories 文件的内容都会被合并并解析。

2.7 如何避免 spring.factories 冲突?

如果多个 JAR 包的 spring.factories 中包含 相同的 Key,但不同的实现(比如多个 JAR 里都有 EnableAutoConfiguration),可能会导致:

  • 重复加载(多个类都被注册)
  • 冲突(不同的类覆盖了同一个 Bean)

解决方案

  • 1、使用 spring-boot-autoconfigure 提供的新方式
    • Spring Boot 2.7+ 推荐使用 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    • 这样可以更好地控制哪些配置应该被加载,不会导致 spring.factories 的全局合并问题。
  • 2、使用 @ConditionalOnMissingBean 避免重复加载
    • 在自动配置类中使用 @ConditionalOnMissingBean,避免同一个 Bean 被多次注册:
@Configuration
@ConditionalOnClass(MyService.class)
public class MyAutoConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public MyService myService() {
        return new MyService();
    }
}
  • 3、使用 @AutoConfigureBefore 或 @AutoConfigureAfter
    • 控制自动配置类的加载顺序:
@Configuration
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class MyCustomAutoConfiguration {
    // 自定义配置
}

结论

✅ Spring Boot 确实会扫描所有 JAR 包的 META-INF/spring.factories 文件,并合并所有配置项。
✅ 这使得 Starter 组件可以自动注册需要的 Bean 和配置,但也可能导致 Bean 冲突或重复加载的问题。
✅ Spring Boot 2.7+ 推荐使用 AutoConfiguration.imports 作为更可控的替代方案。

三、Starter的命名规范建议

官方并没有强制性的命名约束,但 Spring Boot 社区和官方 Starter 一般遵循一些最佳实践,推荐使用 统一的命名规范,以提高可读性和易用性。

1️⃣ 官方 Starter 的命名规范

Spring Boot 官方 Starter 的命名规则通常是:

spring-boot-starter-<技术名称>

典型官方 Starter 例子

Starter 说明
spring-boot-starter-web Web 开发 Starter,包含 Spring MVC、Tomcat 等
spring-boot-starter-data-jpa JPA 数据访问 Starter
spring-boot-starter-security Spring Security Starter
spring-boot-starter-thymeleaf Thymeleaf 模板引擎 Starter

结论:

  • 所有官方 Starter 以 spring-boot-starter- 开头,然后跟 具体的技术名称。
  • 这让开发者一眼就能知道 它是官方的,并且它支持什么技术。

2️⃣ 自定义 Starter 的推荐命名规则

对于 自定义 Starter,社区推荐使用:

<公司/团队前缀>-starter-<功能名称>

或者:

<组织名称>-spring-boot-starter-<功能名称>

推荐命名方式

命名示例 适用场景
mycompany-starter-dingtalk 企业内部使用,用于钉钉异常通知
com-example-spring-boot-starter-logging 社区 Starter,支持 Spring Boot 日志增强
custom-starter-redis 通用 Starter,适用于 Redis 相关增强功能
awesome-spring-boot-starter-cache 开源项目,提供缓存增强

推荐命名规则

✅ 避免以 spring-boot-starter- 开头

  • 这样可以 避免和官方 Starter 混淆,让用户明确知道它是第三方 Starter。
  • 推荐:mycompany-starter-xxx 或 xxx-spring-boot-starter
  • 不推荐:spring-boot-starter-xxx ❌(可能导致误以为是官方的)

✅ 前缀代表团队或组织名称,例如:

  • alibaba-starter-seata(阿里巴巴 Seata 事务管理)
  • baidu-starter-paddlepaddle(百度 PaddlePaddle AI 框架)
  • mycompany-starter-wechat(企业内部的微信消息推送)

✅ 后缀描述功能

  • starter-dingtalk(钉钉通知)
  • starter-mq(消息队列支持)
  • starter-log-enhancer(日志增强)

3️⃣ 约束和注意事项

官方约束

Spring Boot 不会 强制要求 Starter 必须符合某种命名规则,但官方有一些约定:

  • 1、官方 Starter 需要由 Spring 团队维护(放在 org.springframework.boot 下面)。
  • 2、社区或第三方 Starter 不能以 spring-boot-starter- 开头,避免误导用户。

依赖命名

如果 Starter 需要提供 自动配置,通常需要提供:

  • 核心依赖(starter 依赖)
<artifactId>mycompany-starter-dingtalk</artifactId>
  • 自动配置依赖(autoconfigure 依赖)
<artifactId>mycompany-starter-dingtalk-autoconfigure</artifactId>

这样可以 拆分 Starter 的自动配置和核心功能,提高灵活性。

结论

✅ 官方 Starter 以 spring-boot-starter- 开头,后面跟具体技术,如 spring-boot-starter-web。
✅ 自定义 Starter 推荐 mycompany-starter-xxx 或 xxx-spring-boot-starter,避免与官方混淆。
✅ 组织前缀 可加 mycompany- 或 com-example-,以区分不同的公司或团队。
✅ 最好拆分 starter 和 starter-autoconfigure,增强模块化和可扩展性。

四、自定义starter

4.1 步骤

  • 1、新建两个模块,命名规范: xxx-spring-boot-starter

    • 1.1、xxx-spring-boot-autoconfigure:自动配置核心代码
    • 1.2、xxx-spring-boot-starter:管理依赖
    • 1.3、ps:如果不需要将自动配置代码和依赖项管理分离开来,则可以将它们组合到一个模块中。但 SpringBoot 官方建议将两个模块分开。
  • 2、在 xxx-spring-boot-autoconfigure 项目中

    • 2.1、引入 spring-boot-autoconfigure 的 maven 依赖
    • 2.2、创建自定义的 XXXProperties 类: 这个类的属性根据需要是要出现在配置文件中的。
    • 2.3、创建自定义的类,实现自定义的功能。
    • 2.4、创建自定义的 XXXAutoConfiguration 类:这个类用于做自动配置时的一些逻辑,需将上方自定义类进行 Bean 对象创建,同时也要让 XXXProperties 类生效。
    • 2.5、创建自定义的 spring.factories 文件:在 resources/META-INF 创建一个 spring.factories 文件和 spring-configuration-metadata.json,spring-configuration-metadata.json 文件是用于在填写配置文件时的智能提示,可要可不要,有的话提示起来更友好。spring.factories用于导入自动配置类,必须要有。
  • 3、在 xxx-spring-boot-starter 项目中引入 xxx-spring-boot-autoconfigure 依赖,其他项目使用该 starter 时只需要依赖 xxx-spring-boot-starter 即可

4.2 示例工程

新建工程 jokerku-log-spring-boot-autoconfigure

命名为jokerku-log-spring-boot-autoconfigure,代码编写在这个工程中。

新建工程 jokerku-log-spring-boot-starter

命名为 jokerku-log-spring-boot-starter 工程为一个空工程,只依赖jokerku-log-spring-boot-autoconfigure


jokerku-log-spring-boot-autoconfigure 项目结构:

1.新增注解命令为Log

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
    
    String desc() default "";
}

2.新增LogInterceptor拦截器

@Slf4j
public class LogInterceptor implements HandlerInterceptor {
    
    private static final ThreadLocal<TraceInfo> THREAD_LOCAL = new InheritableThreadLocal();
    
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Log log = handlerMethod.getMethodAnnotation(Log.class);
        
        if (log != null) {
            long start = System.currentTimeMillis();
            TraceInfo traceInfo = new TraceInfo();
            String uri = request.getRequestURI();
            String method = handlerMethod.getMethod().getDeclaringClass() + "#" + handlerMethod.getMethod();
            
            traceInfo.setStart(start);
            traceInfo.setRequestMethod(method);
            traceInfo.setRequestUri(uri);
            
            String traceId = UUID.randomUUID().toString().replaceAll("-", "");
            traceInfo.setTraceId(traceId);
            
            MDC.put(TraceInfo.TRACE_ID, traceId);
            
            THREAD_LOCAL.set(traceInfo);
        }
        
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Log logAnnotation = handlerMethod.getMethodAnnotation(Log.class);
        
        if (logAnnotation != null) {
            long end = System.currentTimeMillis();
            TraceInfo traceInfo = THREAD_LOCAL.get();
            long start = traceInfo.getStart();
            
            log.info("requestUri:{}, requestMethod:{}, 请求耗时:{} ms", traceInfo.getRequestUri(), traceInfo.getRequestMethod(), end - start);
            THREAD_LOCAL.remove();
        }
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}

3.新增config,将 LogInterceptor 加入拦截器中

@Configuration
public class LogAutoConfigure implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LogInterceptor());
    }
}

4.核心点:在resources下新建 META-INF 文件夹,然后创建spring.factories文件

springboot自动装配会读取该配置文件,会将 LogAutoConfigure 这个类自动装配,此时strart包就被装配成功

# springboot自动装配机制 会读取该配置 进行自动装配
org.springframework.boot.autoconfigure.EnableAutoConfiguration = com.jokerku.autoconfigure.LogAutoConfigure

spring.factories 书写规则

spring.factories 由 key = value 结构组成

  • key为接口类,可以使用spring的接口,可以使用自定义的接口(自定义接口实现类必须加上 @component才能被加载)
  • value为需要加载的实现类,不是必须实现interface
org.springframework.boot.autoconfigure.EnableAutoConfiguration = \
  com.jokerku.autoconfigure.LogAutoConfigure

org.springframework.boot.SpringApplicationRunListener= \
  com.jokerku.testListener

org.springframework.context.ApplicationContextInitializer= \
  com.jokerku.testInitializer

#自定义接口
com.jokerku.service.TestService = com.jokerku.service.impl.TestServiceImpl

#如其一个接口有多个实现,如下配置:
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

其他项目引入jokerku-log-spring-boot-starter依赖

<dependency>
    <groupId>com.jokerku</groupId>
    <artifactId>jokeku-log-spring-boot-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

接口添加该注解

自定义 starter 时会可能会用到的注解

  • @Conditional:按照一定的条件进行判断,满足条件给容器注册bean
  • @ConditionalOnMissingBean:给定的在bean不存在时,则实例化当前Bean
  • @ConditionalOnProperty:配置文件中满足定义的属性则创建bean,否则不创建
  • @ConditionalOnBean:给定的在bean存在时,则实例化当前Bean
  • @ConditionalOnClass: 当给定的类名在类路径上存在,则实例化当前Bean
  • @ConditionalOnMissingClass :当给定的类名在类路径上不存在,则实例化当前Bean
  • @ConfigurationProperties:用来把 properties 配置文件转化为bean来使用
  • @EnableConfigurationProperties:使 @ConfigurationProperties 注解生效,能够在 IOC 容器中获取到转化后的 Bean

五、SpringBoot Starter 原理

想要了解 SpringBoot 是如何加载 starter 的(也就是 SpringBoot 的自动装配原理),首先就要从启动类上的 @SpringBootApplication 注解说起。 SpringBoot 通过 SpringApplication.run(App.class, args) 方法启动项目,在启动类上有 @SpringBootApplication 注解,研究上面的原理首先看 @SpringBootApplication 内部的组成结构,如下图:


下面对 @SpringBootConfiguration 和 @EnableAutoConfiguration 进行详解。

5.1 @SpringBootConfiguration 注解

@SpringBootConfiguration 内部结构,如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {...}

@EnableAutoConfiguration 注解

@EnableAutoConfiguration 内部结构,如下:

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

重点看 @AutoConfigurationPackage 注解和 @Import(AutoConfigurationImportSelector.class) 注解。

@AutoConfigurationPackage 注解

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

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

@AutoConfigurationPackage 上包含 @Import(AutoConfigurationPackages.Registrar.class) 注解 看其@Import进来的类AutoConfigurationPackages.Registrar类:

    /**
     * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
     * configuration.
     */
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

        @Override
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
        }

        @Override
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new PackageImports(metadata));
        }

    }

这是一个内部类,继续关注内部的 registerBeanDefinitions 方法中调用的 AutoConfigurationPackages#register 方法

    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(BeanDefinition.ROLE_INFRASTRUCTURE);
            registry.registerBeanDefinition(BEAN, beanDefinition);
        }
    }

register 方法 apidoc 解释:以编程方式注册自动配置包名称,随后的调用将把给定的包名添加到已经注册的包名中。您可以使用此方法手动定义将用于给定BeanDefinitionRegistry的基本包。通常,建议您不要直接调用此方法,而是依赖默认约定,其中包名是从@EnableAutoConfiguration配置类中设置的。


register 方法会生成一个 BasePackages 类型的 BeanDefinition,最终会扫描 basePackages 目录及其子目录下的类,将其全部注入 Spring IOC容器中。 总结: @AutoConfigurationPackage 注解最终会扫描 packageNames 目录下的类并将其全部注入到 Spring IOC 容器中。packageNames 为当前启动类的根目录(当前的根目录为 com.jokerku)。

@Import(AutoConfigurationImportSelector.class) 注解

首先解释 @Import注解在 Spring 中的作用: @Import 通过快速导入的方式实现把实例加入spring的IOC容器中

  • @Import({TestA.class}):这样就会把 TestA 注入进 IOC 容器,生成一个名字为 “com.demo.testA” 的 bean
    所以 AutoConfigurationImportSelector 最终也会被 Spring 加载注入进 IOC 容器,重点关注AutoConfigurationImportSelector 中的内部类 AutoConfigurationGroup。


AutoConfigurationGroup 的 process 方法会调用 getAutoConfigurationEntry 方法,getAutoConfigurationEntry 的作用是获取自动配置项。其底层会通过 getCandidateConfigurations 方法调用SpringFactoriesLoader.loadFactoryNames 去 META-INF/spring.factories 目录下加载 org.springframework.boot.autoconfigure.EnableAutoConfiguration 自动配置项。 最终META-INF/spring.factories 目录下的 自动配置项会被 Spring IOC 容器进行加载。

流程图

SPI机制

SpringBoot 为了更好地达到 OCP 原则(即“对扩展开放,对修改封闭”的原则)通过将自动配置项写在 META-INF/spring.factories 目录下的方式进行加载。而这种方式其实就是 SPI 机制。 在 Java 中 提供了原生的 SPI 机制,Java SPI(Service Provider Interface)实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。SPI是一种为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。JDK SPI 提供了 ServiceLoader 类,入口方法是 ServiceLoader.load() 方法,ServiceLoader 会默认加载 META-INF/services/ 下的文件。 各个框架中都有类似 SPI 机制的实现,如 MyBatis 中的 Plugin、Dubbo 中的 Dubbo-SPI。

参考:
https://www.cnblogs.com/softidea/p/18130709

https://blog.csdn.net/kaka_buka/article/details/146262291

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容