掘金 后端 ( ) • 2024-04-13 18:23

项目 nacos-property-spring-boot-refresher 通过 Nacos 实现 Property 和 自定义 Scope 类型 Bean 的动态刷新。

示例项目 nacos-property-refresher-spring-boot-starter-examples

众所周知:

  1. NacosSpring Cloud (the "SC") 体系下, 实现了 SC 的标准规范, 天然支持自动刷新。

  2. NacosSpring Boot (the "SB") 其实也是支持自动刷新的。

那为什么还要自己实现呢?

原因:

根据个人的实践经验(可能不正确)。

  1. Nacos 其实自动刷新的是上下文的环境变量 org.springframework.core.env.Environment;
    1. 也就是不刷新 PropertyBean;
    2. 通过 Environment#getProperty 是可以动态感知的。
  2. 通过 @Value("${a.b.c.....z}") @org.springframework.beans.factory.annotation.Autowired 等注解实现依赖注入的并不会动态刷新。

1.怎么使用

1.1.版本号

https://central.sonatype.com/artifact/io.github.photowey/nacos-property-refresher-spring-boot-starter
<!-- ${nacos-property-refresher-starter.version} == ${latest.version} -->
<dependency>
    <groupId>io.github.photowey</groupId>
    <artifactId>nacos-property-refresher-spring-boot-starter</artifactId>
    <version>${nacos-property-refresher-starter.version}</version>
    <type>pom</type>
</dependency>

2.APIs

2.1.DataIds

  • ${spring.application.name}
  • ${spring.application.name}-dev
  • ${spring.application.name}-app
  • ...

2.2.Properties

2.2.1.AppProperties

@Data
//@ConfigurationProperties(prefix = "io.github.photowey.dynamic.property")
public class AppProperties {

    private Cache cache = new Cache();

    @Data
    public static class Cache implements Serializable {

        // ${io.github.photowey.dynamic.property.cache.loader}
        private String loader = "local";
        // ${io.github.photowey.dynamic.property.cache.expired}
        private long expired = TimeUnit.MINUTES.toMillis(5);
        // ${io.github.photowey.dynamic.property.cache.unit}
        private TimeUnit unit = TimeUnit.MILLISECONDS;
    }

    public static String getPrefix() {
        return "io.github.photowey.dynamic.property";
    }
}

2.2.2.HelloProperties

@Data
//@ConfigurationProperties(prefix = "io.github.photowey.static.property")
public class HelloProperties {

    private Cache cache = new Cache();

    @Data
    public static class Cache implements Serializable {

        // ${io.github.photowey.static.property.cache.loader}
        private String loader = "local";
        // ${io.github.photowey.static.property.cache.expired}
        private long expired = TimeUnit.MINUTES.toMillis(5);
        // ${io.github.photowey.static.property.cache.unit}
        private TimeUnit unit = TimeUnit.MILLISECONDS;
    }

    public static String getPrefix() {
        return "io.github.photowey.static.property";
    }
}

2.3.Configuration

// 示例配置 -> 其他方式均可, 核心思想: 被注解 @NacosDynamicRefreshScope 修饰
@Configuration
public class DynamicPropertyConfigure {

	// ...
    
    @Bean
    @NacosDynamicRefreshScope // 添加 Nacos 动态刷新注解 -> 类似于 SC 的 @RefreshScope
    public AppProperties appProperties(Environment environment) {
        return PropertyBinders.bind(environment, AppProperties.getPrefix(), AppProperties.class);
    }

    @Bean
    @NacosDynamicRefreshScope
    public HelloProperties helloProperties(Environment environment) {
        return PropertyBinders.bind(environment, HelloProperties.getPrefix(), HelloProperties.class);
    }
    
    // ...
}

2.4.Beans

@RestController
@RequestMapping("/api/v1/scope")
@NacosDynamicRefreshScope // 添加动态刷新注解
public class ScopeApiController {
	// ...
}

3.核心

3.1.监听器

AbstractNacosDynamicRefreshListener // 已经实现了大部分动态刷新需要的功能

开发者:

  • 1.定义需要监听 data-id 列表 DYNAMIC_DATA_IDS
  • 2.内部会根据实际情况调用两次刷新
    • 接收到 Nacos 的固有事件 NacosConfigReceivedEvent
    • 接收到 Nacos 的笔变更事件 ConfigChangeEvent
      • 当出现 json 解析错误的情况,可能不会触发,如果也期望触发,可能需要结合 NacosConfigReceivedEvent 实现
        • 重写 preRefresh 方法,并返回 true.
  • 3.通过监听器添加对 data-id 的监听
    • addListener
    • addTemplateListener
      • 支持 ${spring.application.name} 这样的占位符
      • 也就是通过 {}-x 占位, 会自动解析成 ${spring.application.name}-x 对应的值.
// @Component || @Bean
public class HelloDynamicNacosConfigListener extends AbstractNacosDynamicRefreshListener {

    // {} -> ${spring.application.name}
    // Register the dataid list that needs to be refreshed dynamically. 
    private static final List<String> DYNAMIC_DATA_IDS = Lists.newArrayList(
            "{}-app"
    );

    @Override
    public void registerListener(Collection<ConfigService> configServices) {
        for (ConfigService configService : configServices) {
            DYNAMIC_DATA_IDS.forEach(dataIdTemplate -> this.addTemplateListener(configService, dataIdTemplate));
        }
    }
    
    // ...this#addListener
    
