人人都会AI编程

17.1 自定义 Starter 开发流程

更新时间:2026-07-11

当项目发展到一定规模,团队内部会积累一批通用的技术组件(如公司统一的消息工具、加解密库、监控埋点等)。把每一个组件都写成一个独立的 Starter,可以让所有 Spring Boot 工程通过引入一个依赖就能自动获得这些能力,真正做到“一处开发,多处复用”。本节将一步一步展示一个规范的自定义 Starter 从零到可用的完整过程。

17.1.1 Starter 的本质与命名约定

Starter 本身不包含业务逻辑,它的职责是完成两件事:

  1. 把你需要复用的库(jar)带进来(通过 Maven/Gradle 依赖);
  2. 提供自动配置类,将该库的核心 Bean 自动注册到 Spring 容器。

一个标准的 Starter 通常包含两个模块(虽然简单场景可以合二为一)

  • xxx-spring-boot-autoconfigure:包含自动配置类、属性映射类、配置元数据等。
  • xxx-spring-boot-starter:一个近乎空的模块,只依赖上面的 autoconfigure 模块以及功能所需的第三方库。它存在的意义是让使用方只需引入一个 GAV 坐标,无需关心内部的拆解。

官方推荐的命名规范:

  • 官方 Starter:spring-boot-starter-{功能},如 spring-boot-starter-data-redis
  • 自定义 Starter:{功能}-spring-boot-starter,如 mycompany-log-spring-boot-starter

17.1.2 一个实用案例:短信服务 Starter

假设我们要封装一个短信发送服务,对接公司内部的 SMS 网关。目标让业务模块只需引入 sms-spring-boot-starter,并在配置文件中写几个属性,就能在代码中直接注入 SmsClient 使用。

最终效果:

# application.yml
sms:
  enabled: true
  endpoint: https://sms-api.example.com
  app-id: myApp
  secret-key: ${SMS_SECRET}
@Service
public class VerificationService {
    private final SmsClient smsClient;

    public VerificationService(SmsClient smsClient) {
        this.smsClient = smsClient;
    }

    public void sendCode(String phone, String code) {
        smsClient.send(phone, "您的验证码是:" + code);
    }
}

17.1.3 开发 autoconfigure 模块

1. 创建项目结构

sms-spring-boot-autoconfigure
├── pom.xml
└── src/main/java/com/example/sms/
    ├── SmsProperties.java
    ├── SmsClient.java
    └── autoconfigure/
        └── SmsAutoConfiguration.java

2. 定义属性类

使用 @ConfigurationProperties 将配置项映射为类型安全的 Java 对象,同时利用 @Validated 做基本校验。

package com.example.sms;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.validation.annotation.Validated;

import javax.validation.constraints.NotBlank;

@Validated
@ConfigurationProperties(prefix = "sms")
public class SmsProperties {

    private boolean enabled = true;

    @NotBlank(message = "SMS endpoint is required")
    private String endpoint;

    @NotBlank(message = "SMS app-id is required")
    private String appId;

    @NotBlank(message = "SMS secret-key is required")
    private String secretKey;

    // getters and setters...
}

属性类中的 enabled 字段用于提供一个“开关”,可以在配置中关闭整个服务。

3. 编写功能组件

SmsClient 就是真正提供短信发送能力的类,它的初始化依赖上述属性。

package com.example.sms;

public class SmsClient {
    private final SmsProperties properties;

    public SmsClient(SmsProperties properties) {
        this.properties = properties;
    }

    public void send(String phone, String message) {
        if (!properties.isEnabled()) {
            throw new IllegalStateException("SMS client is disabled");
        }
        // 实际调用短信网关
        System.out.printf("Sending to %s: %s (via %s)\n",
                phone, message, properties.getEndpoint());
    }
}

4. 编写自动配置类

这是 Starter 的大脑,决定哪些 Bean 在什么条件下被创建。

package com.example.sms.autoconfigure;

import com.example.sms.SmsClient;
import com.example.sms.SmsProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(SmsProperties.class)
@ConditionalOnProperty(prefix = "sms", name = "enabled", havingValue = "true", matchIfMissing = true)
public class SmsAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public SmsClient smsClient(SmsProperties properties) {
        return new SmsClient(properties);
    }
}

关键注解解读:

  • @EnableConfigurationProperties:激活 SmsProperties,使其可以被注入。
  • @ConditionalOnProperty:当 sms.enabled=true 时(或未配置时,默认 true),配置类生效;否则整个 SmsClient 不会被创建。
  • @ConditionalOnMissingBean:如果容器中已存在用户自定义的 SmsClient,则不再创建默认的,尊重使用方的覆盖。

5. 注册自动配置类

Spring Boot 2.7 之前,需要在 META-INF/spring.factories 中声明:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.sms.autoconfigure.SmsAutoConfiguration

