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

2,606 阅读15分钟

实际项目开发中少不了各种配置,如连接数据库的配置、连接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扫描bean就是将被@Component这类注解注释的类扫描出来并生成BeanDefinitionSpring在创建bean时就是根据BeanDefinition创建的。doScan方法扫描生成BeanDefinition之后还会将BeanDefinition注册到bena工厂,只有注册到bean工厂bean才能被创建出来。

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

如源码所示,当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,源码如下图所示。

注意看图中画线的代码,该方法会创建一个新的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方法源码如下图所示。

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

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

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

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实现动态配置的两个关键类。

  • 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方法的源码如下图所示。

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方法的源码如下图所示。

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的字段。