    // protected boolean preRefresh(NacosConfigReceivedEvent event) {}
    // protected void posRefresh(NacosConfigReceivedEvent event) {}
    // protected boolean preRefresh(ConfigChangeEvent event) {}
    // protected void posRefresh(ConfigChangeEvent event) {}
    
    // protected boolean determineHandleNacosConfigReceivedEvent(NacosConfigReceivedEvent event) {}
    // protected boolean determineHandleConfigChangeEvent(ConfigChangeEvent event) {}
}

4.测试

4.1.Static

ApiController 不动态刷新

@RestController
@RequestMapping("/api/v1")
public class ApiController {

    public static String DYNAMIC_KEY = "io.github.photowey.dynamic.property.cache.loader";
    public static String STATIC_KEY = "io.github.photowey.static.property.cache.loader";

    @Autowired
    private Environment environment;

    @Value("${io.github.photowey.dynamic.property.cache.loader}")
    private String dynamicLoader;

    @Value("${io.github.photowey.static.property.cache.loader}")
    private String staticLoader;

    @Autowired
    private AppProperties appProperties;
    @Autowired
    private HelloProperties helloProperties;

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * http://localhost:9527/api/v1/static/get/dataid/dev
     *
     * @return {@link DynamicValuesDTO}
     */
    @GetMapping("/static/get/dataid/dev")
    public ApiResult<DynamicValuesDTO> dev() {
        DynamicValuesDTO dto = DynamicValuesDTO.builder()
                .value(this.staticLoader)
                .environment(this.tryAcquireLoaderFromEnvironment(STATIC_KEY))
                .property(this.helloProperties.getCache().getLoader())
                .ctxProperty(this.applicationContext.getBean(HelloProperties.class).getCache().getLoader())
                .build();

        return ApiResult.ok(dto);
    }

    /**
     * http://localhost:9527/api/v1/dynamic/get/dataid/app
     *
     * @return {@link DynamicValuesDTO}
     */
    @GetMapping("/dynamic/get/dataid/app")
    public ApiResult<DynamicValuesDTO> app() {
        DynamicValuesDTO dto = DynamicValuesDTO.builder()
                .value(this.dynamicLoader)
                .environment(this.tryAcquireLoaderFromEnvironment(DYNAMIC_KEY))
                .property(this.appProperties.getCache().getLoader())
                .ctxProperty(this.applicationContext.getBean(AppProperties.class).getCache().getLoader())
                .build();

        return ApiResult.ok(dto);
    }

    private String tryAcquireLoaderFromEnvironment(String key) {
        return this.environment.getProperty(key);
    }
}

4.2.Dynamic

ScopeApiController@NacosDynamicRefreshScope 注解修饰, 会自动刷新

  • @Value
  • @Autowired
    • 均会自动刷新
@RestController
@RequestMapping("/api/v1/scope")
@NacosDynamicRefreshScope
public class ScopeApiController {

    public static String DYNAMIC_KEY = "io.github.photowey.dynamic.property.cache.loader";
    public static String STATIC_KEY = "io.github.photowey.static.property.cache.loader";

    @Autowired
    private Environment environment;

    @Value("${io.github.photowey.dynamic.property.cache.loader}")
    private String dynamicLoader;

    @Value("${io.github.photowey.static.property.cache.loader}")
    private String staticLoader;

    @Autowired
    private AppProperties appProperties;
    @Autowired
    private HelloProperties helloProperties;

    @Autowired
    private ApplicationContext applicationContext;

    /**
     * http://localhost:9527/api/v1/scope/static/get/dataid/dev
     *
     * @return {@link DynamicValuesDTO}
     */
    @GetMapping("/static/get/dataid/dev")
    public ApiResult<DynamicValuesDTO> dev() {
        DynamicValuesDTO dto = DynamicValuesDTO.builder()
                .value(this.staticLoader)
                .environment(this.tryAcquireLoaderFromEnvironment(STATIC_KEY))
                .property(this.helloProperties.getCache().getLoader())
                .ctxProperty(this.applicationContext.getBean(HelloProperties.class).getCache().getLoader())
                .build();

        return ApiResult.ok(dto);
    }

    /**
     * http://localhost:9527/api/v1/scope/dynamic/get/dataid/app
     *
     * @return {@link DynamicValuesDTO}
     */
    @GetMapping("/dynamic/get/dataid/app")
    public ApiResult<DynamicValuesDTO> app() {
        DynamicValuesDTO dto = DynamicValuesDTO.builder()
                .value(this.dynamicLoader)
                .environment(this.tryAcquireLoaderFromEnvironment(DYNAMIC_KEY))
                .property(this.appProperties.getCache().getLoader())
                .ctxProperty(this.applicationContext.getBean(AppProperties.class).getCache().getLoader())
                .build();

        return ApiResult.ok(dto);
    }

    private String tryAcquireLoaderFromEnvironment(String key) {
        return this.environment.getProperty(key);
    }
}

4.3.启动

4.3.1.示例接口

http://localhost:9527/api/v1/scope/static/get/dataid/dev
http://localhost:9527/api/v1/scope/dynamic/get/dataid/app

4.3.2.修改 Nacos

当修改 data-id 的值之后, 再次访问即可看到差异。

4.3.3.数据结构

{
  "code": "200",
  "message": "OK",
  "data": {
    "value": "database",
    "environment": "database",
    "property": "database",
    "ctxProperty": "database"
  }
}

5.总结

5.1.核心思想

  • 模仿 SC@RefreshScope 实现
  • Spring Scope bean 类型
  • 监听 Nacos 对应 data-id 的变更.