转载

Spring Cloud动态配置实现原理与源码分析

实际项目开发中少不了各种配置,如连接数据库的配置、连接 Redis 集群的配置等,通常我们也会为一个项目部署到每个环境准备不同的配置文件,例如测试环境配置连接测试的数据库。基本上静态配置就已经满足日常需求,但是静态配置缺少灵活性,一经修改就需要重新构建部署应用,同时也缺少安全性,容易泄漏线上环境的配置,所以我们需要一种更灵活更安全的配置方式:动态配置。

动态配置的使用场景并不是为了替换静态配置而出现的,数据库连接配置这些一般都不会改动,所以数据库连接这类配置使用静态配置还是动态配置都没有多大影响。对于那些变动频率高的配置,才会迫切去使用动态配置。例如支付页面展示的支付方式,当第三方支付公司升级服务时,就可以暂时隐藏掉该支付方式;例如集群环境下控制哪些节点做哪些事情;例如控制接口降级、路由修改等等。

实现动态配置的方式很简单,我们可以将配置写到一个专门用来做动态配置的数据库,又或者使用其它的持久化存储方式,然后在代码中定时查看配置有没有更新,有更新就替换旧的配置,然后做一些配置更新后的操作。也可以将实现动态配置的逻辑封装为一个 jar 包,实现代码复用。

因为动态配置有它存在的意义,所以 Spring Cloud 也为我们封装了大部分的实现动态配置的逻辑,让我们使用动态配置更方便。而具体的配置信息存储在哪、怎么获取,这些则交给配置中心去实现,如 NacosDiamondDisconf

本篇从源码分析 Spring Cloud 实现动态配置的原理。 Spring Cloud 实现动态配置需要结合 Spring 源码分析。

目录:

Spring Cloud
@RefreshScope
Spring Cloud

Spring Cloud动态配置的使用方式

Spring Cloud 项目中,无论你使用何种配置中心,使用动态配置功能的方式都可以是一种,我们来看一个使用动态配置的例子。

@Component
@ConfigurationProperties(prefix = "sck-demo")
@RefreshScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class DemoProps {
    private String message;
}
复制代码

DemoProps 类省略了 getset 方法。 DemoProps 类使用 @Component 注解和 @ConfigurationProperties 注解声明为用于装载配置的 bean@RefreshScope 注解则用于声明该 beanscope 以及代理模式 ScopedProxyMode

为了便于理解,我们将这类用于装载配置的类称为 Properties 类,这类用于装载配置的 bean 称为动态配置 bean

我们常见的 scopesingleton (单例)、 prototype (原型),当然还有其它的,而今天我们要学习一个新的 scoperefresh@RefreshScope 注解类的源码如下。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
	ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
复制代码

@RefreshScope 注解也被一个 @Scope 注解注释,这就相当于是两个注解的结合使用。如源码所示,当我们不配置 @RefreshScope 注解的 proxyMode 属性时,默认使用的代理模式为 TARGET_CLASS

为什么使用 @RefreshScope 注解就能让一个动态配置 bean 实现动态装载配置呢?这是第一个等待我们从源码中寻找答案的问题。

使用@RefreshScope可能会遇到的问题

Properties 类添加 @RefreshScope 注解的目的是声明动态配置 Beanscoperefresh ,以及声明 Bean 的代理模式( ScopedProxyMode )。

代理模式 ScopedProxyMode 的可取值为:

  • NO :不创建代理类;
  • DEFAULT :其作用通常等于 NO
  • INTERFACES :创建一个 JDK 动态代理类来实现目标对象的类的所有接口;
  • TARGET_CLASS :使用 Cglib 为目标对象的类创建一个代理类,这是 @RefreshScope 使用的默认值;

其中 INTERFACES 代理模式不适用于动态配置 Bean ,因为 Properties 类没有实现任何接口,如果强行给 @RefreshScope 注解配置代理模式使用 INTERFACESSpring 将会抛出异常。

当我们配置 @RefreshScopeproxyMode 属性使用默认的 TARGET_CLASS 代理模式时,我们可能会遇到获取该 Bean 的属性为 Null 的情况,这是因为我们在其它 Bean 中使用 @Resource@Autowired 注解方式引用的对象是动态代理对象,即使用 Cglib 生成的动态代理类的实例。所以我们只能通过 get 方法去获取对象的字段的值,这是我们在使用动态配置时需要注意的。

