【Nacos】配置动态刷新

10,452 阅读4分钟

「这是我参与2022首次更文挑战的第2天,活动详情查看:2022首次更文挑战

一、前言

Spring Cloud 基础上体验配置动态刷新。

场景:当配置中心的配置修改,不用重启服务就能刷新服务中的配置。

那么问题来了:

  • Spring Cloud 下如何实现配置动态刷新?
  • 配置动态刷新原理是什么?

Spring Cloud 配置动态刷新的三种使用方式如下:

  1. 通过 Environment 获取的配置:例如 env.getProperty("book.category")
  2. @RefreshScope 注解修饰的类:这些 scope 值为 refresh 的类初始化时经过了特殊处理,当触发 RefreshEvent 事件后会重新构造
  3. @ConfigurationProperties 注解修饰的配置类:这些配置类生效的原因是触发了 EnvironmentChangeEvent 事件

Spring Cloud 配置动态刷新机制基于事件监听机制,涉及以下两个事件:

  1. RefreshEvent 事件:配置刷新事件。

    接收到此事件后会构造一个临时的 ApplicationContext (会加上 BootstrapApplicationListenerConfigFileApplicationListener,这意味着从配置中心和配置文件重新获取配置数据)。构造完毕后,新的 Environment 里的 PropertySource 会跟原先的 Environment 里的 PropertySource 进行对比并覆盖。

  2. EnvironmentChangeEvent 事件:环境变化事件。

    接收到此事件表示应用里的配置数据已经发生改变。EnvironmentChangeEvent 事件里维护着一个配置项 keys 集合,当配置动态修改后,配置值发生变化后的 key 会设置到事件的 keys 集合中。

本文先来了解配置动态刷新三种方式,小结如下:

  1. 通过 Environment 获取的配置:不能刷新 @Value 中的值。
  2. @RefreshScope 注解修饰的类@RefreshScope 注解和需要发送 RefreshEvent 得不在同一个类中,否则可能会产生死锁。
  3. @ConfigurationProperties 注解修饰的配置类:最为省事。


二、举个栗子

栗子分为四个部分:

  1. 实验准备demo,用于基础对比
  2. Environment 案例
  3. @RefreshScope 案例
  4. ConfigurationProperties 案例

配置前要:

  1. 本地文件配置,例如:bootstrap.properties

本文主要是用本地文件。

  1. 基于 Nacos 配置文件

