Nacos Confg 支持标准 Spring Cloud @RefreshScope特性,即应用订阅某个 Nacos 配置后,当配置内容变化时,Refresh Scope Beans 中的绑定配置的属性将有条件的更新。所谓的条件是指 Bean 必须:

  • 必须条件:Bean 的声明类必须标注 @RefreshScope
  • 二选一条件:
    • 属性(非 static 字段)标注 @Value
    • @ConfigurationPropertiesBean

除此之外,Nacos Confg 也引入了 Nacos Client 底层数据变化监听接口,即 com.alibaba.nacos.api.config.listener.Listener。下面的内容将分别讨论这三种不同的使用场景。

  • 使用 Nacos Config 实现 Bean @Value属性动态刷新
  • 使用 Nacos Config 实现 @ConfigurationPropertiesBean 属性动态刷新
  • 使用 Nacos Config 监听实现 Bean 属性动态刷新

@Value属性动态刷新

基于应用 nacos-config-sample 修改,将引导类 NacosConfigDemo标注@RefreshScope和 @RestController,使得该类变为 Spring MVC REST 控制器,同时具备动态刷新能力,具体代码如下

  1. package com.alibaba.cloud.nacosconfigsample;
  2. import javax.annotation.PostConstruct;
  3. import org.springframework.beans.factory.annotation.Autowired;
  4. import org.springframework.beans.factory.annotation.Value;
  5. import org.springframework.cloud.context.config.annotation.RefreshScope;
  6. import org.springframework.web.bind.annotation.RequestMapping;
  7. import org.springframework.web.bind.annotation.RestController;
  8. @RestController
  9. @RefreshScope
  10. public class NacosConfigDemo {
  11. @Value("${user.name}")
  12. private String userName;
  13. @Value("${user.age}")
  14. private int userAge;
  15. @PostConstruct
  16. public void init() {
  17. System.out.printf("[init] user name : %s , age : %d%n", userName, userAge);
  18. }
  19. @RequestMapping("/user")
  20. public String user() {
  21. return String.format("[HTTP] user name : %s , age : %d", userName, userAge);
  22. }
  23. }

重新编译并启动应用:

  1. mvn clean package && java -jar target/nacos-config-sample-0.0.1-SNAPSHOT.jar

应用启动中,会看到如下输出,说明启动过程中可以读取到服务端的配置:

  1. [init] user name : nacos-config-sample , age : 90

打开新 tab ()并通过命令行访问 REST 资源 /user:

  1. curl http://127.0.0.1:60000/user

你会看到下面的输入:

  1. [HTTP] user name : nacos-config-sample , age : 90

本次请求结果中的 user name 和 age 数据与应用启动时的一致,因为此时 Nacos Server 中的配置数据没变化。

通过nacos控制台调整 nacos-config-sample.properties 配置,将 user.age 从 90 变更为 99: image.png
点击“发布”按钮,观察应用日志变化(部分内容被省略):

  1. c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.1_8848] [data-received] dataId=nacos-config-sample.properties, group=DEFAULT_GROUP, tenant=null, md5=4a8cb29154adb9a0e897e071e1ec8d3c, content=user.name=nacos-config-sample
  2. user.age=99, type=properties
  3. o.s.boot.SpringApplication : Started application in 0.208 seconds (JVM running for 290.765)
  4. o.s.c.e.event.RefreshEventListener : Refresh keys changed: [user.age]
  • 第 1 和 2 行代码是由 Nacos Client 输出,通知开发者具体的内容变化,不难发现,这里没有输出完整的配置内容,仅为变更部分,即配置 user.age。
  • 第 3 行日志似乎让 SpringApplication 重启了,不过消耗时间较短,这里暂不解释,后文将会具体讨论,只要知道这与 Bootstrap 应用上下文相关即可。
  • 最后一行日志是由 Spring Cloud 框架输出,提示开发人员具体变更的 Spring 配置 Property,可能会有多个,不过本例仅修改一处,所以显示单个。

重新访问 REST 资源 /user:

  1. curl http://127.0.0.1:8080/user

会看到如下输出:

  1. [HTTP] user name : nacos-config-sample , age : 99

终端日志显示了这次配置变更同步到了 @Value(“${user.age}”) 属性 userAge 的内容。除此之外,应用控制台也输出了以下内容:

  1. [init] user name : nacos-config-sample , age : 99

