外卖霸王餐灰度开关:基于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开发者团队,转载请注明出处!

相关推荐
雨中飘荡的记忆1 小时前
Spring Security详解
java·spring
小许学java1 小时前
网络编程套接字
java·网络·udp·socket·tcp·套接字
向葭奔赴♡1 小时前
Android AlertDialog实战:5种常用对话框实现
android·java·开发语言·贪心算法·gitee
坐不住的爱码1 小时前
静态资源映射-spring整合
java·spring·状态模式
大佐不会说日语~1 小时前
基于Spring AI Alibaba的AI聊天系统中,流式输出暂停时出现重复插入问题的分析与解决
java·人工智能·spring
0和1的舞者1 小时前
API交互:前后端分离开发实战指南
java·spring·tomcat·web3·maven·springmvc·springweb
一 乐1 小时前
宠物店管理|基于Java+vue的宠物猫店管理管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
天天摸鱼的小学生1 小时前
【Java泛型一遍过】
java·开发语言·windows
骇客野人2 小时前
JAVA获取一个LIST中的最大值
java·linux·list