如何发布自定义 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...

相关推荐
qq_12498707531 小时前
基于SSM的动物保护系统的设计与实现(源码+论文+部署+安装)
java·数据库·spring boot·毕业设计·ssm·计算机毕业设计
Coder_Boy_1 小时前
基于SpringAI的在线考试系统-考试系统开发流程案例
java·数据库·人工智能·spring boot·后端
2301_818732061 小时前
前端调用控制层接口,进不去,报错415,类型不匹配
java·spring boot·spring·tomcat·intellij-idea
汤姆yu5 小时前
基于springboot的尿毒症健康管理系统
java·spring boot·后端
暮色妖娆丶5 小时前
Spring 源码分析 单例 Bean 的创建过程
spring boot·后端·spring
biyezuopinvip6 小时前
基于Spring Boot的企业网盘的设计与实现(任务书)
java·spring boot·后端·vue·ssm·任务书·企业网盘的设计与实现
JavaGuide7 小时前
一款悄然崛起的国产规则引擎,让业务编排效率提升 10 倍!
java·spring boot
figo10tf7 小时前
Spring Boot项目集成Redisson 原始依赖与 Spring Boot Starter 的流程
java·spring boot·后端
zhangyi_viva7 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
橙露7 小时前
Spring Boot 核心原理:自动配置机制与自定义 Starter 开发
java·数据库·spring boot