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

相关推荐
小小虫码6 小时前
项目中用的网关Gateway及SpringCloud
spring·spring cloud·gateway
拾忆,想起15 小时前
如何选择Spring AOP的动态代理?JDK与CGLIB的适用场景
spring boot·后端·spring·spring cloud·微服务
程序猿零零漆1 天前
SpringCloud系列教程:微服务的未来(十九)请求限流、线程隔离、Fallback、服务熔断
java·spring cloud·微服务
2的n次方_2 天前
Eureka 服务注册和服务发现的使用
spring boot·spring cloud·云原生·eureka·服务发现
奔跑吧邓邓子3 天前
SpringCloud之服务间通信超时:突破微服务的“时间枷锁”
spring cloud·微服务·通信超时
荆州克莱3 天前
mysql重学(一)mysql语句执行流程
spring boot·spring·spring cloud·css3·技术
忆~遂愿3 天前
3大关键点教你用Java和Spring Boot快速构建微服务架构:从零开发到高效服务注册与发现的逆袭之路
java·人工智能·spring boot·深度学习·机器学习·spring cloud·eureka
程序猿零零漆3 天前
SpringCloud系列教程:微服务的未来(十八)雪崩问题、服务保护方案、Sentinel快速入门
spring cloud·微服务·sentinel
程序猿零零漆6 天前
SpringCloud系列教程:微服务的未来(十七)监听Nacos配置变更、更新路由、实现动态路由
java·spring cloud·微服务
customer086 天前
【开源免费】基于SpringBoot+Vue.JS贸易行业crm系统(JAVA毕业设计)
java·vue.js·spring boot·后端·spring cloud·开源