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

相关推荐
echoyu.14 小时前
消息队列-初识kafka
java·分布式·后端·spring cloud·中间件·架构·kafka
AAA修煤气灶刘哥15 小时前
缓存这「加速神器」从入门到填坑,看完再也不被产品怼慢
java·redis·spring cloud
AAA修煤气灶刘哥16 小时前
接口又被冲崩了?Sentinel 这 4 种限流算法,帮你守住后端『流量安全阀』
后端·算法·spring cloud
T_Ghost20 小时前
SpringCloud微服务服务容错机制Sentinel熔断器
spring cloud·微服务·sentinel
喂完待续1 天前
【序列晋升】28 云原生时代的消息驱动架构 Spring Cloud Stream的未来可能性
spring cloud·微服务·云原生·重构·架构·big data·序列晋升
惜.己1 天前
Docker启动失败 Failed to start Docker Application Container Engine.
spring cloud·docker·eureka
chenrui3102 天前
Spring Boot 和 Spring Cloud: 区别与联系
spring boot·后端·spring cloud
喂完待续2 天前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
麦兜*3 天前
MongoDB 性能调优:十大实战经验总结 详细介绍
数据库·spring boot·mongodb·spring cloud·缓存·硬件架构