从 Spring Boot 2.7 开始,推荐使用新的方式:在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中直接写入自动配置类的全限定名,每行一个。

com.example.sms.autoconfigure.SmsAutoConfiguration

如果使用较新版本,建议优先使用 .imports 文件。

6. 提供配置元数据(可选但推荐)

为了让 IDE 能够自动提示配置项,可以在 META-INF/spring-configuration-metadata.json 或通过添加 spring-boot-configuration-processor 依赖自动生成。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

编译后在 META-INF 下会生成 additional-spring-configuration-metadata.json 和标准元数据文件,IDE 即可识别 sms 前缀下的所有属性。

17.1.4 创建 Starter 模块

sms-spring-boot-starter 模块极其简单,pom.xml 中只声明对 autoconfigure 模块的依赖(以及 SmsClient 运行时需要的第三方库)。这样,使用方只需引入这个 Starter:

<dependency>
    <groupId>com.example</groupId>
    <artifactId>sms-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

内部 pom 大致为:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
    <relativePath/>
</parent>
<artifactId>sms-spring-boot-starter</artifactId>
<dependencies>
    <dependency>
        <groupId>com.example</groupId>
        <artifactId>sms-spring-boot-autoconfigure</artifactId>
        <version>${project.version}</version>
    </dependency>
    <!-- 若 SmsClient 依赖其他库(如 HTTP 客户端),也在此引入 -->
</dependencies>

至此,一个规范的 Starter 开发完成。将两个模块安装或发布到公司私有仓库,其他项目即可直接使用。

17.1.5 测试 Starter 的完整性

在发布前,我们应编写一个简单的集成测试来验证 Starter 的行为。

在 autoconfigure 模块的 src/test 中,使用 ApplicationContextRunner 可以模拟应用启动并断言 Bean 的存在情况,无需完整的 Spring Boot 测试上下文。

class SmsAutoConfigurationTest {
    private final ApplicationContextRunner contextRunner =
            new ApplicationContextRunner()
                    .withConfiguration(AutoConfigurations.of(SmsAutoConfiguration.class));

    @Test
    void shouldCreateSmsClientWhenPropertyEnabled() {
        contextRunner
                .withPropertyValues(
                    "sms.endpoint=https://test.com",
                    "sms.app-id=test",
                    "sms.secret-key=secret")
                .run(ctx -> {
                    assertThat(ctx).hasSingleBean(SmsClient.class);
                    assertThat(ctx).hasSingleBean(SmsProperties.class);
                });
    }

    @Test
    void shouldNotCreateSmsClientWhenPropertyDisabled() {
        contextRunner
                .withPropertyValues(
                    "sms.enabled=false",
                    "sms.endpoint=https://test.com",
                    "sms.app-id=test",
                    "sms.secret-key=secret")
                .run(ctx -> assertThat(ctx).doesNotHaveBean(SmsClient.class));
    }

    @Test
    void shouldUseUserDefinedClientWhenPresent() {
        SmsClient customClient = new SmsClient(null);
        contextRunner
                .withPropertyValues(
                    "sms.endpoint=https://test.com",
                    "sms.app-id=test",
                    "sms.secret-key=secret")
                .withBean(SmsClient.class, () -> customClient)
                .run(ctx -> assertThat(ctx.getBean(SmsClient.class)).isSameAs(customClient));
    }
}

这些测试确保自动配置在正常、关闭和用户自定义 Bean 的场景下都符合预期。

17.1.6 开发规范与最佳实践

  • 属性前缀隔离:使用自定义前缀(如 sms)避免与企业级配置冲突。
  • 提供合理的默认值:属性类的字段尽可能有默认值(如 enabled=true),降低使用门槛。
  • 通过 @Conditional 精细控制:善用 @ConditionalOnClass(确保某个类在 classpath 下才生效)、@ConditionalOnMissingBean(允许用户覆盖)、@ConditionalOnWebApplication(区分 Web 与非 Web 环境)等条件注解。
  • 对外暴露的类尽量保持稳定:自动配置类和属性类是 Starter 的公开 API,修改时要注意兼容性。
  • 发布前进行严格的依赖收敛:Starter 传递进来的第三方库应与主项目兼容,避免版本冲突。
  • 提供 starter 自身的监控信息:如果涉及连接池、线程池等资源,考虑通过 Actuator 暴露指标。

17.1.7 实战小结

从属性类到自动配置类,从条件注解到 Spring Boot 的注册机制,一个高质量 Starter 背后体现的是 约定优于配置封闭可扩展 的设计原则。掌握它的开发流程,意味着你可以将公司内部的所有“技术积木”标准化、模块化,让团队里的每一个 Spring Boot 项目都能以极低的成本获得最强基础设施能力,真正实现技术资产的复用与传承。