前两天晚上在公司楼下抽烟的时候,我们组的小李跟我吐槽,说他们项目的配置改一次得重启服务,搞得他半夜在群里发“重启完成”截图,特别痛苦。我一听就笑了,这不就是没用 Spring Cloud 的动态刷新嘛。然后我跟他说,其实 SpringBoot 里用@RefreshScope就能很优雅地解决这个问题,不需要写一堆乱七八糟的逻辑。
其实这种问题我也踩过坑,尤其是早几年微服务刚火的时候,很多人一股脑把配置都写到application.yml里面,结果一旦要改数据库密码、限流阈值之类的配置,就只能重启服务。生产环境里重启一次有时候要排窗口期,甚至得熬到半夜,真是折腾人。后来接触了@RefreshScope,算是找到了一个还算优雅的解法。
配置为什么需要动态刷新
先别急着看代码,我们先想个生活里的例子。就好比你家空调的温度,如果每次想调节温度都得拔电源再插上,你肯定受不了。正常情况是不是按遥控器一按,温度立马就变?动态刷新配置其实就是这么个事,让服务不用重启就能感知到最新配置。
那到底什么场景需要?举几个常见的:
数据库连接的用户名密码变了,得马上切换
线上某个接口要临时限流,把阈值调低点
日志级别想改一下,从INFO提到DEBUG
如果每次都靠重启服务,那运维会骂死你。所以啊,配置热更新几乎是微服务项目的标配。
@RefreshScope 到底是干嘛的
说人话:@RefreshScope这个注解就是帮你把 Bean 放到一个“可刷新”的作用域里。你平常的 Spring Bean 默认是单例的,一旦创建就固定住了。而加了@RefreshScope之后,Spring 会在你触发 refresh 事件的时候,销毁老 Bean,重新创建一个新的,把最新的配置值注入进去。
举个最简单的例子,你的配置里有这么一段:
myapp:
greeting: "Hello World"
然后你在代码里写:
@Component
@RefreshScope
public class GreetingService {
@Value("${myapp.greeting}")
private String greeting;
public String sayHello() {
return greeting;
}
}
正常情况下,这个greeting在启动时会被注入。如果你把配置文件里的"Hello World"改成"你好,世界",默认情况下是不会变的。但加了@RefreshScope之后,你只要触发刷新,这个值就会更新。
怎么触发刷新?
这里很多人第一次玩都会卡住。其实触发刷新有几种方式:
手动调用 Actuator 提供的/actuator/refresh接口前提是你引入了spring-boot-starter-actuator,然后配置里允许refresh这个端点。调用这个接口就能刷新。
management:
endpoints:
web:
exposure:
include: refresh
然后用 curl 请求:
curl -X POST http://localhost:8080/actuator/refresh
这个时候,你的 Bean 就会被销毁重建,新的配置值生效。
结合 Spring Cloud Config / Nacos如果你用的是配置中心,比如 Spring Cloud Config、Nacos、Apollo,这些都会帮你自动监听配置变化,然后触发 refresh。 这样就不用手动调接口了,完全自动化。
和配置中心搭配的“骚操作”
我以前在项目里干过一件挺有意思的事:我们系统有一堆限流参数,一开始是写死在配置文件里的,改动一次要重启,很麻烦。后来接了 Nacos 配置中心,然后用@RefreshScope来刷新。效果就是运维只要在 Nacos 控制台改个值,几秒钟之内服务就能感知到,接口限流立马生效。
比如限流的配置:
rate:
limit: 100
代码里写:
@Component
@RefreshScope
public class RateLimitConfig {
@Value("${rate.limit}")
private int limit;
public int getLimit() {
return limit;
}
}
然后拦截器里直接用这个值判断就行。那次上线之后,领导还夸了句“这个很灵活”,我心想这不就是@RefreshScope的功劳嘛。
@ConfigurationProperties 和 @RefreshScope 一起用
这里有个坑我得提醒一下。很多人喜欢用@ConfigurationProperties来绑定配置,比如:
@Component
@ConfigurationProperties(prefix = "rate")
@RefreshScope
public class RateLimitProperties {
private int limit;
// getter setter
}
这样写是完全没问题的。Spring 会在 refresh 的时候重新绑定配置。但是如果你忘了加@RefreshScope,那就呵呵了,刷新不会生效。
我在这几年里踩过一些坑,给你们总结下:
Bean 不在容器里?那刷新个啥有人把配置值直接用@Value注在某个方法参数里,结果刷新没效果。原因就是@RefreshScope必须加在 Bean 上,Spring 才能感知到。
静态变量刷新不了千万别想着@Value注入到static变量里,这玩意不会更新。因为 Spring 根本没办法替你重新赋值。要想更新,老老实实放到 Bean 里。
性能问题每次刷新其实是销毁重建 Bean,如果这个 Bean 初始化特别重(比如要连数据库、初始化缓存),那刷新时会有点卡顿。解决办法是把这些大对象单独拆出去,不要全绑在@RefreshScopeBean 里。
来个完整的 Demo
给你们看一个简单的 SpringBoot Demo,假设我们要动态刷新欢迎语。
application.yml:
myapp:
welcome: "欢迎使用系统"
WelcomeConfig.java:
@Component
@RefreshScope
public class WelcomeConfig {
@Value("${myapp.welcome}")
private String welcome;
public String getWelcome() {
return welcome;
}
}
WelcomeController.java:
@RestController
public class WelcomeController {
@Autowired
private WelcomeConfig welcomeConfig;
@GetMapping("/welcome")
public String welcome() {
return welcomeConfig.getWelcome();
}
}
然后你启动服务,访问/welcome,返回的是 “欢迎使用系统”。这时候你去配置中心或者本地改成 “系统升级啦”,刷新一下配置,再访问接口,马上就变了。
我印象最深的一次,是我们一个支付服务,数据库密码被 DBA 换掉了(安全要求),如果不用动态刷新,那天晚上全体服务得重启,影响一堆交易。幸好当时我们用的是 Spring Cloud + Nacos 配置,配合@RefreshScope,DB 的 DataSource 参数刷新了一下,连个重启都不用,业务照常跑。那一刻我是真的觉得这东西救命。
当然,这玩意也不是银弹。有时候配置变更的范围特别大,比如你换了 Redis 集群地址,这种刷新时会把连接池销毁重建,可能会有短暂的不可用。所以在生产用的时候还是要小心评估,不是所有配置都适合热更新。
总结一下
啰啰嗦嗦说了这么多,其实重点就三句话:
@RefreshScope让 Bean 支持配置热更新
刷新的触发方式要么是 Actuator,要么是配置中心自动触发
用的时候要注意坑,比如静态变量、Bean 初始化开销
如果你们的系统还在靠重启来更新配置,真的该试试这个东西,省心太多了。