修改 Nacos 配置时动态刷新 Bean 实例(Kotlin 版)

如果配置类上使用了 @ConfigurationProperties 注解,在修改 Nacos 配置时会动态刷新属性的值,但如果通过 @Value 注解或者根据配置类创建的 Bean 则不会动态更新。

使用 @RefreshScope 注解则可以在不重启应用的情况下动态刷新 Bean 实例。

@RefreshScope 注解说明

下面是摘自两篇博客中关于 @RefreshScope 注解的详细说明:

@RefreshScope 注解的实例,在扫描生成 BeanDefiniton 时,注册了两个 Bean 定义,一个 beanName 同名、类型是 LockedScopedProxyFactoryBean.class 代理工厂 Bean,一个 scopedTarget.beanName 的目标 Bean 。 当程序使用 getBean 获取一个被 @RefreshScope 注解的实例时,最终得到的是 LockedScopedProxyFactoryBeangetObject() 返回值,它是一个 JdkDynamicAopProxy 代理对象。^1^


@RefreshScope 主要就是基于 @Scope 注解的作用域代理的基础上进行扩展实现的,加了 @RefreshScope 注解的类,在被 Bean 工厂创建后会加入自己的 refresh scope 这个 Bean 缓存中,后续会优先从 Bean 缓存中获取。RefreshScope 这个 Bean 则是在 RefreshAutoConfiguration#refreshScope() 中创建的。^2^

  1. 配置中心发生变化后,会收到一个 RefreshEvent 事件,RefreshEventListner 监听器会监听到这个事件。

    java 复制代码
    public class RefreshEventListener implements SmartApplicationListener {
    
        private ContextRefresher refresh;
    
        public void handle(RefreshEvent event) {
            if (this.ready.get()) { // don't handle events before app is ready
                log.debug("Event received " + event.getEventDesc());
                // 会调用 refresh 方法,进行刷新
                Set<String> keys = this.refresh.refresh();
                log.info("Refresh keys changed: " + keys);
            }
        }
    }
    
    public abstract class ContextRefresher {
    
        // 这个是 ContextRefresher 类中的刷新方法
        public synchronized Set<String> refresh() {
            // 刷新 Spring 的 Envirionment 变量配置
            Set<String> keys = refreshEnvironment();
            // 刷新 refresh scope 中的所有 Bean
            this.scope.refreshAll();
            return keys;
        }
    }
  2. refresh 方法最终调用 destroy 方法,清空之前缓存的 Bean 。

    java 复制代码
    public class RefreshScope extends GenericScope
            implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {
    
        @ManagedOperation(description = "Dispose of the current instance of all beans "
                + "in this scope and force a refresh on next method execution.")
        public void refreshAll() {
            // 调用父类的 destroy
            super.destroy();
            this.context.publishEvent(new RefreshScopeRefreshedEvent());
        }
    }
    
    public class GenericScope
            implements Scope, BeanFactoryPostProcessor, BeanDefinitionRegistryPostProcessor, DisposableBean {
    
        @Override
        public void destroy() {
            List<Throwable> errors = new ArrayList<Throwable>();
            Collection<BeanLifecycleWrapper> wrappers = this.cache.clear();
            for (BeanLifecycleWrapper wrapper : wrappers) {
                try {
                    Lock lock = this.locks.get(wrapper.getName()).writeLock();
                    lock.lock();
                    try {
                        // 这里主要就是把之前的 Bean 设置为 null, 就会重新走 createBean 的流程了
                        wrapper.destroy();
                    }
                    finally {
                        lock.unlock();
                    }
                }
                catch (RuntimeException e) {
                    errors.add(e);
                }
            }
            if (!errors.isEmpty()) {
                throw wrapIfNecessary(errors.get(0));
            }
            this.errors.clear();
        }
    }

从上面的说明可以看到,整个动态刷新的过程是基于 Spring 的 ApplicationEvent@Scope 实现的。更多信息见引用的原文。

示例代码

下面的示例代码是基于 Spring 2.7.15 ,用 Kotlin 语言编写的完整示例,记录下来以供今后参考。另外在最后提供了 docker-compose.yml 文件可用于在本地启动 Nacos 服务。

build.gradle.kts

kotlin 复制代码
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.7.15"
    id("io.spring.dependency-management") version "1.0.15.RELEASE"
    kotlin("jvm") version "1.6.21"
    kotlin("plugin.spring") version "1.6.21"
}

group = "me.liujiajia.spring"
version = "0.0.1-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_17
}

repositories {
    mavenCentral()
}