当我们配置 @RefreshScopeproxyMode 属性使用 NO 或者 DEFAULT 代理模式时,如果使用 @Resource@Autowired 注解方式方式引用对象,那么动态配置就会失效,也就是动态修改配置后拿到的还是旧的配置。这是因为 @RefreshScope 注解会将 Beanscope 声明为 refresh ,所以对象不是单例的。

当配置改变时, Spring Cloud 的实现是将动态配置 Bean 销毁再创建新的 Bean ,由于是在单例的 Bean 中使用 @Resource@Autowired 注解方式引用该对象,单例 Bean 在初始化时就已经为字段赋值,在单例 Bean 的生命周期内都不会再刷新 bean 字段的引用,所以单例 Bean 就会一直引用一个旧的动态配置 bean ,自然就无法感知配置改变了。

为什么调用代理对象的 get 方法就能获取到新的配置,以及当配置改变时 Spring Cloud 的实现是将动态配置 Bean 销毁再创建新的 Bean 这句怎么理解?这是第二个等待我们从源码中寻找答案的问题。

我们将带着这两个问题从源码中寻找答案。

从源码分析Spring Cloud动态配置的实现原理

根据前面的分析,我们不妨假设:当使用 @RefreshScope 注解配置 Properties 类的代理模式为 TARGET_CLASS 时,被 @RefreshScope 声明的动态配置 bean 将会是一个特殊的动态代理对象,在每次调用该动态代理对象的方法时,都是根据目标对象的 beanName 或者类型从 bean 工厂中获取 bean ,而 bean 不是单例的,所以每次获取都创建新的。这样也就能解释得清为什么使用 @Resource@Autowired 注解如果注入的对象是代理对象就能通过 get 方法获取到字段的最新值。

首先,我们可以在代码中添加如下配置,将 cglib 生成的动态代理输出到文件。

public class App{
    static {
        System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/tmp");
    }
}
复制代码

以前面例子的 DemoProps 类为例, cglib 生成的动态代理类如下:

public class DemoProps$$EnhancerBySpringCGLIB$$593bbd8b extends DemoProps 
            implements ScopedObject, Serializable,
            AopInfrastructureBean, SpringProxy, 
            Advised, Factory {
            // .......
}
复制代码

因为没什么特别的,所以代码就省略了。我们只需要记住, Spring 为使用 @RefreshScope 声明且代理模式为 TARGET_CLASS 的类生成的动态代理类实现了 Advised 接口( AOP 的“通知”或者说是“增强”)。

cglib 生成的动态代理类找不到突破口,那么我们只能从 Spring 扫描 bean 开始了,看下哪些地方使用到 @RefreshScope 注解。 Spring 扫描 bean 的源码在 ClassPathBeanDefinitionScanner 类的 doScan 方法,源码如下图所示。

Spring Cloud动态配置实现原理与源码分析

Spring 扫描 bean 就是将被 @Component 这类注解注释的类扫描出来并生成 BeanDefinitionSpring 在创建 bean 时就是根据 BeanDefinition 创建的。 doScan 方法扫描生成 BeanDefinition 之后还会将 BeanDefinition 注册到 bena 工厂,只有注册到 bean 工厂 bean 才能被创建出来。

如上图中画线代码所示, Spring 在将 BeanDefinition 注册到工厂之前,会先解析 BeanDefinition 获取 beanscopeScopedProxyMode ,即 ScopeMetadata 。最后根据代理模式 ScopedProxyMode 判断是否需要为该 BeanDefinition 生成代理类的 BeanDefinitionAnnotationConfigUtilsapplyScopedProxyMode 方法的源码如下图所示。

Spring Cloud动态配置实现原理与源码分析

如源码所示,当 BeanScopedProxyMode 不为 NO 时,该方法会为当前 bean 类生成一个代理类,并返回代理类的 BeanDefinition ,最后 doScan 方法中注册的 BeanDefinition 将是代理类的 BeanDefinition ,所以在其它 bean 中使用 @Resource@Autowired 注解所引用的动态配置 bean 其实是它的代理对象。

ScopedProxyMode 的源码如下。

public class ScopeMetadata {
	private String scopeName = BeanDefinition.SCOPE_SINGLETON;
	private ScopedProxyMode scopedProxyMode = ScopedProxyMode.NO;
}
复制代码

