springCloud如何进行配置刷新
在现在许多分布式应用中都使用到了配置中心来进行配置统一管理、下发以及热更新操作,下面我们来分析一下基于SpringCloud微服务体系下如何实现配置的刷新功能。
首先对于Spring中的Bean大家都不陌生,而对于Bean来说有一个关键的属性就是它的生命周期,在Spring中叫做scope。对于早期版本中的Spring中的bean的生命周期
只有singleton和prototype两种,随着支持web应用后又增加了request、session、global session等,随着springCloud之后又增加了一个RefreshScope的类并且增加了
@RfreshScope的注解,注解内部如下:
@Target({ ElementType.TYPE, ElementType.METHOD }) @Retention(RetentionPolicy.RUNTIME) @Scope("refresh") @Documented public @interface RefreshScope { /** * @see Scope#proxyMode() * @return proxy mode */ ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS; }
内部标记了@Scope注解并且ScopedProxyMode值为ScopedProxyMode.TARGET_CLASS。
首先说一下ScopedProxyMode,这个属于@Scope注解中的属性,有四个枚举值可供选择:
- ScopedProxyMode.DEFAULT 默认代理模式等同于No 不使用代理
- ScopedProxyMode.No 不使用代理
- ScopedProxyMode.INTERFACE 使用jdk代理
- ScopedProxyMode.TARGET_CLASS 使用cglib代理
而@RefreshScope注解内部设置了ScopedProxyMode为ScopedProxyMode.TARGET_CLASS表示使用cgilib代理此对象。
我们可以认为SpringCloud提供了一个新的bean的生命周期种类。这个注解是SpringCloud配置动态刷新的基石。
我们看Spring是如何处理@RefreshScope的bean的。
1.bean加载阶段的处理
首先我们看对于添加了@RefreshScope注解的bean如何加载。写个demo如下
@RefreshScope @Component @ConfigurationProperties(prefix = "jackding") public class NameProperties implements InitializingBean { private List<String> names; public List<String> getNames() { return names; } public void setNames(List<String> names) { this.names = names; } }
我们定义一个简单自动装配的Bean添加上@RefeshScope注解标记为一个支持动态刷新的bean。
我这里使用@Coponment注解并且当前这个bean在我启动类的扫描路径内,这种bean通过Spring
内部对@ComponentScan的处理就可以加载,我们看主要看ComponentScanAnnotationParser
加载这个Bean的逻辑。ComponentScanAnnotationParser调用ClassPathBeanDefinitionScanner
的doScan方法,我们看这个方法:
protected Set<BeanDefinitionHolder> doScan(String... basePackages) { Assert.notEmpty(basePackages, "At least one base package must be specified"); Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>(); for (String basePackage : basePackages) { Set<BeanDefinition> candidates = findCandidateComponents(basePackage); for (BeanDefinition candidate : candidates) { ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate); candidate.setScope(scopeMetadata.getScopeName()); String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry); if (candidate instanceof AbstractBeanDefinition) { postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName); } if (candidate instanceof AnnotatedBeanDefinition) { AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate); } if (checkCandidate(beanName, candidate)) { BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName); definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry); beanDefinitions.add(definitionHolder); registerBeanDefinition(definitionHolder, this.registry); } } } return beanDefinitions; }
这个方法很好理解根据跟定的包路径扫描所有的@Component注解标记的bean并且注册至容器中并返回。此时重点代码在加粗的地方
--处理scopedProxyMode。
static BeanDefinitionHolder applyScopedProxyMode( ScopeMetadata metadata, BeanDefinitionHolder definition, BeanDefinitionRegistry registry) { ScopedProxyMode scopedProxyMode = metadata.getScopedProxyMode(); if (scopedProxyMode.equals(ScopedProxyMode.NO)) { return definition; } boolean proxyTargetClass = scopedProxyMode.equals(ScopedProxyMode.TARGET_CLASS); return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); }
我们接着看createScopeProxy方法。
public static BeanDefinitionHolder createScopedProxy(BeanDefinitionHolder definition, BeanDefinitionRegistry registry, boolean proxyTargetClass) { String originalBeanName = definition.getBeanName(); BeanDefinition targetDefinition = definition.getBeanDefinition(); String targetBeanName = getTargetBeanName(originalBeanName); // Create a scoped proxy definition for the original bean name, // "hiding" the target bean in an internal target definition. RootBeanDefinition proxyDefinition = new RootBeanDefinition(ScopedProxyFactoryBean.class); proxyDefinition.setDecoratedDefinition(new BeanDefinitionHolder(targetDefinition, targetBeanName)); proxyDefinition.setOriginatingBeanDefinition(targetDefinition); proxyDefinition.setSource(definition.getSource()); proxyDefinition.setRole(targetDefinition.getRole()); proxyDefinition.getPropertyValues().add("targetBeanName", targetBeanName); if (proxyTargetClass) { targetDefinition.setAttribute(AutoProxyUtils.PRESERVE_TARGET_CLASS_ATTRIBUTE, Boolean.TRUE); // ScopedProxyFactoryBean's "proxyTargetClass" default is TRUE, so we don't need to set it explicitly here. } else { proxyDefinition.getPropertyValues().add("proxyTargetClass", Boolean.FALSE); } // Copy autowire settings from original bean definition. proxyDefinition.setAutowireCandidate(targetDefinition.isAutowireCandidate()); proxyDefinition.setPrimary(targetDefinition.isPrimary()); if (targetDefinition instanceof AbstractBeanDefinition) { proxyDefinition.copyQualifiersFrom((AbstractBeanDefinition) targetDefinition); } // The target bean should be ignored in favor of the scoped proxy. targetDefinition.setAutowireCandidate(false); targetDefinition.setPrimary(false); // Register the target bean as separate bean in the factory. registry.registerBeanDefinition(targetBeanName, targetDefinition); // Return the scoped proxy definition as primary bean definition // (potentially an inner bean). return new BeanDefinitionHolder(proxyDefinition, originalBeanName, definition.getAliases()); }
这段代码意思就是使用原本的BeanName重新构建一个beanDefination,将目标bean隐藏在其内部。并且将目标bean的beanName
改为scopedTarget.targetBeanName的形式并且注册到容器中。此时容器中已经有了我们原始的bean但是名字已经被改掉。
对于当前重新构造的Bean我们暂且称它为代理bean,这个代理bean使用FactoryBean的方式进行构建,这也是我们经常包装代理Bean
的一种常用手段。
将代理bean定义生成后并且将目标bean定义注册至容器后方法返回到ComponentScanAnnotationParser.doScan,我们看到之后会将
返回的bean定义注册至容器,此时对于我们这个bean来说容器内存在两个:
一个是真正的目标bean定义,一个是重新生成的bean定义。
新生成的Bean定义使用我们原始的beanName。并且对于这个代理bean定义会将Scope设置为sigleton。这样我们使用@RefreshScope
注解的bean就像正常的bean使用,我们无须关心背后逻辑,都被Spring屏蔽了。
此时bean定义加载完之后我们看如何生成代理bean。
2.实例化
Spring在加载完bean定义后会实例化所有非懒加载的单例Bean。
我们这个代理bean使用FactoryBean创建一个代理对象。
我们看ScopedProxyFactoryBean的主要两个方法:
@Override public void setBeanFactory(BeanFactory beanFactory) { if (!(beanFactory instanceof ConfigurableBeanFactory)) { throw new IllegalStateException("Not running in a ConfigurableBeanFactory: " + beanFactory); } ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory; this.scopedTargetSource.setBeanFactory(beanFactory); ProxyFactory pf = new ProxyFactory(); pf.copyFrom(this); pf.setTargetSource(this.scopedTargetSource); Assert.notNull(this.targetBeanName, "Property 'targetBeanName' is required"); Class<?> beanType = beanFactory.getType(this.targetBeanName); if (beanType == null) { throw new IllegalStateException("Cannot create scoped proxy for bean '" + this.targetBeanName + "': Target type could not be determined at the time of proxy creation."); } if (!isProxyTargetClass() || beanType.isInterface() || Modifier.isPrivate(beanType.getModifiers())) { pf.setInterfaces(ClassUtils.getAllInterfacesForClass(beanType, cbf.getBeanClassLoader())); } // Add an introduction that implements only the methods on ScopedObject. ScopedObject scopedObject = new DefaultScopedObject(cbf, this.scopedTargetSource.getTargetBeanName()); pf.addAdvice(new DelegatingIntroductionInterceptor(scopedObject)); // Add the AopInfrastructureBean marker to indicate that the scoped proxy // itself is not subject to auto-proxying! Only its target bean is. pf.addInterface(AopInfrastructureBean.class); this.proxy = pf.getProxy(cbf.getBeanClassLoader()); } @Override public Object getObject() { if (this.proxy == null) { throw new FactoryBeanNotInitializedException(); } return this.proxy; }
这个对象实现了BeanFactoryAware接口会在对象初始化调用initMethod的时候调用setBeanFactory方法,这个方法我们可以看到
使用AOP生成了代理对象通过getObject方法我们就获取到了已经生成的代理对象。此时真正的bean对象并不会生成,因为它不属
于单例,spring并不会提前实例化,在我们程序中注入的就是这个代理对象,是一个单例bean。当我们调用方法时才会真正的生成。
我们来看下生成真正Bean的逻辑。
对于Aop这里就不细说了,注意上边代码设置了一个targetSource参数,对于TargetSource接口是SpringAop中重要的一个接口,aop
拦截时通过调用TargetSource.getTarget方法获取真正的目标对象。这里ScopedProxyFactoryBean生成我们refreshScope代理bean的
类使用的是SimpleBeanTargetSource,其内部实现:
public Object getTarget() throws Exception { return getBeanFactory().getBean(getTargetBeanName()); }
就是调用容器获取我们真正的bean。对于Spring源码熟悉的都知道除了单例和原型bean其余的scope类型的bean都会真正调用Scope本身
去生成bean。前边提到了SpringCLoud加入了RefreshScope这个类,就是通过它生成我们的对象。
调用父类genericScope的get方法:
public Object get(String name, ObjectFactory<?> objectFactory) { BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory)); this.locks.putIfAbsent(name, new ReentrantReadWriteLock()); try { return value.getBean(); } catch (RuntimeException e) { this.errors.put(name, e); throw e; } }
我们看代码第一句是重点,这里使用了一个cache包装的hashMap做缓存放入一个BeanLifecycleWrapper包装对象并返回,
之后调用BeanLifecycleWrapper的getBean方法。
public Object getBean() { if (this.bean == null) { synchronized (this.name) { if (this.bean == null) { this.bean = this.objectFactory.getObject(); } } } return this.bean; }
我们看这个方法,如果bean其实就是我们真正的对象为Null则会调用objectFactory生成一个反之则返回当前的对象。
这个ObjectFcatory是一个生成object对象的接口在前边通过调用Scope生成对象时以匿名类传入进来,实际上就是
AbstractBeanFactory中生成调用Scope生成对象这一点:
Object scopedInstance = scope.get(beanName, () -> { beforePrototypeCreation(beanName); try { return createBean(beanName, mbd, args); } finally { afterPrototypeCreation(beanName); } }); bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd);
此时就会真正的生成我们的目标对象并且缓存在refreshScope中。
总结一下上边的内容:
对于我们定义了@RefreshScope注解的bean,spring在bean定义加载时期会额外创建一个单例类型的bean定义进行替代
并且返回一个代理对象,真正的bean定义也被注册至容器但是不会跟随容器实例化单例Bean而实例化。
其生成有两种方式:
1.RefreshScope接受到ContextRefreshedEvent事件后会根据判断决定是否直接实例化这些真实的bean。
2.如果方法调用时真实的bean没有生成则会实例化并将其放入缓存。
我们通过使用代理对象来操作真正的对象。
对象实例化后接下来就是我们使用了,在使用过程中我们更改了配置中心的配置文件进行配置下发时这些Bean中的属性如何刷新呢?
简单来说就是刷新之后我们的真实对象会销毁,再次调用时会生成一个新的对象使用。来看代码:
我们直接从ContextRefresher类看,适配springCLoud的这些配置中心感知到配置发生变化后会发布一个RefreshEvent事件告知Spring
cloud配置更改了,这时RefreshEventListener接受到事件后会调用ContextRefresher的refresh方法来处理这次刷新动作。
public synchronized Set<String> refresh() { Set<String> keys = refreshEnvironment(); this.scope.refreshAll(); return keys; }
我们主要看scope.refreshAll方法。这个第一个动作时重新构建一个applicationContext执行一个完整的上下文创建动作,主要目的是生成一个新的
Enviroment对象,里面包含的所有数据源都是配置更新的,然后将新的Enviroment对象中的数据源替换至当前上下文中的Enviroment对象中,这样
进行完第一步后我们当前的上下文中的所有k v配置都是最新的了。
public void refreshAll() { super.destroy(); this.context.publishEvent(new RefreshScopeRefreshedEvent()); }
我们看RefreshScope中的方法很简单调用了父类的destroy方法,然后发布一个事件。
public void destroy() { List<Throwable> errors = new ArrayList<Throwable>(); Collection<BeanLifecycleWrapper> wrappers = this.cache.clear(); for (BeanLifecycleWrapper wrapper : wrappers) { try { Lock lock = this.locks.get(wrapper.getName()).writeLock(); lock.lock(); try { wrapper.destroy(); } finally { lock.unlock(); } } catch (RuntimeException e) { errors.add(e); } } if (!errors.isEmpty()) { throw wrapIfNecessary(errors.get(0)); } this.errors.clear(); }
destroy方法会将当前存储的refreshScope类型的真实的对象清空,然后调用一些销毁的回调方法。
前面我们提到了refreshScope真实对象生成后会放入这个cache中以便下次使用,如果缓存中没有
则会生成一个新的。新生成的对象就会通过ioc的populate、initialize方法重新依赖注入新的属性了,
这样我们就是实现了配置的热更新,对于使用者而言没有感觉,我们依旧使用的是我们的代理对象,
但是其背部的真实对象可能重新生成了好几个了。
对了除了@RefreshScope注解可以更新属性外,上边我们看到ContextRefresher有一步刷新enviroment的
动作,在这个方法内部会发布一个EnviromentChanged的事件,此时ConfigurationPropertiesRebinder会
监听这个事件并且触发rebind方法,调用binder对所有使用@ConfigurationProperties注解的自动装配对象
进行一个重绑定属性的动作。具体方法是调用bean的destory方法之后在调用initializeBean方法:
public boolean rebind(String name) { if (!this.beans.getBeanNames().contains(name)) { return false; } if (this.applicationContext != null) { try { Object bean = this.applicationContext.getBean(name); if (AopUtils.isAopProxy(bean)) { bean = ProxyUtils.getTargetObject(bean); } if (bean != null) { // TODO: determine a more general approach to fix this. // see https://github.com/spring-cloud/spring-cloud-commons/issues/571 if (getNeverRefreshable().contains(bean.getClass().getName())) { return false; // ignore } this.applicationContext.getAutowireCapableBeanFactory() .destroyBean(bean); this.applicationContext.getAutowireCapableBeanFactory() .initializeBean(bean, name); return true; } } catch (RuntimeException e) { this.errors.put(name, e); throw e; } catch (Exception e) { this.errors.put(name, e); throw new IllegalStateException("Cannot rebind to " + name, e); } } return false; }
这样也可以在一定程度上进行属性的刷新。但是对于hashMap这种结构会有问题,主要原因是因为这里调用
destoryBean并不代表讲个bean删除了重新构建一个新的bean,而只是回调一些destory的接口,此时该bean
的属性值都还在,删除kv的时候bean中的kv并不会更新删除,详细可以看看MapBinder如何操作的。
解决方法可以实现DisposableBean的destory方法将map结构的属性设置为Null就可以了。