extra["springCloudVersion"] = "2021.0.8"
extra["springCloudAlibabaVersion"] = "2021.1"

dependencies {
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.springframework.cloud:spring-cloud-starter")
    implementation("org.springframework.cloud:spring-cloud-starter-bootstrap")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config")
    implementation("org.projectlombok:lombok:1.18.28")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
}

dependencyManagement {
    imports {
        mavenBom("org.springframework.cloud:spring-cloud-dependencies:${property("springCloudVersion")}")
        mavenBom("com.alibaba.cloud:spring-cloud-alibaba-dependencies:${property("springCloudAlibabaVersion")}")
    }
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs += "-Xjsr305=strict"
        jvmTarget = "17"
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

UserProperties.kt

kotlin 复制代码
import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration

/**
 * 不需要添加 @RefreshScope 注解即可动态更新
 */
@Configuration
@ConfigurationProperties(prefix = "my.user")
class UserProperties {
    var name: String = ""
    var age: Int = 0
}

UserConfig.kt

kotlin 复制代码
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class UserConfig {

    /**
     * 需要添加 @RefreshScope 注解 UserService 才会动态更新
     */
    @Bean
    @RefreshScope
    fun userService(user: UserProperties): UserService {
        return UserServiceImpl(user.name, user.age);
    }
}

UserService.kt

kotlin 复制代码
interface UserService {
    fun getName(): String
}

UserServiceImpl.kt

kotlin 复制代码
class UserServiceImpl(private var name: String, private var age: Int) : UserService {
    override fun getName(): String {
        return name;
    }
}

HelloController.kt

kotlin 复制代码
import org.springframework.beans.factory.annotation.Value
import org.springframework.cloud.context.config.annotation.RefreshScope
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RefreshScope
class HelloController(
    var userService: UserService,
    var userProperties: UserProperties
) {

    /**
     * 类上未添加 @RefreshScope 注解时,该字段不会动态更新。
     */
    @Value("\${my.user.name}")
    private lateinit var name: String;

    @GetMapping("hello")
    fun sayHello(): String {
        return "Hello,${userProperties.name}(${userService.getName()})(${name})!";
    }
}

SampleNacosApplication.kt

kotlin 复制代码
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class SampleNacosApplication

fun main(args: Array<String>) {
    runApplication<SampleNacosApplication>(*args)
}

bootstrap.yml

yaml 复制代码
spring:
  application:
    name: sample-nacos
  cloud:
    nacos:
      config:
        server-addr: localhost:8848
        namespace: local
        file-extension: yml

docker-compose.yml

yaml 复制代码
version: '3.1'
services:
  nacos:
    image: nacos/nacos-server:v2.2.3
    environment:
      - PREFER_HOST_MODE=hostname
      - MODE=standalone
      - NACOS_AUTH_IDENTITY_KEY=serverIdentity
      - NACOS_AUTH_IDENTITY_VALUE=security
      - NACOS_AUTH_TOKEN=SecretKey012345678901234567890123456789012345678901234567890123456789
    volumes:
      - ./standalone-logs:/home/nacos/logs
    ports:
      - "8848:8848"
      - "9848:9848"

Footnotes

  1. 一文带你理解 @RefreshScope 注解实现动态刷新原理

  2. Spring Cloud @RefreshScope 原理分析:代理类的创建

相关推荐
身如柳絮随风扬1 天前
Dubbo 与 Spring Cloud 终极对比:RPC 框架 vs 微服务生态
spring cloud·rpc·dubbo
一个有温度的技术博主1 天前
Spring Cloud 入门与实战:从架构拆分到核心组件详解
spring·spring cloud·架构
uNke DEPH1 天前
SpringCloud Gateway 集成 Sentinel 详解 及实现动态监听Nacos规则配置实时更新流控规则
spring cloud·gateway·sentinel
慕容卡卡1 天前
你所不知道的RAG那些事
java·开发语言·人工智能·spring boot·spring cloud
dLYG DUMS1 天前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Ken_11152 天前
SpringCloud系列(61)--Nacos之服务配置中心的介绍与使用
spring cloud
Ken_11152 天前
SpringCloud系列(62)--Nacos之命名空间、分组和DataID三者之间的关系
spring cloud
Ken_11152 天前
SpringCloud系列(63)--Nacos读取不同配置之DataID配置方案
spring cloud
Devin~Y2 天前
从Spring Boot到Spring AI:音视频AIGC内容社区Java大厂面试三轮连环问(含Kafka/Redis/安全/可观测性答案)
java·spring boot·redis·spring cloud·kafka·spring security·resilience4j
qqty12172 天前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud