写在前面
最近在研究 Nacos 配置刷新时,我遇到了复杂嵌套 Map 刷新失败的问题,写了一篇文章记录:《Nacos 配置刷新踩坑:复杂嵌套 Map 为什么刷不上?》。
loveqq 框架作者 kfyty725 老师在评论区提到:可以使用 ScopeRefreshed 优雅地解决这个问题。
我花了一个下午研究 loveqq 框架源码,发现 ScopeRefreshed 的价值远不止解决泛型擦除——它背后的设计理念更值得学习。
关于 Nacos 长轮询机制,可参考我的另一篇文章:《手写一个 Nacos 配置中心:搞懂长轮询推送机制》。
一、问题:Spring Cloud RefreshScope 的局限
1.1 默认刷新机制
@Component
@RefreshScope
public class ThreadPoolConfig {
private ThreadPoolExecutor executor;
@PostConstruct
public void init() {
this.executor = new ThreadPoolExecutor(...);
}
@PreDestroy
public void destroy() {
this.executor.shutdown(); // 配置刷新时被调用
}
}
配置刷新流程:
- 调用
@PreDestroy- 线程池关闭 - 销毁 Bean
- 下次访问时重建
问题:
- 线程池任务被强制终止
- 内部状态全部丢失
- 无法介入刷新过程
1.2 业务痛点
以 API 密钥配置为例,我们需要:
- 记录审计日志(谁改的)
- 发送变更通知(安全告警)
- 校验配置合法性
- 支持回滚机制
但默认机制下,Bean 直接销毁重建,无法实现这些需求。
核心矛盾:框架要自动刷新,业务要控制流程。
二、解决方案:ScopeRefreshed 回调接口
2.1 设计思想
核心很简单:实现接口就回调,否则走默认流程。
public interface ScopeRefreshed {
void onRefreshed(Environment environment);
}
// 框架决策
if (bean instanceof ScopeRefreshed) {
bean.onRefreshed(environment); // 回调,Bean 存活
} else {
refreshScope.refresh(beanName); // 销毁重建
}
设计原则:
- 控制权下放:开发者掌控刷新逻辑
- 策略分离:接口实现者走回调,否则走默认
- 开闭原则:扩展开放,修改封闭
2.2 接口定义
public interface ScopeRefreshed {
void onRefreshed(Environment environment);
}
设计要点:
- 参数是
Environment:开发者自行读配置 - 无返回值:由开发者决定是否应用
- 职责单一:只负责通知
三、技术实现
3.1 框架核心组件
@Component
public class RefreshScopeEnhancer implements BeanPostProcessor {
// 缓存所有 @RefreshScope Bean
private final Map<String, Object> refreshScopeBeanCache = new ConcurrentHashMap<>();
@Autowired
private org.springframework.cloud.context.scope.refresh.RefreshScope refreshScope;
}
3.2 Bean 拦截与缓存
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
// 检查是否是 @RefreshScope Bean
String scope = beanFactory.getBeanDefinition(beanName).getScope();
if ("refresh".equals(scope)) {
refreshScopeBeanCache.put(beanName, bean); // 缓存
}
return bean;
}
3.3 核心刷新逻辑
public void triggerRefresh(Map<String, Object> updatedProperties) {
for (Map.Entry<String, Object> entry : refreshScopeBeanCache.entrySet()) {
try {
Object bean = entry.getValue();
if (bean instanceof ScopeRefreshed scopeRefreshed) {
scopeRefreshed.onRefreshed(environment); // 回调
} else {
refreshScope.refresh(entry.getKey()); // 重建
}
} catch (Exception e) {
log.error("刷新失败", e); // 异常隔离
}
}
}
3.4 完整流程
sequenceDiagram
participant Nacos
participant Enhancer
participant Bean1 as 实现接口的Bean
participant Bean2 as 普通Bean
Nacos->>Enhancer: 配置变更
Enhancer->>Bean1: onRefreshed()
Bean1->>Bean1: 业务逻辑
Bean1-->>Enhancer: Bean存活
Enhancer->>Bean2: refresh()
Bean2->>Bean2: @PreDestroy
Note over Bean2: Bean销毁
四、实战应用
4.1 ThreadPoolConfig:性能场景
@Component
@RefreshScope
public class ThreadPoolConfig implements ScopeRefreshed {
private int coreSize;
private ThreadPoolExecutor executor;
@Override
public void onRefreshed(Environment environment) {
int newCoreSize = environment.getProperty("threadpool.core-size", Integer.class);
// 动态调整,不重建线程池
if (newCoreSize != coreSize) {
executor.setCorePoolSize(newCoreSize);
this.coreSize = newCoreSize;
}
}
}
价值:
- 线程池不重建
- 任务不中断
- 配置热更新
4.2 SecurityConfig:业务场景(核心)
这是最能体现价值的场景:
@Component
@RefreshScope
@ConfigurationProperties(prefix = "app.security")
public class SecurityConfig implements ScopeRefreshed {
private String apiKey;
private int maxRetries;
private final List<ChangeRecord> changeHistory = new ArrayList<>();
@Override
public void onRefreshed(Environment environment) {
// 1. 备份配置
ConfigSnapshot snapshot = new ConfigSnapshot(apiKey, maxRetries);
// 2. 读取新配置
String newApiKey = environment.getProperty("app.security.api-key");
Integer newMaxRetries = environment.getProperty("app.security.max-retries", Integer.class);
// 3. 校验合法性
if (newApiKey != null && newApiKey.length() < 8) {
log.error("API Key 太短,拒绝变更");
return; // 拒绝
}
// 4. 记录审计日志
log.warn("审计:API Key 从 {} 变更为 {}", mask(apiKey), mask(newApiKey));
// 5. 发送告警
alertService.send("敏感配置变更", String.format("API Key 已变更,时间:%s", LocalDateTime.now()));
// 6. 应用配置
this.apiKey = newApiKey;
if (newMaxRetries != null) {
this.maxRetries = newMaxRetries;
}
// 7. 验证生效
if (!verify()) {
rollback(snapshot); // 回滚
return;
}
// 8. 保存历史
changeHistory.add(new ChangeRecord(LocalDateTime.now(), apiKey, newApiKey));
}
private String mask(String key) {
return key.substring(0, 4) + "****";
}
}
业务价值:
| 需求 | 默认方式 | ScopeRefreshed |
|---|---|---|
| 审计记录 | ❌ | ✅ 完整日志 |
| 变更通知 | ❌ | ✅ 实时告警 |
| 配置校验 | ❌ | ✅ 拒绝非法 |
| 回滚机制 | ❌ | ✅ 自动回滚 |
| 变更历史 | ❌ | ✅ 可追溯 |
4.3 业务流程图
flowchart TD
Start([配置变更]) --> Backup[备份配置]
Backup --> Read[读取新配置]
Read --> Validate{校验合法性}
Validate -->|失败| Reject[拒绝变更]
Reject --> End1([结束])
Validate -->|通过| Audit[记录审计]
Audit --> Notify[发送通知]
Notify --> Apply[应用配置]
Apply --> Verify{验证生效}
Verify -->|失败| Rollback[回滚]
Rollback --> End2([结束])
Verify -->|成功| History[保存历史]
History --> End3([成功])
style Reject fill:#FFB6C1
style Rollback fill:#FFB6C1
style End3 fill:#90EE90
五、设计模式
5.1 策略模式
flowchart TD
Start([配置变更]) --> Check{instanceof?}
Check -->|是| Callback[调用回调]
Check -->|否| Destroy[销毁重建]
Callback --> Keep[Bean存活]
Destroy --> Lost[状态丢失]
style Keep fill:#90EE90
style Lost fill:#FFB6C1
5.2 回调模式
框架定义接口,用户实现逻辑,框架在合适时机调用。这是控制反转(IoC)的体现。
类似的设计:
- Spring 的
InitializingBean.afterPropertiesSet() - Servlet 的
Filter.doFilter()
5.3 开闭原则
- 对扩展开放:实现
ScopeRefreshed接口 - 对修改封闭:框架代码无需改动
六、实际应用场景
6.1 限流配置动态调整
@Component
@RefreshScope
public class RateLimiterConfig implements ScopeRefreshed {
private int requestsPerSecond;
private RateLimiter rateLimiter;
@Override
public void onRefreshed(Environment env) {
int newRate = env.getProperty("rate.limit", Integer.class);
// 校验
if (newRate <= 0 || newRate > 10000) {
log.error("非法限流值: {}", newRate);
return;
}
// 告警
if (newRate < requestsPerSecond) {
alertService.warn("限流降低可能影响业务");
}
// 应用
rateLimiter.setRate(newRate);
this.requestsPerSecond = newRate;
// 通知集群
clusterManager.broadcast("rate.limit", newRate);
}
}
6.2 数据库配置切换
@Component
@RefreshScope
public class DatabaseConfig implements ScopeRefreshed {
private DataSource dataSource;
@Override
public void onRefreshed(Environment env) {
String newUrl = env.getProperty("datasource.url");
// 1. 测试连接
if (!testConnection(newUrl)) {
log.error("连接测试失败,拒绝切换");
return;
}
// 2. 等待请求处理完
waitForRequestsDrain();
// 3. 切换数据源
DataSource oldDataSource = this.dataSource;
this.dataSource = createDataSource(newUrl);
closeDataSource(oldDataSource);
// 4. 发布事件
eventBus.publish(new DatabaseSwitchedEvent(newUrl));
}
}
七、最佳实践
7.1 何时使用
推荐场景:
- 重量级资源(线程池、连接池)
- 业务敏感配置(API 密钥、安全配置)
- 需要审计的配置
- 需要校验的配置
- 需要通知的配置
不推荐场景:
- 简单配置参数
- 无状态配置类
- 不需要特殊处理的配置
7.2 实践建议
-
优先考虑业务价值
- 审计、通知、校验比性能重要
-
做好异常处理
- 备份旧配置
- 校验新配置
- 失败时回滚
-
记录详细日志
- 谁、何时、改了什么
- 变更前后对比
- 变更结果
-
发送适当通知
- 敏感配置要告警
- 变更失败要通知
八、核心价值总结
8.1 不是性能优化
很多人以为 ScopeRefreshed 是为了避免线程池重建的性能损耗。
错!性能提升只是副产品,配置刷新频率很低(几小时一次),几毫秒的差异可以忽略。
8.2 是业务控制
真正的价值在于:把配置刷新从"框架黑盒"变成"业务可控"。
graph LR
subgraph "传统方式"
A1[配置变更] --> B1[Bean销毁]
B1 --> C1[无法介入]
C1 --> D1[重建Bean]
end
subgraph "ScopeRefreshed"
A2[配置变更] --> B2[回调通知]
B2 --> C2[业务逻辑]
C2 --> D2{校验}
D2 -->|失败| E2[拒绝]
D2 -->|成功| F2[审计通知]
F2 --> G2[应用配置]
end
style C1 fill:#FFB6C1
style G2 fill:#90EE90
让我们能够:
- 掌控刷新流程
- 插入业务逻辑(审计、通知、校验、回滚)
- 保持系统状态
- 提升可靠性
- 满足合规要求
8.3 设计思想
- 回调接口:定义契约
- 策略模式:运行时选择
- 控制反转:框架调用用户代码
- 开闭原则:扩展开放,修改封闭
九、总结
在实际项目中,我们常面临选择:用框架默认行为,还是去扩展?
ScopeRefreshed 给了我们启示:优秀的框架应该提供合理的默认行为,同时为特殊需求留下扩展点。
作为开发者,我们应该:
- 理解框架设计思想
- 识别业务真实需求
- 选择合适的扩展方式
- 避免过度设计
性能优化往往不是最重要的,真正重要的是:让系统在业务层面可控、可靠、可追溯。
这才是优秀架构设计的核心。