ScopeMetadata 类的源码可以看出,当 bean 没有被 @Scope 注解声明时,默认的 scopesingleton (单例),当 bean 没有被 @RefreshScope 注解声明时,默认使用的 ScopedProxyModeNO

@RefreshScope 注解声明的 bean ,其 scoperefresh ,默认使用的 ScopedProxyModeTARGET_CLASS 。所以 AnnotationConfigUtilsapplyScopedProxyMode 方法将调用 ScopedProxyCreatorcreateScopedProxy 方法为 bean 的类创建一个代理类,并为该代理类创建 BeanDefinition ,源码如下图所示。

Spring Cloud动态配置实现原理与源码分析

注意看图中画线的代码,该方法会创建一个新的 BeanDefinition ,该 BeanDefinitionbean 类型为 ScopedProxyFactoryBean ,并且为该 bean 注入属性 targetBeanNametargetBeanName 为目标 beanbeanName ,最后返回该 BeanDefinition

截图中少了部分代码,原来的 BeanDefinition 在该方法的后面会注册到 bean 工厂,但使用的是 getTargetBeanName 方法返回的 beanName ,就是将原来的 beanName 加上前缀 scopedTarget. 。也就是说原来的 BeanDefinition 被换了个名称注册到 bean 工厂了, beanNamescopedTarget.[原来的beanName]

ScopedProxyFactoryBean 是一个 FactoryBean<?> ,所以我们重点关注它的 getObject 方法返回的代理对象。 ScopedProxyFactoryBeangetObject 方法源码如下。

public class ScopedProxyFactoryBean extends ProxyConfig
		implements FactoryBean<Object>, 
		BeanFactoryAware, AopInfrastructureBean {
    @Override
	public Object getObject() {
		return this.proxy;
	}
}
复制代码

getObject 方法返回 this.proxy ,这个 proxy 是什么时候创建的?

前面我们查看 cglib 生成的代理类发现其实现了一个 Advised 接口,这个 Advised 接口有一个 getTargetSource 方法。

public interface Advised extends TargetClassAware {
    TargetSource getTargetSource();
    // 其它省略
}
复制代码

我们在 ScopedProxyFactoryBean 类中也发现一个 TargetSourceTargetSource 是一个接口,其中有一个 getTarget 方法我们要重点关注。

public interface TargetSource extends TargetClassAware {
    Object getTarget() throws Exception;
    // 其它省略
}
复制代码

ScopedProxyFactoryBean 类的 TargetSource 字段类型为 SimpleBeanTargetSource

public class ScopedProxyFactoryBean extends ProxyConfig
		implements FactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean {

	private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
	private String targetBeanName;
	
	public void setTargetBeanName(String targetBeanName) {
		this.targetBeanName = targetBeanName;
		this.scopedTargetSource.setTargetBeanName(targetBeanName);
	}
}
复制代码

SimpleBeanTargetSource 的源码如下:

public class SimpleBeanTargetSource extends AbstractBeanFactoryBasedTargetSource {
	@Override
	public Object getTarget() throws Exception {
		return getBeanFactory().getBean(getTargetBeanName());
	}
}
复制代码

SimpleBeanTargetSourcegetTarget 方法返回一个从 bean 工厂中根据目标 beanName 获取的 bean ,这跟我们的猜想很符合,我们继续关注这个 SimpleBeanTargetSource 是怎么被使用的。

ScopedProxyFactoryBean 实现 BeanFactoryAware 接口, xxxAware 接口的方法在 bean 被实例化且注入属性完成之后,在调用 bean 的初始化方法之前被调用,代理对象实际是在 setBeanFactory 方法中创建的。 setBeanFactory 方法源码如下图所示。

Spring Cloud动态配置实现原理与源码分析

通过 ProxyFactory 代理工厂创建的代理类都会实现 Advised 接口,使用 cglib 生成的代理类我们也已经看过了。

所以,当代理对象的 getXxx 方法被调用时,会被方法拦截器拦截,然后走切面逻辑。那么我们就可以通过在方法拦截器的 invoke 方法或者通知方法( AOP 的“通知”)中调用代理对象的 getTargetSource 方法获取 ScopedProxyFactoryBeansetBeanFactory 方法中为代理对象注入的 TargetSource 对象,然后调用 TargetSource 对象的 getTarget 方法从 bean 工厂中获取目标 bean ,再通过反射调用目标 beangetXxx 方法。通过这种方式是可以实现动态配置的,这离我们的猜测已经很接近了。