而该日志是由 init()方法输出,那么是否说明该方法被框架调用了呢?答案是肯定的。既然 @PostConstruct方法执行了,那么 @PreDestroy方法会不会被调用呢?不妨增加 Spring Bean 销毁回调方法

  1. package com.alibaba.cloud.nacosconfigsample;
  2. import javax.annotation.PostConstruct;
  3. import javax.annotation.PreDestroy;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.beans.factory.annotation.Value;
  6. import org.springframework.cloud.context.config.annotation.RefreshScope;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. @RestController
  10. @RefreshScope
  11. public class NacosConfigDemo {
  12. @Value("${user.name}")
  13. private String userName;
  14. @Value("${user.age}")
  15. private int userAge;
  16. @PostConstruct
  17. public void init() {
  18. System.out.printf("[init] user name : %s , age : %d%n", userName, userAge);
  19. }
  20. @RequestMapping("/user")
  21. public String user() {
  22. return String.format("[HTTP] user name : %s , age : %d", userName, userAge);
  23. }
  24. @PreDestroy
  25. public void destroy() {
  26. System.out.printf("[destroy] user name : %s , age : %d%n", userName, userAge);
  27. }
  28. }

重新编译并启动应用:

  1. mvn clean package && java -jar target/nacos-config-sample-0.0.1-SNAPSHOT.jar

启动过程会看到下面的日志

  1. [init] user name : nacos-config-sample , age : 99

将配置 user.age 内容从 99 调整为 18,观察控制台日志变化:

  1. c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.18848] [data-received] dataId=nacos-config-sample.properties, group=DEFAULTGROUP, tenant=null, md5=e25e486af432c403a16d5fc8a5aa4ab2, content=user.name=nacos-config-sample user.age=18, type=properties
  2. o.s.boot.SpringApplication : Started application in 0.208 seconds (JVM running for 144.467)
  3. [destroy] user name : nacos-config-sample , age : 99
  4. o.s.c.e.event.RefreshEventListener : Refresh keys changed: [user.age]

相较于前一个版本,日志插入了 destroy()方法输出内容,并且Bean 属性 userAge 仍旧是变更前的数据 99。随后,再次访问 REST 资源 /user,其中终端日志:

  1. curl http://127.0.0.1:8080/user
  1. [HTTP] user name : nacos-config-sample , age : 18

应用控制台日志:

  1. [init] user name : nacos-config-sample , age : 18

两者与前一版本并无差异,不过新版本给出了一个现象,即当 Nacos Config 接收到服务端配置变更时,对应的 @RefreshScopeBean 生命周期回调方法会被调用,并且是先销毁,然后由重新初始化。本例如此设计,无非想提醒读者,要意识到 Nacos Config 配置变更对 @RefreshScopeBean 生命周期回调方法的影响,避免出现重复初始化等操作。
注: Nacos Config 配置变更调用了 Spring Cloud API ContextRefresher,该 API 会执行以上行为。同理,执行 Spring Cloud Acutator Endpoint refresh也会使用 ContextRefresher。

@ConfigurationPropertiesBean 属性动态刷新

在应用 nacos-config-sample 新增 User类,并标注 @RefreshScope和 @ConfigurationProperties,代码如下

  1. package com.alibaba.cloud.nacosconfigsample;
  2. import org.springframework.boot.context.properties.ConfigurationProperties;
  3. import org.springframework.cloud.context.config.annotation.RefreshScope;
  4. @RefreshScope
  5. @ConfigurationProperties(prefix = "user")
  6. public class User {
  7. private String name;
  8. private int age;
  9. public String getName() {
  10. return name;
  11. }
  12. public void setName(String name) {
  13. this.name = name;
  14. }
  15. public int getAge() {
  16. return age;
  17. }
  18. public void setAge(int age) {
  19. this.age = age;
  20. }
  21. @Override
  22. public String toString() {
  23. return "User{" +
  24. "name='" + name + '\'' +
  25. ", age=" + age +
  26. '}';
  27. }
  28. }

