修改 Nacos 配置时动态刷新 Bean 实例(Kotlin 版)
🏷️ Nacos
如果配置类上使用了 @ConfigurationProperties
注解,在修改 Nacos 配置时会动态刷新属性的值,但如果通过 @Value
注解或者根据配置类创建的 Bean 则不会动态更新。
使用 @RefreshScope
注解则可以在不重启应用的情况下动态刷新 Bean 实例。
@RefreshScope 注解说明
下面是摘自两篇博客中关于 @RefreshScope
注解的详细说明:
被
@RefreshScope
注解的实例,在扫描生成BeanDefiniton
时,注册了两个 Bean 定义,一个beanName
同名、类型是LockedScopedProxyFactoryBean.class
代理工厂 Bean,一个scopedTarget.beanName
的目标 Bean。
当程序使用getBean
获取一个被@RefreshScope
注解的实例时,最终得到的是LockedScopedProxyFactoryBean
的getObject()
返回值,它是一个JdkDynamicAopProxy
代理对象。[1]
@RefreshScope
主要就是基于@Scope
注解的作用域代理的基础上进行扩展实现的,加了@RefreshScope
注解的类,在被 Bean 工厂创建后会加入自己的 refresh scope 这个 Bean 缓存中,后续会优先从 Bean 缓存中获取。RefreshScope
这个 Bean 则是在RefreshAutoConfiguration#refreshScope()
中创建的。[2]
配置中心发生变化后,会收到一个
RefreshEvent
事件,RefreshEventListner
监听器会监听到这个事件。javapublic 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; } }
refresh
方法最终调用destroy
方法,清空之前缓存的 Bean。javapublic 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
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
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
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
interface UserService {
fun getName(): String
}
UserServiceImpl.kt
class UserServiceImpl(private var name: String, private var age: Int) : UserService {
override fun getName(): String {
return name;
}
}
HelloController.kt
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
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
@SpringBootApplication
class SampleNacosApplication
fun main(args: Array<String>) {
runApplication<SampleNacosApplication>(*args)
}
bootstrap.yml
spring:
application:
name: sample-nacos
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: local
file-extension: yml
docker-compose.yml
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"