前面分析了这么多的代码还只是 Spring 的源码,要想证实假设,我们还需要分析 Spring Cloud 实现动态配置的源码。源码在 spring-cloud-context 模块的 autoconfigure 包下,如下图所示。

Spring Cloud动态配置实现原理与源码分析

RefreshAutoConfiguration 类就是自动配置 Spring Cloud 动态配置的配置类,这个配置类会往容器中注入两个与实现动态配置密切相关的 bean

// 非完整代码
public class RefreshAutoConfiguration {

    @Bean
	@ConditionalOnMissingBean(RefreshScope.class)
	public static RefreshScope refreshScope() {
		return new RefreshScope();
	}

    @Bean
	@ConditionalOnMissingBean
	public ContextRefresher contextRefresher(ConfigurableApplicationContext context,
			RefreshScope scope) {
		return new ContextRefresher(context, scope);
	}
}
复制代码

RefreshScopeContextRefresherSpring Cloud 实现动态配置的两个关键类。

Spring Cloud动态配置实现原理与源码分析
  • ContextRefresher :负责刷新环境 Environment
  • RefreshScope :负责销毁 @RefreshScope 声明的动态配置 bean ,即调用 bean 生命周期的销毁方法;

Spring Cloud 负责更新环境 Environment 以及创建新的动态配置 bean ,而判断配置是否改变,以及怎么获取新的配置则是由第三方框架实现的,如 nacos

假设我们自己实现接入注册中心,使用 mysql 作为注册中心,那么我们需要做的就是定时从 mysql 查询配置,然后对比配置有没有改变,如果改变了,那就调用 ContextRefresherrefresh 方法,其它的就可以交由 Spring Cloud 去完成。

ContextRefresherrefresh 方法实现更新环境 Environment ,并调用 RefreshScoperefreshAll 方法使旧的动态配置 bean 无效。 refresh 方法的源码如下:

public class ContextRefresher {
    public synchronized Set<String> refresh() {
        // 更新环境`Environment`
		Set<String> keys = refreshEnvironment();
		// 调用`RefreshScope`的`refreshAll`方法
		this.scope.refreshAll();
		return keys;
	}
}
复制代码

refreshEnvironment 方法的实现比较复杂,我们不展开分析。 refreshEnvironment 方法通过创建一个新的 ConfigurableApplicationContext 去获取新的 Environment ,然后将新的 EnvironmentPropertySource<?> 替换当前 Environment 的,这样就实现了环境刷新。但由于是通过创建一个新的 ConfigurableApplicationContext 方式加载新的配置,所以 refreshEnvironment 方法的执行会很耗时,不过这种方式也确实巧妙。

refreshEnvironment 更新完 Environment 后会发送一个 EnvironmentChangeEvent 事件,该事件会携带更新的配置项的 key

如果是监听 EnvironmentChangeEvent 事件感知配置改变,那么我们需要注意,在监听到 EnvironmentChangeEvent 事件时,调用动态配置 bean 的代理对象的 getXxx 方法获取到的字段的值还是旧的,因为 RefreshScoperefreshAll 方法还没有被调用。

你可能会有疑问,被 @RefreshScope 声明的 bean 不是单例的吗?是因为缓存, RefreshScope 会缓存动态配置 bean ,避免每调用一个 getXxx 方法都创建一个新的动态配置 bean

RefreshScope 类与前面分析的 ScopedProxyFactoryBean 类还有一层关系。 RefreshScope 继承 GenericScope ,而 GenericScope 实现了 BeanDefinitionRegistryPostProcessor 接口, postProcessBeanDefinitionRegistry 方法的源码如下图所示。

Spring Cloud动态配置实现原理与源码分析

postProcessBeanDefinitionRegistry 方法将所有的 scoperefreshbean 类型为 ScopedProxyFactoryBeanBeanDefinition 都找出来,并且将 bean 类型全部替换为 LockedScopedProxyFactoryBeanLockedScopedProxyFactoryBeanScopedProxyFactoryBean 的子类,重写了 setBeanFactory 方法,源码如下。

