本篇文章涉及底層設計以及原理,以及問題定位,比較深入,篇幅較長,是以拆分成上下兩篇:
上:問題簡單描述以及 Spring Cloud RefreshScope 的原理
下:目前 spring-cloud-openfeign + spring-cloud-sleuth 帶來的 bug 以及如何修複
其實在測試的程式中,我們已經實作了一個簡單的 Bean 重新整理的設計。Spring Cloud 的自動重新整理中,包含兩種元素的重新整理,分别是:
配置重新整理,即 <code>Environment.getProperties</code> 和 <code>@ConfigurationProperties</code> 相關 Bean 的重新整理
添加了 <code>@RefreshScope</code> 注解的 Bean 的重新整理
<code>@RefreshScope</code> 注解其實和我們上面自定義 Scope 使用的注解配置類似,即指定名稱為 <code>refresh</code>,同時使用 CGLIB 代理:
<code>RefreshScope</code>
同時需要自定義 Scope 進行注冊,這個自定義的 Scope 即 <code>org.springframework.cloud.context.scope.refresh.RefreshScope</code>,他繼承了 <code>GenericScope</code>,我們先來看這個父類,我們專注我們前面測試的那三個 Scope 接口方法,首先是 get:
然後是注冊 Destroy 的回調,其實就放在對應的 Bean 中,在移除的時候,會調用這個回調:
最後是移除 Bean,就更簡單了,從緩存中移除這個 Bean:
這樣,如果緩存中的 bean 被移除,下次調用 get 的時候,就會重新生成 Bean。并且,由于 RefreshScope 注解中預設的 ScopedProxyMode 為 CGLIB 代理模式,是以每次通過 BeanFactory 擷取 Bean 以及自動裝載的 Bean 調用的時候,都會調用這裡 Scope 的 get 方法。
Spring Cloud 将動态重新整理接口通過 Spring Boot Actuator 進行暴露,對應路徑是 <code>/actuator/refresh</code>,對應源碼是:
<code>RefreshEndpoint</code>
可以看出其核心是 ContextRefresher,他的核心邏輯也非常簡單:
<code>ContextRefresher</code>
調用 RefreshScope 的 RefreshAll,其實就是調用我們上面說的 GenericScope 的 destroy,之後釋出 RefreshScopeRefreshedEvent:
GenericScope 的 destroy 其實就是将緩存清空,這樣所有标注 <code>@RefreshScope</code> 注解的 Bean 都會被重建。
通過上篇的源碼分析,我們知道,如果想實作 Feign.Options 的動态重新整理,目前我們不能把它放入 NamedContextFactory 生成的 ApplicationContext 中,而是需要将它放入項目的根 ApplicationContext 中,這樣 Spring Cloud 暴露的 refresh actuator 接口,才能正确重新整理。spring-cloud-openfeign 中,也是這麼實作的。
如果配置了
那麼在初始化每個 FeignClient 的時候,就會将 Feign.Options 這個 Bean 注冊到根 ApplicationContext,對應源碼:
<code>FeignClientsRegistrar</code>
這樣,在調用 <code>/actuator/refresh</code> 接口的時候,這些 Feign.Options 也會被重新整理。但是注冊到根 ApplicationContext 中的話,對應的 FeignClient 如何擷取這個 Bean 使用呢?即在 Feign 的 NamedContextFactory (即 FeignContext )中生成的 ApplicationContext 中,如何找到這個 Bean 呢?
這個我們不用擔心,因為所有的 NamedContextFactory 生成的 ApplicationContext 的 parent,都設定為了根 ApplicationContext,參考源碼:
這樣設定後,FeignClient 在自己的 ApplicationContext 中如果找不到的話,就會去 parent 的 ApplicationContext 也就是根 ApplicationContext 去找。
這樣看來,設計是沒問題的,但是我們的項目啟動不了,應該是啟用其他依賴導緻的。
我們在擷取 Feign.Options Bean 的地方打斷點調試,發現并不是直接從 FeignContext 中擷取 Bean,而是從 spring-cloud-sleuth 的 TraceFeignContext 中擷取的。
spring-cloud-sleuth 為了保持鍊路,在很多地方增加了埋點,對于 OpenFeign 也不例外。在 <code>FeignContextBeanPostProcessor</code>,将 FeignContext 包裝了一層變成了 TraceFeignContext:
這樣,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 想開啟自動重新整理的話,可以考慮使用同名同路徑的類替換代碼先解決這個問題。等待我送出的代碼釋出新版本了。
參考代碼:
微信搜尋“我的程式設計喵”關注公衆号,每日一刷,輕松提升技術,斬獲各種offer: