外卖霸王餐灰度开关:基于Spring Cloud Config+Bus动态刷新踩坑

外卖霸王餐灰度开关:基于Spring Cloud Config+Bus动态刷新踩坑

业务场景与技术选型

"吃喝不愁"App需对新上线的"霸王餐"功能进行城市级灰度发布,例如仅对北京、上海用户开放。系统采用 Spring Cloud Config 作为配置中心,结合 Spring Cloud Bus + RabbitMQ 实现配置变更广播,目标是:修改 Git 中的 feature-toggle.yml 后,所有服务实例自动刷新灰度开关状态,无需重启。

Config Server 配置

bootstrap.yml(Config Server):

yaml 复制代码
server:
  port: 8888
spring:
  application:
    name: config-server
  cloud:
    config:
      server:
        git:
          uri: https://github.com/juwatech/eatfree-config.git
          default-label: main
          search-paths: '{application}'

确保仓库中存在 eatfree-service/feature-toggle.yml

yaml 复制代码
feature:
  free-meal:
    enabled-cities: ["beijing", "shanghai"]
    global-switch: true

客户端依赖与配置

服务模块(如 order-service)引入:

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

bootstrap.yml(客户端):

yaml 复制代码
spring:
  application:
    name: eatfree-service
  cloud:
    config:
      uri: http://localhost:8888
      profile: default
  rabbitmq:
    host: localhost
    port: 5672
    username: guest
    password: guest

management:
  endpoints:
    web:
      exposure:
        include: busrefresh

灰度开关配置类

java 复制代码
package juwatech.cn.config;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@RefreshScope
@ConfigurationProperties(prefix = "feature.free-meal")
public class FreeMealToggle {

    private boolean globalSwitch = false;
    private List<String> enabledCities;

    public boolean isGlobalSwitch() {
        return globalSwitch;
    }

    public void setGlobalSwitch(boolean globalSwitch) {
        this.globalSwitch = globalSwitch;
    }

    public List<String> getEnabledCities() {
        return enabledCities;
    }

    public void setEnabledCities(List<String> enabledCities) {
        this.enabledCities = enabledCities;
    }

    public boolean isEnabledForCity(String cityCode) {
        return globalSwitch && enabledCities != null && enabledCities.contains(cityCode);
    }
}

关键注解:@RefreshScope ------ 使 Bean 在 /actuator/busrefresh 触发时重建。

使用灰度开关的业务代码

java 复制代码
package juwatech.cn.service;

import juwatech.cn.config.FreeMealToggle;
import org.springframework.stereotype.Service;

@Service
public class OrderEligibilityService {

    private final FreeMealToggle freeMealToggle;

    public OrderEligibilityService(FreeMealToggle freeMealToggle) {
        this.freeMealToggle = freeMealToggle;
    }

    public boolean canClaimFreeMeal(String userId, String cityCode) {
        // 其他逻辑略
        return freeMealToggle.isEnabledForCity(cityCode);
    }
}

踩坑1:@RefreshScope 不生效

现象:调用 POST /actuator/busrefresh 后,FreeMealToggle 的字段值未更新。

原因@ConfigurationProperties 类若同时被 @Component@RefreshScope 注解,需确保其被 Spring 正确代理。更稳妥的做法是分离配置类与使用类:

java 复制代码
// 配置类(无 @Component)
@ConfigurationProperties(prefix = "feature.free-meal")
public class FreeMealProperties {
    // fields + getters/setters
}

// 使用类
@Component
@RefreshScope
public class FreeMealToggle {
    private final FreeMealProperties props;

    public FreeMealToggle(FreeMealProperties props) {
        this.props = props;
    }

    public boolean isEnabledForCity(String cityCode) {
        return props.getGlobalSwitch() && props.getEnabledCities().contains(cityCode);
    }
}

// 主启动类启用 @EnableConfigurationProperties
@SpringBootApplication
@EnableConfigurationProperties(FreeMealProperties.class)
public class EatfreeApplication {
    public static void main(String[] args) {
        SpringApplication.run(EatfreeApplication.class, args);
    }
}

踩坑2:Bus 广播未触发

现象:Config Server 收到 Webhook,但客户端未收到刷新事件。

排查步骤

  1. 确认 RabbitMQ 中存在 springCloudBus exchange;
  2. 客户端日志是否包含 Received remote refresh request
  3. 检查 Config Server 是否也引入了 spring-cloud-starter-bus-amqp ------ 必须引入,否则无法转发事件。

Config Server 也需添加:

xml 复制代码
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

并配置相同 RabbitMQ 连接。

踩坑3:List 类型反序列化失败

Git 中配置为:

yaml 复制代码
enabled-cities: beijing,shanghai

导致 List<String> 解析为单个字符串 "beijing,shanghai"

正确写法(YAML 数组):

yaml 复制代码
enabled-cities:
  - beijing
  - shanghai

或使用方括号:

yaml 复制代码
enabled-cities: ["beijing", "shanghai"]

验证流程

  1. 修改 Git 仓库,添加 "guangzhou"enabled-cities

  2. 向 Config Server 发送 POST 请求(模拟 Webhook):

    bash 复制代码
    curl -X POST http://config-server:8888/monitor -H "Content-Type: application/json" -d '{"destination":"eatfree-service:**"}'
  3. 观察客户端日志:Refresh scope refresh requested

  4. 调用业务接口,验证广州用户 now 可参与活动。

本文著作权归吃喝不愁app开发者团队,转载请注明出处!

相关推荐
小园子的小菜3 分钟前
Spring事务失效9大场景(Java面试高频)
java·spring·面试
向前V10 分钟前
Flutter for OpenHarmony数独游戏App实战:胜利弹窗
java·flutter·游戏
WilliamHu.17 分钟前
A2A协议
java·数据结构·算法
JAVA+C语言18 分钟前
如何在Java中实现线程间的通信?
java·大数据·python
这儿有个昵称18 分钟前
Java面试场景:从音视频到微服务的技术深挖
java·spring boot·spring cloud·微服务·面试·kafka·音视频
modelmd19 分钟前
Go、Java 的值类型和引用类型对比
java·golang
移远通信20 分钟前
短信的应用
java·git·python
a努力。20 分钟前
阿里Java面试被问:WebSocket的心跳检测和自动重连实现
java·开发语言·python·websocket·面试·职场和发展·哈希算法
冷雨夜中漫步21 分钟前
Python入门——__init__.py文件作用
android·java·python
deng120428 分钟前
【排序算法总结(1)】
java·算法·排序算法