public static class LockedScopedProxyFactoryBean<S extends GenericScope>
			extends ScopedProxyFactoryBean implements MethodInterceptor {
	
	@Override
	public void setBeanFactory(BeanFactory beanFactory) {
		super.setBeanFactory(beanFactory);
		Object proxy = getObject();
		if (proxy instanceof Advised) {
			Advised advised = (Advised) proxy;
			advised.addAdvice(0, this);
		}
	}
	// .....
}
复制代码

setBeanFactory 方法调用父类的 setBeanFactory 方法完成代理对象的创建。

LockedScopedProxyFactoryBean 还实现了 MethodInterceptor 接口,所以 LockedScopedProxyFactoryBean 还是一个方法拦截器。 MethodInterceptorinvoke 方法会优先 Advised 被调用。 LockedScopedProxyFactoryBeaninvoke 方法的源码如下图所示。

Spring Cloud动态配置实现原理与源码分析

invoke 方法首先获取代理对象,然后通过反射调用目标方法,而在调用目标方法时,传入的目标对象是通过代理对象的 TargetSource 获取的,也就是从 bean 工厂中根据目标 beanName 获取的。

RefreshScoperefreshAll 源码如下:

public class RefreshScope extends GenericScope implements ApplicationContextAware,
		ApplicationListener<ContextRefreshedEvent>, Ordered {
    public void refreshAll() {
		super.destroy();
		this.context.publishEvent(new RefreshScopeRefreshedEvent());
	}
}
复制代码

refreshAll 调用 destroy 方法“销毁”旧的动态配置 bean ,然后发送一个 RefreshScopeRefreshedEvent 事件,如果监听 RefreshScopeRefreshedEvent 事件实现感知配置改变,那么在监听到 RefreshScopeRefreshedEvent 事件时,就可以调用动态配置 bean 的代理对象的 getXxx 方法获取最新的配置。

RefreshScoperefreshAll 方法并非真的销毁 bean ,也没有调用 bean 的生命周期的销毁方法,只是清空下缓存的 bean

RefreshScoperefreshAll 方法执行后,当动态配置 bean 的代理对象的 getXxx 方法下一次被调用时,先取得代理对象的 TargetSource 对象,再调用 TargetSource 对象的 getTarget 方法获取目标 bean ,最后反射调用目标 beangetXxx 方法。由于缓存已经不存在,调用 TargetSource 对象的 getTarget 方法就会从 bean 工厂中获取,就会创建新的动态配置 bean ,而在创建新的 bean 时,在实例化 bean 以及完成属性注入之后,在调用 bean 的初始化方法之前,会调用一些 BeanPostProcessorbean 加工,而为 @ConfigurationProperties 注解声明的 bean 的属性赋值的工作则由 ConfigurationPropertiesBindingPostProcessor 完成。

ConfigurationPropertiesBindingPostProcessorEnvironment 中获取配置通过反射赋值给 bean 的字段。

总结,回答两个问题

Spring Cloud 动态配置的实现原理我们已经从分析源码的过程中了解,如果看懂源码分析部分,那么文章前面提到的两个问题也就有了答案。

第一个问题:为什么使用 @RefreshScope 注解就能实现动态刷新配置?

使用 @RefreshScope 注解声明的 bean ,其 scoperefresh ,每次从 bean 工厂拿这类 bean 都会是一个新的 bean

第二个问题:为什么调用代理对象的 get 方法就能获取到新的配置,以及当配置改变时 Spring Cloud 的实现是将动态配置 Bean 销毁再创建新的 Bean 这句怎么理解?

这与 bean 的生命周期有关, bean 中的字段只会在 bean 创建阶段赋值一次,后续不会改变,如果引用的是代理对象,那么当调用代理对象的方法时,方法拦截器先从代理对象拿到 TargetSource ,然后调用 TargetSource 对象的 getTarget 方法从 bean 工厂获取目标 bean ,最后再通过反射调用目标 bean 的方法,以此实现 bean 的动态更新。

Spring Cloud 的实现并非真的将动态配置 Bean 销毁,而是清除为提升性能所缓存的动态配置 Bean 。当配置改变时,清除缓存后,下次就会从 Bean 工厂获取新的 BeanSpring 在创建 Bean 时,由 ConfigurationPropertiesBindingPostProcessor 这个 BeanPostProcessorEnvironment 中获取配置通过反射赋值给 bean 的字段。

原文  https://juejin.im/post/5efafbf75188252e5961a8ce
正文到此结束
Loading...