修改 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 原理分析:代理类的创建

相关推荐
luom010213 小时前
springcloud springboot nacos版本对应
spring boot·spring·spring cloud
南昌彭于晏15 小时前
springcloud+openFeign单元测试解决初始化循环依赖的问题
spring·spring cloud·单元测试
⑩-20 小时前
服务注册与发现的原理?Nacos vs Eureka?
spring cloud·云原生·eureka
SmartBrain1 天前
基于SpringAI架构的多智能体协作(进阶版)
人工智能·spring boot·python·spring cloud
qingwufeiyang_5301 天前
Nacos学习笔记
java·笔记·学习·spring cloud·服务发现
小江的记录本2 天前
【Spring Boot—— .yml(YAML)】Spring Boot中.yml文件的基础语法、高级特性、实践技巧
xml·java·spring boot·后端·spring·spring cloud·架构
xiaolingting2 天前
Gateway 网关流控与限流架构指南
spring cloud·架构·gateway·sentinel
唯一世2 天前
Open Feign最佳实践
java·spring cloud
Don.TIk2 天前
SpringCloud学习笔记
笔记·学习·spring cloud
z_鑫2 天前
SpringCloud FeignClient 中 Bean 重复注册冲突解决方案解析
java·spring boot·spring cloud