如何发布自定义 Spring Boot Starter

引言

最近在工作中遇到了 API 限流的需求,市面上虽然有一些现成的解决方案,但总觉得不够灵活,要么功能太重,要么配置复杂。于是萌生了一个想法:为什么不自己开发一个轻量级的限流器Starter呢?既能满足自己的需求,又能学习Spring Boot Starter的开发和发布流程。

这篇文章就来分享一下我是如何从零开始开发一个自定义限流器Spring Boot Starter,并最终成功发布到Maven Central的经历。希望能给有类似需求的同学一些参考。

为什么需要自定义Spring Boot Starter?

在开始之前,我们先来聊聊Spring Boot Starter到底是什么,以及为什么要使用它。

简单来说,Spring Boot Starter是一种特殊的依赖项,它封装了一组相关的依赖和配置,让我们能够快速集成某个功能模块。比如我们常用的 spring-boot-starter-webspring-boot-starter-data-jpa等,它们内部已经帮我们整合了多个相关的依赖,并提供了合理的默认配置。

那么,Spring Boot Starter相比传统的JAR包有什么优势呢?

  1. 自动配置:Starter可以根据类路径下的依赖自动配置相应的组件,大大减少了手动配置的工作量。
  2. 依赖聚合:一个Starter可以聚合多个相关的依赖,避免了手动管理复杂的依赖关系。
  3. 约定优于配置:提供了合理的默认配置,开箱即用。
  4. 条件化配置 :通过 @ConditionalOnProperty@ConditionalOnClass等注解,可以根据条件决定是否加载某些配置。

举个例子,如果我们直接使用Redis进行限流,需要引入 spring-boot-starter-data-redis,然后手动配置RedisTemplate,再编写限流逻辑。而有了限流器Starter后,只需要引入一个依赖,配置几个参数,就可以通过注解的方式实现限流,整个过程变得非常简单。

它背后也是 "行为和数据分离" 的软件设计理念的实际运用。

我的限流器 Starter 设计思路

功能需求

我的限流器Starter需要支持以下功能:

  1. 多种限流算法:令牌桶、漏桶、固定窗口、滑动窗口计数器和滑动窗口日志五种算法
  2. 注解驱动:通过简单的注解就能实现接口限流
  3. Redis存储:支持分布式环境下的限流一致性
  4. 灵活配置:支持全局配置和局部配置
  5. AOP无侵入:基于Spring AOP实现,对业务代码无侵入

核心架构

整个Starter的核心架构如下:

plain 复制代码
┌─────────────────────────────────────┐
│        RateLimiterAutoConfiguration │
│        ──────────────────────────────│
│        • 配置各种限流算法的Bean      │
│        • 条件化加载                  │
└─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────┐
│              限流注解               │
│        ──────────────────────────────│
│        • @FixedWindowRateLimiter    │
│        • @TokenBucketRateLimiter    │
│        • @LeakyBucketRateLimiter    │
└─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────┐
│              限流切面               │
│        ──────────────────────────────│
│        • 基于AOP拦截方法调用        │
│        • 实现具体的限流逻辑         │
└─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────┐
│              存储层                 │
│        ──────────────────────────────│
│        • Redis存储                  │
│        • Lua脚本保证原子性          │
└─────────────────────────────────────┘

关键代码实现

1. 自动配置类

首先,我们需要一个自动配置类,它会在Spring容器启动时自动配置我们的限流器组件:

