Skip to content

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

🏷️ Nacos

如果配置类上使用了 @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"

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

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