实际项目开发中少不了各种配置,如连接数据库的配置、连接 Redis
集群的配置等,通常我们也会为一个项目部署到每个环境准备不同的配置文件,例如测试环境配置连接测试的数据库。基本上静态配置就已经满足日常需求,但是静态配置缺少灵活性,一经修改就需要重新构建部署应用,同时也缺少安全性,容易泄漏线上环境的配置,所以我们需要一种更灵活更安全的配置方式:动态配置。
动态配置的使用场景并不是为了替换静态配置而出现的,数据库连接配置这些一般都不会改动,所以数据库连接这类配置使用静态配置还是动态配置都没有多大影响。对于那些变动频率高的配置,才会迫切去使用动态配置。例如支付页面展示的支付方式,当第三方支付公司升级服务时,就可以暂时隐藏掉该支付方式;例如集群环境下控制哪些节点做哪些事情;例如控制接口降级、路由修改等等。
实现动态配置的方式很简单,我们可以将配置写到一个专门用来做动态配置的数据库,又或者使用其它的持久化存储方式,然后在代码中定时查看配置有没有更新,有更新就替换旧的配置,然后做一些配置更新后的操作。也可以将实现动态配置的逻辑封装为一个 jar
包,实现代码复用。
因为动态配置有它存在的意义,所以 Spring Cloud
也为我们封装了大部分的实现动态配置的逻辑,让我们使用动态配置更方便。而具体的配置信息存储在哪、怎么获取,这些则交给配置中心去实现,如 Nacos
、 Diamond
、 Disconf
。
本篇从源码分析 Spring Cloud
实现动态配置的原理。 Spring Cloud
实现动态配置需要结合 Spring
源码分析。
目录:
Spring Cloud @RefreshScope Spring Cloud
在 Spring Cloud
项目中,无论你使用何种配置中心,使用动态配置功能的方式都可以是一种,我们来看一个使用动态配置的例子。
@Component @ConfigurationProperties(prefix = "sck-demo") @RefreshScope(proxyMode = ScopedProxyMode.TARGET_CLASS) public class DemoProps { 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
实现动态装载配置呢?这是第一个等待我们从源码中寻找答案的问题。
给 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
这句怎么理解?这是第二个等待我们从源码中寻找答案的问题。
我们将带着这两个问题从源码中寻找答案。
根据前面的分析,我们不妨假设:当使用 @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
这类注解注释的类扫描出来并生成 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
的源码如下。
public class ScopeMetadata { 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
方法源码如下。
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
类中也发现一个 TargetSource
, TargetSource
是一个接口,其中有一个 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()); } } 复制代码
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
。
// 非完整代码 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); } } 复制代码
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
方法的源码如下:
public class ContextRefresher { public synchronized 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
方法,源码如下。
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
还是一个方法拦截器。 MethodInterceptor
的 invoke
方法会优先 Advised
被调用。 LockedScopedProxyFactoryBean
的 invoke
方法的源码如下图所示。
invoke
方法首先获取代理对象,然后通过反射调用目标方法,而在调用目标方法时,传入的目标对象是通过代理对象的 TargetSource
获取的,也就是从 bean
工厂中根据目标 beanName
获取的。
RefreshScope
的 refreshAll
源码如下:
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
方法获取最新的配置。
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
的字段。