本篇文章涉及底層設計以及原理,以及問題定位,比較深入,篇幅較長,所以拆分成上下兩篇:
- 上:問題簡單描述以及 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 代理:
@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
,對應源碼是:
@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,他的核心邏輯也非常簡單:
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,對應源碼:
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 分別提了修改:
大家如果在項目中使用了 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: