Spring Boot 通过 Nacos 实现 Property 和 自定义 Scope 类型 Bean 的动态刷新

项目 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.版本号

http 复制代码
https://central.sonatype.com/artifact/io.github.photowey/nacos-property-refresher-spring-boot-starter
xml 复制代码
<!-- ${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

java 复制代码
@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

java 复制代码
@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

java 复制代码
// 示例配置 -> 其他方式均可, 核心思想: 被注解 @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

java 复制代码
@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 对应的值.
java 复制代码
// @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 不动态刷新

java 复制代码
@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
    • 均会自动刷新
java 复制代码
@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 复制代码
http://localhost:9527/api/v1/scope/static/get/dataid/dev
http 复制代码
http://localhost:9527/api/v1/scope/dynamic/get/dataid/app

4.3.2.修改 Nacos

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

4.3.3.数据结构

json 复制代码
{
  "code": "200",
  "message": "OK",
  "data": {
    "value": "database",
    "environment": "database",
    "property": "database",
    "ctxProperty": "database"
  }
}

5.总结

5.1.核心思想

  • 模仿 SC@RefreshScope 实现
  • Spring Scope bean 类型
  • 监听 Nacos 对应 data-id 的变更.
相关推荐
HaiFan.1 小时前
SpringBoot 事务
java·数据库·spring boot·sql·mysql
大梦百万秋2 小时前
Spring Boot实战:构建一个简单的RESTful API
spring boot·后端·restful
斌斌_____3 小时前
Spring Boot 配置文件的加载顺序
java·spring boot·后端
苹果醋34 小时前
React系列(八)——React进阶知识点拓展
运维·vue.js·spring boot·nginx·课程设计
等一场春雨5 小时前
springboot 3 websocket react 系统提示,选手实时数据更新监控
spring boot·websocket·react.js
荆州克莱6 小时前
Golang的性能监控指标
spring boot·spring·spring cloud·css3·技术
AI人H哥会Java6 小时前
【Spring】控制反转(IoC)与依赖注入(DI)—IoC容器在系统中的位置
java·开发语言·spring boot·后端·spring
赖龙7 小时前
springboot restful mybatis连接mysql返回日期格式不对
spring boot·mybatis·restful
自律的kkk7 小时前
SpringBoot中使用AOP切面编程实现登录拦截
java·spring boot·aop·切面编程·登录拦截
武昌库里写JAVA7 小时前
【MySQL】MySQL 通过127.0.0.1和localhost登录的区别
spring boot·spring·毕业设计·layui·课程设计