【Nacos】配置动态刷新原理

1,730 阅读4分钟

「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战

一、前言

前文介绍了 Spring Cloud 的三种配置动态刷新机制,先来回顾下:

  1. 通过 Environment 获取的配置:例如 env.getProperty("book.category")
  2. @RefreshScope 注解修饰的类:这些 scope 值为 refresh 的类初始化时经过了特殊处理,当触发 RefreshEvent 事件后会重新构造
  3. @ConfigurationProperties 注解修饰的配置类:这些配置类生效的原因是触发了 EnvironmentChangeEvent 事件

Spring Cloud 配置动态刷新机制基于事件监听机制,涉及以下两个事件:

  1. RefreshEvent 事件:配置刷新事件。

    接收到此事件后会构造一个临时的 ApplicationContext (会加上 BootstrapApplicationListenerConfigFileApplicationListener,这意味着从配置中心和配置文件重新获取配置数据)。构造完毕后,新的 Environment 里的 PropertySource 会跟原先的 Environment 里的 PropertySource 进行对比并覆盖。

  2. EnvironmentChangeEvent 事件:环境变化事件。

    接收到此事件表示应用里的配置数据已经发生改变。EnvironmentChangeEvent 事件里维护着一个配置项 keys 集合,当配置动态修改后,配置值发生变化后的 key 会设置到事件的 keys 集合中。



二、@RefreshScope 原理

@RefreshScope 注解的作用:就是使其修饰的类在收到 RefreshEvent 事件的时候被销毁,再次获取这个类的时候会重性构造,重新构造意味着重性解析表达式,这也代表着获取最新的配置。

主要动作:

  1. 创建:获取类时,构造创建
  2. 销毁:收到 RefreshEvent 事件时,销毁对象

先来看下 @RefreshScope 的定义:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh") // Spring Cloud 新增了 scope 值为 refresh 类型的定义
@Documented
public @interface RefreshScope {
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}

说到 scope,先来看下 Springscope 有哪些:

scope作用
singlon全局只有一个实例。每次获取的相同类型的 Bean 都是同一个实例
prototype每次获取 Bean 都创建一个新的实例
request同一个 Request 作用域内会返回之前保留的 Bean, 否则重新创建 Bean
session同一个 Session 作用域内会返回之前保留的 Bean,否则重新创建 Bean
global sessionPortlet 环境下多个 Portlet 共享的 Session 作用域内会返回之前保留的 Bean,否则重新创建 Bean

Spring Cloud 新增了 scope 值为 refresh 类型的定义,表示 Bean 支持配置动态刷新:

scope作用
refresh支持配置动态刷新

(1)创建

这里就不得不提 Spring 创建 Bean 流程:

对应源码:BeanFactory -> AbstractBeanFactory -> getBean() -> doGetBean()

protected <T> T doGetBean(final String name, final Class<T> requiredType, 
                          final Object[] args, boolean typeCheckOnly)
    throws BeansException {     

    // ... ...

    try {
        // ... ...

        // 创建单例 Bean
        if (mbd.isSingleton()) {
            sharedInstance = getSingleton(beanName, new ObjectFactory<Object>() {
                @Override
                public Object getObject() throws BeansException {
                    try {
                        return createBean(beanName, mbd, args);
                    }
                    // ... ...
                }
            });
            // ... ...
        }
        // 创建原型 Bean
        else if (mbd.isPrototype()) {
            // It's a prototype -> create a new instance.
            Object prototypeInstance = null;
            try {
                beforePrototypeCreation(beanName);
                prototypeInstance = createBean(beanName, mbd, args);
            }
            // ... ...
        }
        // 其他类型 Bean
        else {
            // 由 @RefreshScope 注解可知:
            // 这边就是 scopeName = refresh
            String scopeName = mbd.getScope();
            final Scope scope = this.scopes.get(scopeName);
           
            try {
                Object scopedInstance = scope.get(beanName, new ObjectFactory<Object>() {
                    @Override
                    public Object getObject() throws BeansException {
                        beforePrototypeCreation(beanName);
                        try {
                            return createBean(beanName, mbd, args);
                        }
                        finally {
                            afterPrototypeCreation(beanName);
                        }
                    }
                });
                bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
            }
            // ... ...
        }
    }
	// ... ...
}