java 复制代码
@Configuration
@ConditionalOnProperty(prefix = "rate-limiter", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(RateLimiterProperties.class)
@ComponentScan(basePackages = "cn.springboot.starter.ratelimiter")
public class RateLimiterAutoConfiguration {

    @Bean
    public FixedWindowCounterScriptFactory fixedWindowCounterScriptFactory() {
        return new FixedWindowCounterScriptFactory();
    }

    @Bean
    public EnhancedTokenBucketScriptFactory tokenBucketScriptFactory() {
        return new EnhancedTokenBucketScriptFactory();
    }

    // ... 其他算法的ScriptFactory
}

这里有一个关键点需要注意:@ComponentScan(basePackages = "cn.springboot.starter.ratelimiter")注解的作用是让Spring能够扫描到我们Starter中定义的所有组件(包括切面、异常处理器等),这样它们才能被自动注册到Spring容器中。如果没有这个注解,消费端的Spring Boot应用在引入我们的Starter后将无法自动发现和加载这些组件。

这里有几个关键点:

  • @ConditionalOnProperty:只有在配置了 rate-limiter.enabled=true时才加载配置
  • @EnableConfigurationProperties:启用配置属性绑定
  • @ComponentScan:扫描限流器相关的组件
  • @ConditionalOnBean(StringRedisTemplate.class):只有当RedisTemplate存在时才创建Redis相关的组件

2. 配置属性类

为了让用户能够灵活配置限流参数,我们需要定义配置属性类:

java 复制代码
@Data
@ConfigurationProperties(prefix = "rate-limiter")
public class RateLimiterProperties {
    private boolean enabled = true;
    private long defaultLimit = 10;
    private long defaultWindowSize = 60;
    private String defaultMessage = "请求过于频繁,请稍后再试";
    private int maxKeyLength = 255;
}

这样用户就可以在 application.yml中配置:

yaml 复制代码
rate-limiter:
  enabled: true
  default-limit: 20
  default-window-size: 120
  default-message: "访问频率过高,请稍后再试"

3. 限流注解

为了方便使用,我定义了多个限流注解,每种算法对应一个:

java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface FixedWindowRateLimiter {
    String key() default "";
    long limit() default 10;
    long windowSize() default 60;
    int permits() default 1;
    String message() default "请求过于频繁,请稍后再试";
}

4. AOP切面

这是实现限流逻辑的核心部分,通过AOP拦截带有限流注解的方法:

java 复制代码
@Aspect
@Component
@Slf4j
@ConditionalOnProperty(name = "rate-limiter.enabled", havingValue = "true", matchIfMissing = true)
public class FixedWindowRateLimiterAspect extends AbstractRateLimiterAspect {
  
    private final RedisScript<Long> fixedWindowScript;

    public FixedWindowRateLimiterAspect(@Autowired(required = false) StringRedisTemplate redisTemplate,
                                        RateLimiterProperties properties,
                                        @Autowired(required = false) FixedWindowCounterScriptFactory scriptFactory) {
        super(redisTemplate, properties, null);
        this.fixedWindowScript = scriptFactory != null ? scriptFactory.createRateLimitScript() : null;
    }

    @Around("@annotation(rateLimiter)")
    public Object around(ProceedingJoinPoint point, FixedWindowRateLimiter rateLimiter) throws Throwable {
        Method method = getMethod(point);
        String key = generateKey(method, point.getArgs(), rateLimiter.key());

        long startTime = System.nanoTime();
        boolean allowed = checkFixedWindowRateLimit(key, rateLimiter);
        long executionTime = System.nanoTime() - startTime;

        if (!allowed) {
            log.warn("固定窗口限流超出配额,键值: {}", key);
            throw new RateLimitException(rateLimiter.message());
        }

        return point.proceed();
    }

    private boolean checkFixedWindowRateLimit(String key, FixedWindowRateLimiter rateLimiter) {
        if (!checkRedisAndScriptAvailability(key, fixedWindowScript)) {
            return false;
        }

        RedisRateLimitStorage fixedWindowRedisStorage = new RedisRateLimitStorage(redisTemplate, fixedWindowScript);
        return fixedWindowRedisStorage.isAllowed(key, rateLimiter.limit(), rateLimiter.windowSize(), rateLimiter.permits());
    }
}

5. Redis 存储与 Lua 脚本

为了保证限流操作的原子性,我使用了Redis的Lua脚本来实现各种限流算法。以令牌桶算法为例:

java 复制代码
private static String getTokenBucketScript() {
    return """
        -- 令牌桶限流脚本
        -- KEYS[1] = 限流器的键
        -- ARGV[1] = 桶容量(最大令牌数)
        -- ARGV[2] = 填充速率(每秒令牌数)
        -- ARGV[3] = 需要获取的许可数

        local key = KEYS[1]
        local capacity = tonumber(ARGV[1])
        local refill_rate = tonumber(ARGV[2])  -- 每秒令牌数
        local permits = tonumber(ARGV[3])

        -- 从Redis获取当前桶状态(令牌数,上次填充时间)
        local bucket_state = redis.call('HMGET', key, 'tokens', 'last_refill_time')

        local current_tokens, last_refill_time

        if bucket_state[1] and bucket_state[2] then
            current_tokens = tonumber(bucket_state[1])
            last_refill_time = tonumber(bucket_state[2])
        else
            -- 如果桶不存在则初始化
            current_tokens = capacity
            last_refill_time = tonumber(redis.call('TIME')[1])
            redis.call('HMSET', key, 'tokens', current_tokens, 'last_refill_time', last_refill_time)
        end

        -- 获取当前时间
        local current_time = tonumber(redis.call('TIME')[1])

        -- 根据经过的时间计算要添加的令牌数
        local time_elapsed = current_time - last_refill_time
        local tokens_to_add = math.floor(time_elapsed * refill_rate)

        -- 更新令牌数,但不超过容量
        local new_tokens = math.min(capacity, current_tokens + tokens_to_add)

        -- 检查是否有足够的令牌用于请求
        if new_tokens >= permits then
            -- 扣除令牌并更新上次填充时间
            redis.call('HMSET', key, 'tokens', new_tokens - permits, 'last_refill_time', current_time)
            return 1  -- 请求允许
        else
            -- 即使请求被拒绝也要更新上次填充时间(防止滥用)
            redis.call('HMSET', key, 'tokens', new_tokens, 'last_refill_time', current_time)
            return 0  -- 请求拒绝
        end
        """;
}

实现限流器 Spring Boot Starter

1. 项目结构

Spring Boot Starter的项目结构如下:

plain 复制代码
src/
├── main/
│   ├── java/
│   │   └── cn/springboot/starter/ratelimiter/
│   │       ├── config/           # 配置相关类
│   │       ├── core/             # 核心功能类
│   │       │   ├── exception/    # 异常处理
│   │       │   ├── metrics/      # 指标监控(预留目录,目前为空)
│   │       │   └── storage/      # 存储相关(含Redis和Lua脚本)
│   │       │       └── script/   # Lua脚本实现
│   │       └── demo/             # 示例代码
│   └── resources/
│       └── META-INF/
│           └── additional-spring-configuration-metadata.json  # 配置元数据(手动补充的描述信息)
└── target/classes/META-INF/  # 编译后生成
    ├── spring-configuration-metadata.json  # 自动生成的配置元数据
    └── additional-spring-configuration-metadata.json  # 手动补充的配置元数据
└── test/

其中,core包是整个限流器的核心,包含了:

  • exception:限流相关的异常定义
  • metrics:性能指标收集(预留目录,目前为空)
  • storage:存储层实现,包含Redis存储和Lua脚本
  • storage/script:各种限流算法的Lua脚本实现
  • *Aspect:各个限流算法对应的AOP切面(如FixedWindowRateLimiterAspect等,位于core根目录)
  • *RateLimiter:各个限流算法对应的注解(如FixedWindowRateLimiter等,位于core根目录)

2. 关键配置文件

配置元数据文件

Spring Boot提供了配置元数据功能,可以让IDE提供配置提示。配置元数据文件的工作机制如下:

  1. 自动元数据生成spring-boot-configuration-processor在编译时自动扫描 @ConfigurationProperties注解的类,生成 spring-configuration-metadata.json文件,包含基本的配置属性信息,会自动将属性的注释信息作为元数据的 description。
  2. 手动补充元数据 :在 src/main/resources/META-INF/additional-spring-configuration-metadata.json中可以手动编写补充文件,添加更详细的描述信息,或补充处理器无法自动生成的配置属性,这个是可选的。
  3. 元数据合并 :编译时,手动编写的 additional-spring-configuration-metadata.json文件会与自动生成的元数据合并,最终形成完整的配置元数据。

additional-spring-configuration-metadata.json元数据文件:

json 复制代码
{
  "groups": [
    {
      "name": "rate-limiter",
      "type": "cn.springboot.starter.ratelimiter.config.RateLimiterProperties",
      "sourceType": "cn.springboot.starter.ratelimiter.config.RateLimiterProperties"
    }
  ],
  "properties": [
    {
      "name": "rate-limiter.enabled",
      "type": "java.lang.Boolean",
      "description": "是否启用限流",
      "defaultValue": true
    },
    {
      "name": "rate-limiter.default-limit",
      "type": "java.lang.Long",
      "description": "默认限制次数",
      "defaultValue": 10
    }
  ]
}

注册自动配置类

Spring Boot 3.x,在 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件写入:

plain 复制代码
cn.springboot.starter.ratelimiter.config.RateLimiterAutoConfiguration

Spring Boot 2.7及以前版本,在 META-INF/spring.factories文件中注册自动配置类:

properties 复制代码
org.springframework.boot.autoconfigure.AutoConfiguration.imports=\
cn.springboot.starter.ratelimiter.config.RateLimiterAutoConfiguration

3. 依赖管理

pom.xml中,我们需要合理管理依赖:

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

注意将 spring-boot-configuration-processor设置为 optional=true,这样它不会传递给使用Starter的项目。

发布到 Maven Central

发布到Maven Central是一个相对复杂的过程,需要遵循严格的规范。以下是我在实践中总结的完整流程:

1. 准备工作

注册Sonatype账号

首先需要在Sonatype Central Portal注册账号,这是发布到Maven Central的入口。

申请Namespace权限

登录后需要申请命名空间权限。我申请的是 io.github.yuanshenjian-cn,这通常对应你的GitHub用户名或组织名。申请时需要提供项目URL和SCM URL。

重要提醒:这里的命名空间名称需要与你在项目pom.xml中配置的groupId保持一致,因为Sonatype会验证你是否有权在这个命名空间下发布构件。

2. GPG签名设置

发布到 Maven Central 必须使用 GPG签名来保证构件的完整性和真实性。

安装GPG

bash 复制代码
# macOS
brew install gpg

# Ubuntu/Debian
sudo apt-get install gnupg

生成GPG密钥对

bash 复制代码
gpg --gen-key

按照提示填写信息,建议设置一个强密码。

上传公钥到密钥服务器

bash 复制代码
# 推荐使用 keys.openpgp.org
gpg --keyserver keys.openpgp.org --send-keys YOUR_KEY_ID

# 备用服务器
gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID
gpg --keyserver pgp.mit.edu --send-keys YOUR_KEY_ID

3. Maven配置

~/.m2/settings.xml中配置Sonatype认证信息:

xml 复制代码
<settings>
  <servers>
    <server>
      <id>central</id>
      <username>YOUR_SONATYPE_USERNAME</username>
      <password>YOUR_SONATYPE_PASSWORD</password>
    </server>
  </servers>
</settings>

注意:Sonatype现在使用User Token进行认证,需要在Central Portal中生成用户令牌。

4. 项目POM配置

在项目的 pom.xml中添加必要的插件和配置:

xml 复制代码
<build>
    <plugins>
        <!-- 编译插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.11.0</version>
            <configuration>
                <source>17</source>
                <target>17</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
        <!-- 源码插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-source-plugin</artifactId>
            <version>3.3.0</version>
            <executions>
                <execution>
                    <id>attach-sources</id>
                    <goals>
                        <goal>jar-no-fork</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- JavaDoc插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-javadoc-plugin</artifactId>
            <version>3.6.3</version>
            <executions>
                <execution>
                    <id>attach-javadocs</id>
                    <goals>
                        <goal>jar</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- GPG签名插件 -->
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-gpg-plugin</artifactId>
            <version>3.2.4</version>
            <executions>
                <execution>
                    <id>sign-artifacts</id>
                    <phase>verify</phase>
                    <goals>
                        <goal>sign</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        <!-- Central Publishing插件 -->
        <plugin>
            <groupId>org.sonatype.central</groupId>
            <artifactId>central-publishing-maven-plugin</artifactId>
            <version>0.9.0</version>
            <extensions>true</extensions>
            <configuration>
                <publishingServerId>central</publishingServerId>
            </configuration>
        </plugin>
    </plugins>
</build>
<!-- 发布管理 -->
<distributionManagement>
    <snapshotRepository>
        <id>central</id>
        <url>https://central.sonatype.com/content/repositories/snapshots</url>
    </snapshotRepository>
    <repository>
        <id>central</id>
        <url>https://central.sonatype.com/service/local/staging/deploy/maven2/</url>
    </repository>
</distributionManagement>

5. 发布流程

验证构建

bash 复制代码
./mvnw clean verify

确保所有测试通过,且生成了必需的构件(JAR、Sources、Javadoc)。

执行发布

bash 复制代码
export GPG_TTY=$(tty)
./mvnw clean deploy -DskipTests

注意设置 GPG_TTY环境变量,这可以解决在某些终端环境下GPG无法获取密码的问题。

6. 在Central Portal中确认发布

发布完成后,登录Sonatype Central Portal,在"Upload" -> "Components"页面找到刚上传的组件,点击"Publish"按钮完成发布。

发布过程中的踩坑经验

1. GPG签名问题

最常见的问题是GPG签名失败。我遇到过以下几种情况:

  • "gpg: signing failed: Inappropriate ioctl for device" :这是最常见的一种错误,解决方案是设置 GPG_TTY=$(tty)
  • "Invalid signature":通常是因为公钥没有正确上传到PGP服务器,需要等待服务器同步
  • GPG agent问题:有时GPG agent没有正确运行,需要重启

2. 构件验证失败

Maven Central对发布的构件有严格的要求:

  • 必须包含sources和javadoc
  • 所有构件都必须签名
  • POM文件必须包含完整的元数据(许可证、开发者信息、SCM信息等)
  • JavaDoc不能有警告

3. 版本管理

  • 一旦发布到 Maven Central,无法删除或修改已发布的版本
  • 确保版本号唯一且有意义
  • 发布正式版本时,不要使用 -SNAPSHOT后缀

消费端如何使用

发布成功后,用户就可以通过简单的依赖引入来使用我们的限流器:

xml 复制代码
<dependency>
    <groupId>io.github.yuanshenjian-cn</groupId>
    <artifactId>api-rate-limiter-spring-boot-starter</artifactId>
    <version>1.0.7</version>
</dependency>

然后在代码中使用:

java 复制代码
@RestController
public class ApiController {

    // 使用令牌桶算法
    @TokenBucketRateLimiter(
        key = "'api:user:' + #id",         // 限流键,支持 SpEL 表达式
        capacity = 5,                      // 桶容量
        refillRate = 1,                    // 每秒填充1个令牌
        message = "访问频率过高,请稍后再试"
    )
    @GetMapping("/api/user/{id}")
    public String getUser(@PathVariable String id) {
        return "User: " + id;
    }

    // 使用固定窗口算法
    @FixedWindowRateLimiter(
        key = "'api:order:' + #orderId",   // 限流键
        limit = 10,                        // 限制次数
        windowSize = 60,                   // 时间窗口(秒)
        message = "访问频率过高,请稍后再试"
    )
    @PostMapping("/api/order/{orderId}")
    public String createOrder(@PathVariable String orderId) {
        return "Order created: " + orderId;
    }

    // 使用漏桶算法
    @LeakyBucketRateLimiter(
        key = "'api:upload:' + #userId",   // 限流键
        capacity = 5,                      // 桶容量
        leakRate = 2,                      // 每秒处理2个请求
        message = "访问频率过高,请稍后再试"
    )
    @PostMapping("/api/upload")
    public String uploadFile() {
        return "File uploaded successfully";
    }
}

总结

这次跟 Qwen Pair 开发并发布一个 Spring Boot Starter 是一个很好的学习过程,让我深入了解了 Spring Boot 的自动配置机制、AOP编程、Redis应用以及Maven发布流程。

这个项目从构思到发布,经历了需求分析、架构设计、编码实现、测试验证、发布上线等多个阶段。每一个环节都有值得学习的地方,特别是发布到Maven Central的过程,让我对开源软件的发布标准有了更深的认识。

希望这篇分享对你有所帮助,如果有什么问题,欢迎在评论区交流讨论。

本文涉及的完整项目源码可以在GitHub上找到:github.com/yuanshenjia...

相关推荐
haokan_Jia2 小时前
【一、地质灾害气象风险预警互联系统-自由编辑预警区域,打包生成预警成果】
spring boot
计算机毕设指导63 小时前
基于微信小程序的丽江市旅游分享系统【源码文末联系】
java·spring boot·微信小程序·小程序·tomcat·maven·旅游
毕设源码-钟学长4 小时前
【开题答辩全过程】以 基于Spring Boot的社区养老服务管理系统的设计与实现为例,包含答辩的问题和答案
java·spring boot·后端
Coder_Boy_4 小时前
基于SpringAI的在线考试系统-企业级软件研发工程应用规范案例
java·运维·spring boot·软件工程·devops
indexsunny4 小时前
互联网大厂Java面试实战:微服务、Spring Boot与Kafka在电商场景中的应用
java·spring boot·微服务·面试·kafka·电商
SUDO-14 小时前
Spring Boot + Vue 2 的企业级 SaaS 多租户招聘管理系统
java·spring boot·求职招聘·sass
sheji34164 小时前
【开题答辩全过程】以 基于spring boot的停车管理系统为例,包含答辩的问题和答案
java·spring boot·后端
中年程序员一枚5 小时前
多数据源的springboot进行动态连接方案
java·spring boot·后端
w***76555 小时前
SpringBoot集成MQTT客户端
java·spring boot·后端