本篇文章波及底层设计以及原理,以及问题定位,比拟深刻,篇幅较长,所以拆分成高低两篇:
- 上:问题简略形容以及 Spring Cloud RefreshScope 的原理
- 下:以后 spring-cloud-openfeign + spring-cloud-sleuth 带来的 bug 以及如何修复
Spring Cloud 中的配置动静刷新
其实在测试的程序中,咱们曾经实现了一个简略的 Bean 刷新的设计。Spring Cloud 的主动刷新中,蕴含两种元素的刷新,别离是:
- 配置刷新,即
Environment.getProperties
和@ConfigurationProperties
相干 Bean 的刷新 - 增加了
@RefreshScope
注解的 Bean 的刷新
@RefreshScope
注解其实和咱们下面自定义 Scope 应用的注解配置相似,即指定名称为 refresh
,同时应用 CGLIB 代理:
RefreshScope
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Scope("refresh")
@Documented
public @interface RefreshScope {
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
同时须要自定义 Scope 进行注册,这个自定义的 Scope 即 org.springframework.cloud.context.scope.refresh.RefreshScope
,他继承了 GenericScope
,咱们先来看这个父类,咱们专一咱们后面测试的那三个 Scope 接口办法,首先是 get:
private BeanLifecycleWrapperCache cache = new BeanLifecycleWrapperCache(new StandardScopeCache());
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
//放入缓存
BeanLifecycleWrapper value = this.cache.put(name, new BeanLifecycleWrapper(name, objectFactory));
this.locks.putIfAbsent(name, new ReentrantReadWriteLock());
try {
//这里在第一次调用会创立 Bean 实例,所以须要上锁,保障只创立一次
return value.getBean();
}
catch (RuntimeException e) {
this.errors.put(name, e);
throw e;
}
}
而后是注册 Destroy 的回调,其实就放在对应的 Bean 中,在移除的时候,会调用这个回调:
@Override
public void registerDestructionCallback(String name, Runnable callback) {
BeanLifecycleWrapper value = this.cache.get(name);
if (value == null) {
return;
}
value.setDestroyCallback(callback);
}
最初是移除 Bean,就更简略了,从缓存中移除这个 Bean:
@Override
public Object remove(String name) {
BeanLifecycleWrapper value = this.cache.remove(name);
if (value == null) {
return null;
}
return value.getBean();
}
这样,如果缓存中的 bean 被移除,下次调用 get 的时候,就会从新生成 Bean。并且,因为 RefreshScope 注解中默认的 ScopedProxyMode 为 CGLIB 代理模式,所以每次通过 BeanFactory 获取 Bean 以及主动装载的 Bean 调用的时候,都会调用这里 Scope 的 get 办法。
Spring Cloud 将动静刷新接口通过 Spring Boot Actuator 进行裸露,对应门路是 /actuator/refresh
,对应源码是:
RefreshEndpoint
@Endpoint(id = "refresh")
public class RefreshEndpoint {
private ContextRefresher contextRefresher;
public RefreshEndpoint(ContextRefresher contextRefresher) {
this.contextRefresher = contextRefresher;
}
@WriteOperation
public Collection<String> refresh() {
Set<String> keys = this.contextRefresher.refresh();
return keys;
}
}
能够看出其外围是 ContextRefresher,他的外围逻辑也非常简单:
ContextRefresher
public synchronized Set<String> refresh() {
Set<String> keys = refreshEnvironment();
//刷新 RefreshScope
this.scope.refreshAll();
return keys;
}
public synchronized Set<String> refreshEnvironment() {
//提取 SYSTEM、JNDI、SERVLET 之外所有参数变量
Map<String, Object> before = extract(this.context.getEnvironment().getPropertySources());
//从配置源更新 Environment 中的所有属性
updateEnvironment();
//与刷新前作比照,提取出所有变了的属性
Set<String> keys = changes(before, extract(this.context.getEnvironment().getPropertySources())).keySet();
//将该变了的属性,放入 EnvironmentChangeEvent 并公布
this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
//返回所有扭转的属性
return keys;
}
调用 RefreshScope 的 RefreshAll,其实就是调用咱们下面说的 GenericScope 的 destroy,之后公布 RefreshScopeRefreshedEvent:
public void refreshAll() {
super.destroy();
this.context.publishEvent(new RefreshScopeRefreshedEvent());
}
GenericScope 的 destroy 其实就是将缓存清空,这样所有标注 @RefreshScope
注解的 Bean 都会被重建。
问题定位
通过上篇的源码剖析,咱们晓得,如果想实现 Feign.Options 的动静刷新,目前咱们不能把它放入 NamedContextFactory 生成的 ApplicationContext 中,而是须要将它放入我的项目的根 ApplicationContext 中,这样 Spring Cloud 裸露的 refresh actuator 接口,能力正确刷新。spring-cloud-openfeign 中,也是这么实现的。
如果配置了
feign.client.refresh-enabled: true
那么在初始化每个 FeignClient 的时候,就会将 Feign.Options 这个 Bean 注册到根 ApplicationContext,对应源码:
FeignClientsRegistrar
private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) {
if (isClientRefreshEnabled()) {
//应用 "feign.Request.Options-FeignClient 的 contextId" 作为 Bean 名称
String beanName = Request.Options.class.getCanonicalName() + "-" + contextId;
BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
.genericBeanDefinition(OptionsFactoryBean.class);
//设置为 RefreshScope
definitionBuilder.setScope("refresh");
definitionBuilder.addPropertyValue("contextId", contextId);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(),
beanName);
//注册为 CGLIB 代理的 Bean
definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true);
//注册 Bean
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
}
}
private boolean isClientRefreshEnabled() {
return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false);
}
这样,在调用 /actuator/refresh
接口的时候,这些 Feign.Options 也会被刷新。然而注册到根 ApplicationContext 中的话,对应的 FeignClient 如何获取这个 Bean 应用呢?即在 Feign 的 NamedContextFactory (即 FeignContext )中生成的 ApplicationContext 中,如何找到这个 Bean 呢?
这个咱们不必放心,因为所有的 NamedContextFactory 生成的 ApplicationContext 的 parent,都设置为了根 ApplicationContext,参考源码:
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private ApplicationContext parent;
@Override
public void setApplicationContext(ApplicationContext parent) throws BeansException {
this.parent = parent;
}
protected AnnotationConfigApplicationContext createContext(String name) {
//省略其余代码
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
}
//省略其余代码
}
}
这样设置后,FeignClient 在本人的 ApplicationContext 中如果找不到的话,就会去 parent 的 ApplicationContext 也就是根 ApplicationContext 去找。
这样看来,设计是没问题的,然而咱们的我的项目启动不了,应该是启用其余依赖导致的。
咱们在获取 Feign.Options Bean 的中央打断点调试,发现并不是间接从 FeignContext 中获取 Bean,而是从 spring-cloud-sleuth 的 TraceFeignContext 中获取的。
spring-cloud-sleuth 为了放弃链路,在很多中央减少了埋点,对于 OpenFeign 也不例外。在 FeignContextBeanPostProcessor
,将 FeignContext 包装了一层变成了 TraceFeignContext:
public class FeignContextBeanPostProcessor implements BeanPostProcessor {
private final BeanFactory beanFactory;
public FeignContextBeanPostProcessor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) {
return new TraceFeignContext(traceFeignObjectWrapper(), (FeignContext) bean);
}
return bean;
}
private TraceFeignObjectWrapper traceFeignObjectWrapper() {
return new TraceFeignObjectWrapper(this.beanFactory);
}
}
这样,FeignClient 会从这个 TraceFeignContext 中读取 Bean,而不是 FeignContext。然而通过源码咱们发现,TraceFeignContext 并没有设置 parent 为根 ApplicationContext,所以找不到注册到根 ApplicationContext 中的 Feign.Options 这些 Bean。
解决问题
针对这个 Bug,我向 spring-cloud-sleuth 和 spring-cloud-commons 别离提了批改:
- add getter for parent in NamedContextFactory
- fix #2023, add parent in the new TraceFeignContext
大家如果在我的项目中应用了 spring-cloud-sleuth,对于 spring-cloud-openfeign 想开启主动刷新的话,能够思考应用同名同门路的类替换代码先解决这个问题。期待我提交的代码公布新版本了。
参考代码:
public class FeignContextBeanPostProcessor implements BeanPostProcessor {
private static final Field PARENT;
private static final Log logger = LogFactory.getLog(FeignContextBeanPostProcessor.class);
static {
try {
PARENT = NamedContextFactory.class.getDeclaredField("parent");
PARENT.setAccessible(true);
} catch (Exception e) {
throw new Error(e);
}
}
private final BeanFactory beanFactory;
public FeignContextBeanPostProcessor(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FeignContext && !(bean instanceof TraceFeignContext)) {
FeignContext feignContext = (FeignContext) bean;
TraceFeignContext traceFeignContext = new TraceFeignContext(traceFeignObjectWrapper(), feignContext);
try {
traceFeignContext.setApplicationContext((ApplicationContext) PARENT.get(bean));
} catch (IllegalAccessException e) {
logger.warn("Cannot find parent in FeignContext: " + beanName);
}
return traceFeignContext;
}
return bean;
}
private TraceFeignObjectWrapper traceFeignObjectWrapper() {
return new TraceFeignObjectWrapper(this.beanFactory);
}
}
微信搜寻“我的编程喵”关注公众号,每日一刷,轻松晋升技术,斩获各种offer:
发表回复