只是在本机文件配置上,多了一步:消息通知(从 nacos-servernacos-client

2021-01-24 11:34:46.152  INFO 17008 --- [168.226.36_8848] c.a.nacos.client.config.impl.CacheData   : [fixed-192.168.226.36_8848] [notify-listener] time cost=348ms in ClientWorker, dataId=my-provider, group=DEFAULT_GROUP, md5=64ba151ad02c0d96b3a26483de661cf0, listener=com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1@3ea8b8a0 

(1)实验准备

对应项目地址

  1. pom 配置
<properties>
    <java.version>1.8</java.version>
    <spring-cloud-alibaba.version>2.2.0.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>
  1. 添加代码
@RestController
class ConfigurationController {

    @Value("${book.author:unknown}")
    String bookAuthor;

    @Value("${book.name:unknown}")
    String bookName;

    @Value("${book.category:unknown}")
    String bookCategory;

    @Autowired
    ApplicationContext applicationContext;

    @GetMapping("/config1")
    public String config() {
        return "bookAuthor=" + bookAuthor +
            "<br/>bookName=" + bookName +
            "<br/>bookCategory=" + bookCategory;
    }

    @GetMapping("/config2")
    public String config2() {

        return  "env.get('book.author')="
            + applicationContext.getEnvironment().getProperty("book.author", "unknown") 
            + "<br/>env.get('book.name')="
            + applicationContext.getEnvironment().getProperty("book.name", "unknown")
            + "<br/>env.get('book.category')="
            + applicationContext.getEnvironment().getProperty("book.category", "unknown");

    }

}
  1. bootstrap.properties 配置
spring.application.name=my-provider
server.port=8080
spring.cloud.nacos.discovery.server-addr=192.168.226.36:8848

book.author=lala
book.name=springcloud
book.category=category
  1. 启动服务,访问 http://localhost:8080/config1http://localhost:8080/config2 ,输出结果如下:
# http://localhost:8080/config1
bookAuthor=lala
bookName=springcloud
bookCategory=category

# http://localhost:8080/config2
env.get('book.author')=lala
env.get('book.name')=springcloud
env.get('book.category')=category
  1. 修改对应的配置

本地文件修改:target/classes/bootstrap.propertiesbook.author=lala 改为 book.author=haha 文件位置如图:

2021-01-2415-11-13.png

book.author=haha
book.name=springcloud
book.category=category
  1. 不重启服务,再次访问两个地址:
# http://localhost:8080/config1
bookAuthor=lala
bookName=springcloud
bookCategory=category

# http://localhost:8080/config2
env.get('book.author')=lala
env.get('book.name')=springcloud
env.get('book.category')=category

结果:发现修改配置文件,均未拿到最新值。


(2)Environment 案例

在上面的 ConfigurationController 中新增 event 方法,用于发送 RefreshEvent 事件:

@GetMapping("/event")
public String event() {

    applicationContext.publishEvent(new RefreshEvent(this, null, "just for test"));

    return "send RefreshEvent";
}

新增事件接收器用于接收 EnvironmentChangeEvent 事件:

@Component
class EventReceiver implements ApplicationListener<EnvironmentChangeEvent> {

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {

        System.out.println(event.getKeys());
    }
}
  1. 配置文件 bootstrap.properties 内容如下:
book.author=haha
book.name=springcloud
book.category=category
  1. 启动服务,访问 http://localhost:8080/event,输出结果如下:
# 控制台 Console 打印如下:
[]
  1. 修改配置 bootstrap.properties
book.author=jiji
book.name=springcloud
book.category=category
  1. 再次访问 http://localhost:8080/event,输出结果如下:
# 控制台 Console 打印如下:
[book.author]
2021-01-24 13:43:12.184  INFO 25479 --- [nio-8080-exec-7] o.s.c.e.event.RefreshEventListener       : Refresh keys changed: [book.author]

# 访问 http://localhost:8080/config1
bookAuthor=haha
bookName=springcloud
bookCategory=category

# 访问 http://localhost:8080/config2
env.get('book.author')=jiji
env.get('book.name')=springcloud
env.get('book.category')=category

结果:发现配置文件修改了。

结论如下:

  1. 本地文件修改并触发了 RefreshEvent 事件之后,Environment 获取的配置是更新后的配置,配置动态刷新后生效。
  2. 类中的属性(@Value修饰的)仍然是旧配置

(3)@RefreshScope 案例

问题:如何修改类中的属性呢(@Value 修饰)?

这时就需要添加一个 @RefreshScope 注解去修饰 ConfigurationController

注意:@RefreshScope 注解和需要发送 RefreshEvent 得不在同一个类中,否则可能会产生死锁。

@RefreshScope
@RestController
class ConfigurationController {
    ...
}

只在类上增加了 @RefreshScope,其他不变。

服务启动后,访问:

# 1. 初始访问
donald@donald-pro:~$ curl localhost:8080/config1
bookAuthor=haha<br/>bookName=九阳神功<br/>bookCategory=categorydonald@donald-pro:~$ 


# 2. 在 nacos 里修改配置
# book.name=北冥神功

# 3. 再次访问,发现 `@Value` 的发生了变化
donald@donald-pro:~$ curl localhost:8080/config1
bookAuthor=haha<br/>bookName=北冥神功<br/>bookCategory=categorydonald@donald-pro:~$ 

结果:@RefreshScope 能使 @Value 中的值动态刷新。


(4)ConfigurationProperties 案例

Spring Cloud@ConfigurationProperties 注解修饰的配置类做了特殊处理。

当触发 Environment ChangeEvent 事件的时候,这些配置类会进行重绑定(rebind) 以获取最新的配置。

实验步骤如下:

  1. 定义配置类
@Setter
@Configuration
@ConfigurationProperties(prefix = "book")
public class BookProperties {

    private String category;
    private String author;
    private String name;

    // 注意:这里需要有 setter 方法
}

初始值访问:

donald@donald-pro:~$ curl localhost:8080/config3
env.get('book.author')=haha<br/>bookAuthor=haha<br/>bookProperties=BookProperties{category='category', author='haha', name='九阳神功'} 
  1. 修改配置 在 nacos 中修改配置
book.name=北冥神功
  1. 运行结果如下:
donald@donald-pro:~$ curl localhost:8080/config3
env.get('book.author')=haha<br/>bookAuthor=haha<br/>bookProperties=BookProperties{category='category', author='haha', name='北冥神功'}

结果:@ConfigurationProperties 也能支持配置动态刷新。