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 的变更.
相关推荐
华子w90892585930 分钟前
基于 SpringBoot+VueJS 的农产品研究报告管理系统设计与实现
vue.js·spring boot·后端
猴哥源码1 小时前
基于Java+SpringBoot的在线小说阅读平台
java·spring boot
上上迁4 小时前
分布式生成 ID 策略的演进和最佳实践,含springBoot 实现(Java版本)
java·spring boot·分布式
秋千码途4 小时前
小架构step系列07:查找日志配置文件
spring boot·后端·架构
seventeennnnn7 小时前
谢飞机的Java高级开发面试:从Spring Boot到分布式架构的蜕变之旅
spring boot·微服务架构·java面试·分布式系统·电商支付
超级小忍8 小时前
服务端向客户端主动推送数据的几种方法(Spring Boot 环境)
java·spring boot·后端
时间会给答案scidag8 小时前
报错 400 和405解决方案
vue.js·spring boot
Wyc724099 小时前
SpringBoot
java·spring boot·spring
ladymorgana10 小时前
【Spring Boot】HikariCP 连接池 YAML 配置详解
spring boot·后端·mysql·连接池·hikaricp
GJCTYU12 小时前
spring中@Transactional注解和事务的实战理解附代码
数据库·spring boot·后端·spring·oracle·mybatis