由此可看出:

  • 单例和原型 Bean 是硬编码单独处理
  • 其他需要通过 Scope 来处理

对应:RefreshScope refreshScope = scopes.get("refresh", ObjectFactory);

@ManagedResource
public class RefreshScope extends GenericScope implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {
     ......
}

之后就会调用父类 GenericScope.get() ,再之后就是创建对应的 Bean


(2)销毁

Spring Cloud 对这个 scope 值为 refreshBean 做了哪些操作能够使其支持配置动态刷新?

  1. RefreshScope 类实现了 BeanDefinitionRegistryPostProcessor 接口:postProcessBeanDefinitionRegistry()
  2. 对满足 scoperefresh 条件的 BeanDefinition 做了一些修改:把这个 Bean 类型修改成 LockedScopedProxyFactoryBean

对应源码:

@ManagedResource
public class RefreshScope extends GenericScope implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {
    
    // 1. 实现接口
    @Override
	public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
			throws BeansException {
		this.registry = registry;
		super.postProcessBeanDefinitionRegistry(registry);
	}
    
    // 2. LockedScopedProxyFactoryBean 在父类 GenericScope 中
}

每次 RefreshEvent 事件发送完毕之后,都会触发 RefreshScoperefreshAll 方法:

@ManagedResource
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() {
        // 1. 调用父类销毁
		super.destroy();
        // 2. 发送事件:已刷新
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}
}

Tips:销毁之后,下次获取这些 Bean 的时候会重新构造一遍(意味着会重新解析表达式,这也代表着会获取最新的配置)。

由于 LockedScopedProxyFactoryBean 内部的每个操作都会加锁

  • 因此,调用 ConfigurationControllerevent 方法时会获取锁,event 方法内部发送的 RefreshEvent 事件会保触发 RefreshScope#destroy 方法,destroy 方法内部也会获取同一个锁,这就会出现死锁现象
  • 所以,需要将发送 RefreshEvent 事件的方法移到另外一个没有 @RefreshScope 注解的 Controller 中。



三、@ConfigurationProperties 原理

为什么加上 @ConfigurationProperties 也能实现配置动态刷新?

用屁股想想,照猫画虎:

  1. 创建:SpringBean 机制
  2. 销毁:接收 EnvironmentChangeEvent 事件,销毁对象

举个栗子,直接监听这个事件:

项目地址

@Component
class EventReceiver implements ApplicationListener<EnvironmentChangeEvent> {

    @Override
    public void onApplicationEvent(EnvironmentChangeEvent event) {

        System.out.println("eventReceiver: ");
        System.out.println("event: " + event.getKeys());
    }
}

修改配置,日志输出如下:

2022-01-1815-47-02.png

趁胜追击,来看下源码:ConfigurationPropertiesRebinder

2022-01-1815-54-20.png

EnvironmentChangeEvent 的监听器是由 ConfigurationPropertiesRebinder 实现的,其主要逻辑在 rebind 方法中:

@Component
@ManagedResource
public class ConfigurationPropertiesRebinder
		implements ApplicationContextAware, ApplicationListener<EnvironmentChangeEvent> {
    
    // ... ...
    @ManagedOperation
	public boolean rebind(String name) {
		if (!this.beans.getBeanNames().contains(name)) {
			return false;
		}
		if (this.applicationContext != null) {
			try {
				Object bean = this.applicationContext.getBean(name);
				if (AopUtils.isAopProxy(bean)) {
					bean = ProxyUtils.getTargetObject(bean);
				}
				if (bean != null) {
                    
                    // 1. 销毁 Bean
					this.applicationContext.getAutowireCapableBeanFactory()
							.destroyBean(bean);
                    // 2. 再初始化 Bean
					this.applicationContext.getAutowireCapableBeanFactory()
							.initializeBean(bean, name);
					return true;
				}
			}
            
            // ... ...
		}
		return false;
	}
    
    // ... ...
}