Spring Cloud动态配置实现原理与源码分析
实际项目开发中少不了各种配置,如连接数据库的配置、连接Redis
集群的配置等,通常我们也会为一个项目部署到每个环境准备不同的配置文件,例如测试环境配置连接测试的数据库。基本上静态配置就已经满足日常需求,但是静态配置缺少灵活性,一经修改就需要重新构建部署应用,同时也缺少安全性,容易泄漏线上环境的配置,所以我们需要一种更灵活更安全的配置方式:动态配置。
动态配置的使用场景并不是为了替换静态配置而出现的,数据库连接配置这些一般都不会改动,所以数据库连接这类配置使用静态配置还是动态配置都没有多大影响。对于那些变动频率高的配置,才会迫切去使用动态配置。例如支付页面展示的支付方式,当第三方支付公司升级服务时,就可以暂时隐藏掉该支付方式;例如集群环境下控制哪些节点做哪些事情;例如控制接口降级、路由修改等等。
实现动态配置的方式很简单,我们可以将配置写到一个专门用来做动态配置的数据库,又或者使用其它的持久化存储方式,然后在代码中定时查看配置有没有更新,有更新就替换旧的配置,然后做一些配置更新后的操作。也可以将实现动态配置的逻辑封装为一个jar
包,实现代码复用。
因为动态配置有它存在的意义,所以Spring Cloud" title="Spring Cloud">Spring Cloud
也为我们封装了大部分的实现动态配置的逻辑,让我们使用动态配置更方便。而具体的配置信息存储在哪、怎么获取,这些则交给配置中心去实现,如Nacos
、Diamond
、Disconf
。
本篇从源码分析Spring Cloud
实现动态配置的原理。Spring Cloud
实现动态配置需要结合Spring
源码分析。
目录:
Spring Cloud
动态配置的使用方式- 使用
@RefreshScope
可能会遇到的问题 - 从源码分析
Spring Cloud
动态配置的实现原理 - 总结
Spring Cloud动态配置的使用方式
在Spring Cloud
项目中,无论你使用何种配置中心,使用动态配置功能的方式都可以是一种,我们来看一个使用动态配置的例子。
@Component@ConfigurationProperties(prefix = "sck-demo")
@RefreshScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
publicclassDemoProps{
private String message;
}
DemoProps
类省略了get
、set
方法。DemoProps
类使用@Component
注解和@ConfigurationProperties
注解声明为用于装载配置的bean
。@RefreshScope
注解则用于声明该bean
的scope
以及代理模式ScopedProxyMode
。
为了便于理解,我们将这类用于装载配置的类称为Properties
类,这类用于装载配置的bean
称为动态配置bean
。
我们常见的scope
有singleton
(单例)、prototype
(原型),当然还有其它的,而今天我们要学习一个新的scope
:refresh
。@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
注解的目的是声明动态配置Bean
的scope
为refresh
,以及声明Bean
的代理模式(ScopedProxyMode
)。
代理模式ScopedProxyMode
的可取值为:
NO
:不创建代理类;DEFAULT
:其作用通常等于NO
;INTERFACES
:创建一个JDK
动态代理类来实现目标对象的类的所有接口;TARGET_CLASS
:使用Cglib
为目标对象的类创建一个代理类,这是@RefreshScope
使用的默认值;
其中INTERFACES
代理模式不适用于动态配置Bean
,因为Properties
类没有实现任何接口,如果强行给@RefreshScope
注解配置代理模式使用INTERFACES
,Spring
将会抛出异常。
当我们配置@RefreshScope
的proxyMode
属性使用默认的TARGET_CLASS
代理模式时,我们可能会遇到获取该Bean
的属性为Null
的情况,这是因为我们在其它Bean
中使用@Resource
或@Autowired
注解方式引用的对象是动态代理对象,即使用Cglib
生成的动态代理类的实例。所以我们只能通过get
方法去获取对象的字段的值,这是我们在使用动态配置时需要注意的。
当我们配置@RefreshScope
的proxyMode
属性使用NO
或者DEFAULT
代理模式时,如果使用@Resource
或@Autowired
注解方式方式引用对象,那么动态配置就会失效,也就是动态修改配置后拿到的还是旧的配置。这是因为@RefreshScope
注解会将Bean
的scope
声明为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
生成的动态代理输出到文件。
publicclassApp{static {
System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "/tmp");
}
}
以前面例子的DemoProps
类为例,cglib
生成的动态代理类如下:
publicclassDemoProps$$EnhancerBySpringCGLIB$$593bbd8bextendsDemoPropsimplementsScopedObject, Serializable,
AopInfrastructureBean, SpringProxy,
Advised, Factory
{// .......
}
因为没什么特别的,所以代码就省略了。我们只需要记住,Spring
为使用@RefreshScope
声明且代理模式为TARGET_CLASS
的类生成的动态代理类实现了Advised
接口(AOP
的“通知”或者说是“增强”)。
从cglib
生成的动态代理类找不到突破口,那么我们只能从Spring
扫描bean
开始了,看下哪些地方使用到@RefreshScope
注解。Spring
扫描bean
的源码在ClassPathBeanDefinitionScanner
类的doScan
方法,源码如下图所示。
Spring
扫描bean
就是将被@Component
这类注解注释的类扫描出来并生成BeanDefinition
,Spring
在创建bean
时就是根据BeanDefinition
创建的。doScan
方法扫描生成BeanDefinition
之后还会将BeanDefinition
注册到bena
工厂,只有注册到bean
工厂bean
才能被创建出来。
如上图中画线代码所示,Spring
在将BeanDefinition
注册到工厂之前,会先解析BeanDefinition
获取bean
的scope
和ScopedProxyMode
,即ScopeMetadata
。最后根据代理模式ScopedProxyMode
判断是否需要为该BeanDefinition
生成代理类的BeanDefinition
。AnnotationConfigUtils
的applyScopedProxyMode
方法的源码如下图所示。
如源码所示,当Bean
的ScopedProxyMode
不为NO
时,该方法会为当前bean
类生成一个代理类,并返回代理类的BeanDefinition
,最后doScan
方法中注册的BeanDefinition
将是代理类的BeanDefinition
,所以在其它bean
中使用@Resource
或@Autowired
注解所引用的动态配置bean
其实是它的代理对象。
ScopedProxyMode
的源码如下。
publicclassScopeMetadata{private String scopeName = BeanDefinition.SCOPE_SINGLETON;
private ScopedProxyMode scopedProxyMode = ScopedProxyMode.NO;
}
从ScopeMetadata
类的源码可以看出,当bean
没有被@Scope
注解声明时,默认的scope
为singleton
(单例),当bean
没有被@RefreshScope
注解声明时,默认使用的ScopedProxyMode
为NO
。
被@RefreshScope
注解声明的bean
,其scope
为refresh
,默认使用的ScopedProxyMode
为TARGET_CLASS
。所以AnnotationConfigUtils
的applyScopedProxyMode
方法将调用ScopedProxyCreator
的createScopedProxy
方法为bean
的类创建一个代理类,并为该代理类创建BeanDefinition
,源码如下图所示。
注意看图中画线的代码,该方法会创建一个新的BeanDefinition
,该BeanDefinition
的bean
类型为ScopedProxyFactoryBean
,并且为该bean
注入属性targetBeanName
,targetBeanName
为目标bean
的beanName
,最后返回该BeanDefinition
。
截图中少了部分代码,原来的BeanDefinition
在该方法的后面会注册到bean
工厂,但使用的是getTargetBeanName
方法返回的beanName
,就是将原来的beanName
加上前缀scopedTarget.
。也就是说原来的BeanDefinition
被换了个名称注册到bean
工厂了,beanName
为scopedTarget.[原来的beanName]
。
ScopedProxyFactoryBean
是一个FactoryBean<?>
,所以我们重点关注它的getObject
方法返回的代理对象。ScopedProxyFactoryBean
的getObject
方法源码如下。
publicclassScopedProxyFactoryBeanextendsProxyConfigimplementsFactoryBean<Object>,
BeanFactoryAware, AopInfrastructureBean
{@Override
public Object getObject(){
returnthis.proxy;
}
}
getObject
方法返回this.proxy
,这个proxy
是什么时候创建的?
前面我们查看cglib
生成的代理类发现其实现了一个Advised
接口,这个Advised
接口有一个getTargetSource
方法。
publicinterfaceAdvisedextendsTargetClassAware{TargetSource getTargetSource();
// 其它省略
}
我们在ScopedProxyFactoryBean
类中也发现一个TargetSource
,TargetSource
是一个接口,其中有一个getTarget
方法我们要重点关注。
publicinterfaceTargetSourceextendsTargetClassAware{Object getTarget()throws Exception;
// 其它省略
}
ScopedProxyFactoryBean
类的TargetSource
字段类型为SimpleBeanTargetSource
。
publicclassScopedProxyFactoryBeanextendsProxyConfigimplementsFactoryBean<Object>, BeanFactoryAware, AopInfrastructureBean
{privatefinal SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource();
private String targetBeanName;
publicvoidsetTargetBeanName(String targetBeanName){
this.targetBeanName = targetBeanName;
this.scopedTargetSource.setTargetBeanName(targetBeanName);
}
}
SimpleBeanTargetSource
的源码如下:
publicclassSimpleBeanTargetSourceextendsAbstractBeanFactoryBasedTargetSource{@Override
public Object getTarget()throws Exception {
return getBeanFactory().getBean(getTargetBeanName());
}
}
SimpleBeanTargetSource
的getTarget
方法返回一个从bean
工厂中根据目标beanName
获取的bean
,这跟我们的猜想很符合,我们继续关注这个SimpleBeanTargetSource
是怎么被使用的。
ScopedProxyFactoryBean
实现BeanFactoryAware
接口,xxxAware
接口的方法在bean
被实例化且注入属性完成之后,在调用bean
的初始化方法之前被调用,代理对象实际是在setBeanFactory
方法中创建的。setBeanFactory
方法源码如下图所示。
通过ProxyFactory
代理工厂创建的代理类都会实现Advised
接口,使用cglib
生成的代理类我们也已经看过了。
所以,当代理对象的getXxx
方法被调用时,会被方法拦截器拦截,然后走切面逻辑。那么我们就可以通过在方法拦截器的invoke
方法或者通知方法(AOP
的“通知”)中调用代理对象的getTargetSource
方法获取ScopedProxyFactoryBean
的setBeanFactory
方法中为代理对象注入的TargetSource
对象,然后调用TargetSource
对象的getTarget
方法从bean
工厂中获取目标bean
,再通过反射调用目标bean
的getXxx
方法。通过这种方式是可以实现动态配置的,这离我们的猜测已经很接近了。
前面分析了这么多的代码还只是Spring
的源码,要想证实假设,我们还需要分析Spring Cloud
实现动态配置的源码。源码在spring-cloud-context
模块的autoconfigure
包下,如下图所示。
RefreshAutoConfiguration
类就是自动配置Spring Cloud
动态配置的配置类,这个配置类会往容器中注入两个与实现动态配置密切相关的bean
。
// 非完整代码publicclassRefreshAutoConfiguration{
@Bean
@ConditionalOnMissingBean(RefreshScope.class)
publicstatic RefreshScope refreshScope(){
returnnew RefreshScope();
}
@Bean
@ConditionalOnMissingBean
public ContextRefresher contextRefresher(ConfigurableApplicationContext context,
RefreshScope scope){
returnnew ContextRefresher(context, scope);
}
}
RefreshScope
与ContextRefresher
是Spring Cloud
实现动态配置的两个关键类。
ContextRefresher
:负责刷新环境Environment
;RefreshScope
:负责销毁@RefreshScope
声明的动态配置bean
,即调用bean
生命周期的销毁方法;
Spring Cloud
负责更新环境Environment
以及创建新的动态配置bean
,而判断配置是否改变,以及怎么获取新的配置则是由第三方框架实现的,如nacos
。
假设我们自己实现接入注册中心,使用mysql
作为注册中心,那么我们需要做的就是定时从mysql
查询配置,然后对比配置有没有改变,如果改变了,那就调用ContextRefresher
的refresh
方法,其它的就可以交由Spring Cloud
去完成。
ContextRefresher
的refresh
方法实现更新环境Environment
,并调用RefreshScope
的refreshAll
方法使旧的动态配置bean
无效。refresh
方法的源码如下:
publicclassContextRefresher{publicsynchronized Set<String> refresh(){
// 更新环境`Environment`
Set<String> keys = refreshEnvironment();
// 调用`RefreshScope`的`refreshAll`方法
this.scope.refreshAll();
return keys;
}
}
refreshEnvironment
方法的实现比较复杂,我们不展开分析。refreshEnvironment
方法通过创建一个新的ConfigurableApplicationContext
去获取新的Environment
,然后将新的Environment
的PropertySource<?>
替换当前Environment
的,这样就实现了环境刷新。但由于是通过创建一个新的ConfigurableApplicationContext
方式加载新的配置,所以refreshEnvironment
方法的执行会很耗时,不过这种方式也确实巧妙。
refreshEnvironment
更新完Environment
后会发送一个EnvironmentChangeEvent
事件,该事件会携带更新的配置项的key
。
如果是监听EnvironmentChangeEvent
事件感知配置改变,那么我们需要注意,在监听到EnvironmentChangeEvent
事件时,调用动态配置bean
的代理对象的getXxx
方法获取到的字段的值还是旧的,因为RefreshScope
的refreshAll
方法还没有被调用。
你可能会有疑问,被@RefreshScope
声明的bean
不是单例的吗?是因为缓存,RefreshScope
会缓存动态配置bean
,避免每调用一个getXxx
方法都创建一个新的动态配置bean
。
RefreshScope
类与前面分析的ScopedProxyFactoryBean
类还有一层关系。RefreshScope
继承GenericScope
,而GenericScope
实现了BeanDefinitionRegistryPostProcessor
接口,postProcessBeanDefinitionRegistry
方法的源码如下图所示。
postProcessBeanDefinitionRegistry
方法将所有的scope
为refresh
且bean
类型为ScopedProxyFactoryBean
的BeanDefinition
都找出来,并且将bean
类型全部替换为LockedScopedProxyFactoryBean
。LockedScopedProxyFactoryBean
是ScopedProxyFactoryBean
的子类,重写了setBeanFactory
方法,源码如下。
publicstaticclassLockedScopedProxyFactoryBean<SextendsGenericScope>extendsScopedProxyFactoryBeanimplementsMethodInterceptor
{@Override
publicvoidsetBeanFactory(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
还是一个方法拦截器。MethodInterceptor
的invoke
方法会优先Advised
被调用。LockedScopedProxyFactoryBean
的invoke
方法的源码如下图所示。
invoke
方法首先获取代理对象,然后通过反射调用目标方法,而在调用目标方法时,传入的目标对象是通过代理对象的TargetSource
获取的,也就是从bean
工厂中根据目标beanName
获取的。
RefreshScope
的refreshAll
源码如下:
publicclassRefreshScopeextendsGenericScopeimplementsApplicationContextAware,ApplicationListener<ContextRefreshedEvent>, Ordered
{publicvoidrefreshAll(){
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
}
refreshAll
调用destroy
方法“销毁”旧的动态配置bean
,然后发送一个RefreshScopeRefreshedEvent
事件,如果监听RefreshScopeRefreshedEvent
事件实现感知配置改变,那么在监听到RefreshScopeRefreshedEvent
事件时,就可以调用动态配置bean
的代理对象的getXxx
方法获取最新的配置。
RefreshScope
的refreshAll
方法并非真的销毁bean
,也没有调用bean
的生命周期的销毁方法,只是清空下缓存的bean
。
RefreshScope
的refreshAll
方法执行后,当动态配置bean
的代理对象的getXxx
方法下一次被调用时,先取得代理对象的TargetSource
对象,再调用TargetSource
对象的getTarget
方法获取目标bean
,最后反射调用目标bean
的getXxx
方法。由于缓存已经不存在,调用TargetSource
对象的getTarget
方法就会从bean
工厂中获取,就会创建新的动态配置bean
,而在创建新的bean
时,在实例化bean
以及完成属性注入之后,在调用bean
的初始化方法之前,会调用一些BeanPostProcessor
为bean
加工,而为@ConfigurationProperties
注解声明的bean
的属性赋值的工作则由ConfigurationPropertiesBindingPostProcessor
完成。
ConfigurationPropertiesBindingPostProcessor
从Environment
中获取配置通过反射赋值给bean
的字段。
总结,回答两个问题
Spring Cloud
动态配置的实现原理我们已经从分析源码的过程中了解,如果看懂源码分析部分,那么文章前面提到的两个问题也就有了答案。
第一个问题:为什么使用@RefreshScope
注解就能实现动态刷新配置?
使用@RefreshScope
注解声明的bean
,其scope
为refresh
,每次从bean
工厂拿这类bean
都会是一个新的bean
。
第二个问题:为什么调用代理对象的get
方法就能获取到新的配置,以及当配置改变时Spring Cloud
的实现是将动态配置Bean
销毁再创建新的Bean
这句怎么理解?
这与bean
的生命周期有关,bean
中的字段只会在bean
创建阶段赋值一次,后续不会改变,如果引用的是代理对象,那么当调用代理对象的方法时,方法拦截器先从代理对象拿到TargetSource
,然后调用TargetSource
对象的getTarget
方法从bean
工厂获取目标bean
,最后再通过反射调用目标bean
的方法,以此实现bean
的动态更新。
Spring Cloud
的实现并非真的将动态配置Bean
销毁,而是清除为提升性能所缓存的动态配置Bean
。当配置改变时,清除缓存后,下次就会从Bean
工厂获取新的Bean
。Spring
在创建Bean
时,由ConfigurationPropertiesBindingPostProcessor
这个BeanPostProcessor
从Environment
中获取配置通过反射赋值给bean
的字段。
以上是 Spring Cloud动态配置实现原理与源码分析 的全部内容, 来源链接: utcz.com/a/29067.html