根据 @ConfigurationProperties的定义, User类的属性绑定到了配置属性前缀 user。下一步,调整引导类,代码如下

  1. package com.alibaba.cloud.nacosconfigsample;
  2. import javax.annotation.PostConstruct;
  3. import javax.annotation.PreDestroy;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.beans.factory.annotation.Value;
  6. import org.springframework.cloud.context.config.annotation.RefreshScope;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. @RestController
  10. @RefreshScope
  11. @EnableConfigurationProperties(User.class)
  12. public class NacosConfigDemo {
  13. @Value("${user.name}")
  14. private String userName;
  15. @Value("${user.age}")
  16. private int userAge;
  17. @Autowired
  18. private User user;
  19. @PostConstruct
  20. public void init() {
  21. System.out.printf("[init] user name : %s , age : %d%n", userName, userAge);
  22. }
  23. @RequestMapping("/user")
  24. public String user() {
  25. return String.format("[HTTP] user name : %s , age : %d", userName, userAge);
  26. }
  27. @PreDestroy
  28. public void destroy() {
  29. System.out.printf("[destroy] user name : %s , age : %d%n", userName, userAge);
  30. }
  31. @RequestMapping("/userObject")
  32. public String userObject() {
  33. return "[HTTP] " + user;
  34. }
  35. }

较前一个版本 NacosConfigSampleApplication实现,主要改动点:

  • 激活 @ConfigurationPropertiesBean @EnableConfigurationProperties(User.class)
  • 通过 @Autowired依赖注入 UserBean
  • 使用 user Bean( toString() 方法替换 user()中的实现

重启应用后,再将 user.age 配置从 18 调整为 99,控制台日志输出符合期望:

  1. [init] user name : nacos-config-sample , age : 18
  1. [init] user name : nacos-config-sample , age : 18
  2. ……
  3. [fixed-127.0.0.18848] [data-received] dataId=nacos-config-sample.properties, group=DEFAULTGROUP, tenant=null, md5=b0f42fac52934faf69757c2b6770d39c, content=user.name=nacos-config-sample
  4. user.age=90, type=properties
  5. ……
  6. [destroy] user name : nacos-config-sample , age : 18
  7. o.s.c.e.event.RefreshEventListener : Refresh keys changed: [user.age]

接下来,访问 REST 资源 /userObject,观察终端日志输出:

  1. curl http://127.0.0.1:8080/user
  1. [HTTP] User{name='nacos-config-sample', age=90}

User Bean 属性成功地变更为 90,达到实战效果。上小节提到 Nacos Config 配置变更会影响 @RefreshScopeBean 的生命周期方法回调。同理,如果为 User增加初始化和销毁方法的话,也会出现行文,不过本次将 User实现 Spring 标准的生命周期接口 InitializingBean和 DisposableBean

  1. package com.alibaba.cloud.nacosconfigsample;
  2. import org.springframework.beans.factory.DisposableBean;
  3. import org.springframework.beans.factory.InitializingBean;
  4. import org.springframework.boot.context.properties.ConfigurationProperties;
  5. import org.springframework.cloud.context.config.annotation.RefreshScope;
  6. @RefreshScope
  7. @ConfigurationProperties(prefix = "user")
  8. public class User implements InitializingBean, DisposableBean {
  9. private String name;
  10. private int age;
  11. public String getName() {
  12. return name;
  13. }
  14. public void setName(String name) {
  15. this.name = name;
  16. }
  17. public int getAge() {
  18. return age;
  19. }
  20. public void setAge(int age) {
  21. this.age = age;
  22. }
  23. @Override
  24. public String toString() {
  25. return "User{" +
  26. "name='" + name + '\'' +
  27. ", age=" + age +
  28. '}';
  29. }
  30. @Override
  31. public void afterPropertiesSet() throws Exception {
  32. System.out.println("[afterPropertiesSet()] " + toString());
  33. }
  34. @Override
  35. public void destroy() throws Exception {
  36. System.out.println("[destroy()] " + toString());
  37. }
  38. }

代码调整后,重启应用,并修改配置(90 -> 19),观察控制台日志输出:

  1. [init] user name : nacos-config-sample , age : 90 c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.18848] [data-received] dataId=nacos-config-sample.properties, group=DEFAULTGROUP, tenant=null, md5=30d26411b8c1ffc1d16b3f9186db498a, content=user.name=nacos-config-sample user.age=19, type=properties
  2. ……
  3. [destroy()] User{name='nacos-config-sample', age=90}
  4. [afterPropertiesSet()] User{name='nacos-config-sample', age=19}
  5. [destroy] user name : nacos-config-sample , age : 90
  6. ……
  7. o.s.c.e.event.RefreshEventListener : Refresh keys changed: [user.age]

