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

    技术2022-07-12  71

    关注 “Java艺术” 我们一起成长!

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

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

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

    因为动态配置有它存在的意义,所以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) 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实现动态装载配置呢?这是第一个等待我们从源码中寻找答案的问题。

    使用@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生成的动态代理类输出到文件。

    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注册到Bean工厂。

    AnnotationConfigUtils的applyScopedProxyMode方法的源码如下图所示。

    如源码所示,当Bean的ScopedProxyMode不为NO时,该方法会为当前bean类生成一个代理类,并返回代理类的BeanDefinition,这将会改变doScan方法中注册的BeanDefinition,最终注册到Bean工厂的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,也就是被代理的bean的beanName。该方法最后返回的是代理类的BeanDefinition。

    截图中少了部分代码,原来的BeanDefinition在该方法的后面会注册到bean工厂,但使用的是getTargetBeanName方法返回的beanName,就是将原来的beanName加上前缀scopedTarget.,而把原来的beanName留给代理对象使用。

    也就是说,原来的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; } }

    这个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接口,用于获取Bean工厂。xxxAware接口的方法在bean被实例化且注入属性完成之后,在调用bean的初始化方法之前被调用,代理对象实际是在setBeanFactory方法中创建的。setBeanFactory方法源码如下图所示。

    从Cglib生成的代理类可以看出,通过ProxyFactory代理工厂创建的代理类都会实现Advised接口。所以,当代理对象的getXxx方法被调用时,会被方法拦截器拦截,然后走切面逻辑。

    通过在方法拦截器的invoke方法或者通知方法(AOP的“通知”)中调用代理对象的getTargetSource方法可以获取到ScopedProxyFactoryBean在setBeanFactory方法中为该代理对象注入的TargetSource对象,然后调用TargetSource对象的getTarget方法就能从bean工厂中获取目标bean。

    拿到目标bean后再通过反射调用目标bean的getXxx方法。只要每次从bean工厂中获取的目标bean是新的,就可以通过这种方式是实现动态配置。这离我们的猜测已经很接近了。

    前面分析了这么多的代码还只是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方法会将所有的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方法执行,就只会从bean工厂获取一次,此后都从RefreshScope的缓存中取。

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

    往期原创精选

    Spring Cloud Kubernetes服务注册与发现的实现原理与源码分析

    Ribbon重试策略RetryHandler的配置与源码分析

    OpenFeign与Ribbon源码分析总结(面试题)

    Spring Cloud Ribbon源码分析(Spring Cloud Kubernetes)

    Spring Cloud OpenFeign源码分析

    Spring Cloud kubernetes入门项目sck-demo

    为什么要选择Spring Cloud Kubernetes?

    [Java艺术] 微信号:javaskill

    一个只推送原创文章的技术公众号,分享Java后端相关技术。

    Processed: 0.033, SQL: 9