不难发现, UserBean 的生命周期方法不仅被调用,并且仍旧是先销毁,再初始化。那么,这个现象和之前看到的 SpringApplication重启是否有关系呢?答案也是肯定的,不过还是后文再讨论。
下一小节将继续讨论怎么利用底层 Nacos 配置监听实现 Bean 属性动态刷新

监听实现 Bean 属性动态刷新

前文曾提及 com.alibaba.nacos.api.config.listener.Listener是 Nacos Client API 标准的配置监听器接口,由于仅监听配置内容,并不能直接与 Spring 体系打通,因此,需要借助于 Spring Cloud Alibaba Nacos Config API NacosConfigManager,代码调整如下

  1. package com.alibaba.cloud.nacosconfigsample;
  2. import javax.annotation.PostConstruct;
  3. import javax.annotation.PreDestroy;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.beans.factory.annotation.Value;
  6. import org.springframework.cloud.context.config.annotation.RefreshScope;
  7. import org.springframework.web.bind.annotation.RequestMapping;
  8. import org.springframework.web.bind.annotation.RestController;
  9. import com.alibaba.cloud.nacos.NacosConfigManager;
  10. import com.alibaba.nacos.api.config.listener.AbstractListener;
  11. @RestController
  12. @RefreshScope
  13. @EnableConfigurationProperties(User.class)
  14. public class NacosConfigDemo {
  15. @Value("${user.name}")
  16. private String userName;
  17. @Value("${user.age}")
  18. private int userAge;
  19. @Autowired
  20. private User user;
  21. @Autowired
  22. private NacosConfigManager nacosConfigManager;
  23. @Bean
  24. public ApplicationRunner runner() {
  25. return args -> {
  26. String dataId = "nacos-config-sample.properties";
  27. String group = "DEFAULT_GROUP";
  28. nacosConfigManager.getConfigService().addListener(dataId, group, new AbstractListener() {
  29. @Override
  30. public void receiveConfigInfo(String configInfo) {
  31. System.out.println("[Listener] " + configInfo);
  32. }
  33. });
  34. };
  35. }
  36. @PostConstruct
  37. public void init() {
  38. System.out.printf("[init] user name : %s , age : %d%n", userName, userAge);
  39. }
  40. @RequestMapping("/user")
  41. public String user() {
  42. return String.format("[HTTP] user name : %s , age : %d", userName, userAge);
  43. }
  44. @PreDestroy
  45. public void destroy() {
  46. System.out.printf("[destroy] user name : %s , age : %d%n", userName, userAge);
  47. }
  48. @RequestMapping("/user")
  49. public String user() {
  50. return "[HTTP] " + user;
  51. }
  52. }

代码主要变化:

  • @Autowired依赖注入 NacosConfigManager
  • 新增 runner()方法,通过 NacosConfigManagerBean 获取 ConfigService,并增加了 AbstractListener( Listener抽象类)实现,监听 dataId = “nacos-config-sample.properties” 和 group = “DEFAULT_GROUP” 的配置内容

重启应用,并将配置 user.age 从 19 调整到 90,观察日志变化:

  1. c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.18848] [data-received] dataId=nacos-config-sample.properties, group=DEFAULTGROUP, tenant=null, md5=b0f42fac52934faf69757c2b6770d39c, content=user.name=nacos-config-sample
  2. user.age=90, type=properties
  3. [Listener] user.name=nacos-config-sample
  4. user.age=90
  5. ……

在第 1 行日志下方,新增了监听实现代码的输出内容,不过这段内容是完整的配置,而非变化的内容。读者请务必注意其中的差异。下一步要解决的是将配置映射到 Bean 属性,此处给出一个简单的解决方案,实现步骤有两个:

  • 将 String 内容转化为 Properties 对象
  • 将 Properties 属性值设置到对应的 Bean 属性

重启应用,并将配置 user.age 从 90 调整到 19,观察日志变化:
[Listener] user.name=nacos-config-sample
user.age= 19
[Before User] User{name=’nacos-config-sample’, age=90}
[After User] User{name=’nacos-config-sample’, age=19}
上述三个例子均围绕着 Nacos Config 实现 Bean 属性动态更新,不过它们是 Spring Cloud 使用场景。如果读者的应用仅使用 Spring 或者 Spring Boot,可以考虑 Nacos Spring 工程, Github 地址:https://github.com/nacos-group/nacos-spring-project,其中 @NacosValue支持属性粒度的更新。