SpringCloud NamedContextFactory 原理與使用

最近在閱讀 Ribbon 的源碼禀晓,發(fā)現(xiàn) SpringCloud 中 NamedContextFactory 這個類可以實現(xiàn)子容器。Ribbon 為每個 ServiceName 都擁有自己的 Spring Context 和 Bean 實例(不同服務之間的 LoadBalancer 和其依賴的 Bean 都是完全隔離的)击纬。

這么做有什么好處呢

  • 子容器之間數(shù)據(jù)隔離栅表。不同的 LoadBalancer 只管理自己的服務實例谓晌,明確自己的職責笤妙。
  • 子容器之間配置隔離硅则。不同的 LoadBalancer 可以使用不同的配置淹父。例如報表服務需要統(tǒng)計和查詢大量數(shù)據(jù),響應時間可能很慢怎虫。而會員服務邏輯相對簡單暑认,所以兩個服務的響應超時時間可能要求不同。
  • 子容器之間 Bean 隔離大审≌杭剩可以讓子容器之間注冊不同的 Bean。例如訂單服務的 LoadBalancer 底層通過 Nacos 獲取實例徒扶,會員服務的 LoadBalancer 底層通過 Eureka 獲取實例粮彤。也可以讓不同的 LoadBalancer 采用不同的算法

NamedContextFactory 簡介

NamedContextFactory 可以創(chuàng)建一個子容器(或者說子上下文),每個子容器可以通過 Specification 定義 Bean姜骡。 移植自 spring-cloud-netflix FeignClientFactory 和 SpringClientFactory

上面是對于 NamedContextFactory 類注釋的翻譯导坟。

接下來,我會使用 NamedContextFactory 實現(xiàn)一個 demo圈澈,便于各位理解惫周。

我實在沒想到什么特別好的場景。所以我仿照 Ribbon 實現(xiàn)一個 HttpClient康栈,每個子容器的 HttpClient 生效不同的配置闯两,創(chuàng)建不同的 Bean

子容器的定制

NamedContextFactory

下面來進入正題

子容器需要通過 NamedContextFactory 來創(chuàng)建。首先我們先繼承一下該類實現(xiàn)一個自己的 Factory

@Component
public class NamedHttpClientFactory extends NamedContextFactory<NamedHttpClientSpec> {

    public NamedHttpClientFactory() {
        super(NamedHttpClientConfiguration.class, "namedHttpClient", "http.client.name");
    }

}

解釋一下上述 super 構造方法三個參數(shù)的含義

  • 第一個參數(shù)谅将,默認配置類。當使用 NamedHttpClientFactory 創(chuàng)建子容器時重慢,NamedHttpClientConfiguration 一定會被加載
  • 第二個參數(shù)饥臂,我目前沒發(fā)現(xiàn)有什么用,真的就是隨便定義一個 name
  • 第三個參數(shù),很重要。
    創(chuàng)建子容器時通常會提供子容器的容器 name嗜暴。子容器中的 Environment 會被寫入一條配置湿故,http.client.name=容器name(也就是說,子容器可以通過讀取配置 http.client.name 來獲取容器名)

看到這可能還是很迷惑沼填,這實際有什么用呢?以 Ribbon 為例,容器名就是 ServiceName驰坊,Ribbon 可以在配置文件中定制每個子容器的配置或者 Bean,配置如下

# 訂單服務的超時時間為 3000
orderService.ribbon.ReadTimeout = 3000
# 指定 orderService 容器的 ServerList
orderService.ribbon.NIWSServerListClassName = com.netflix.loadbalancer.ConfigurationBasedServerList

當每個子容器都知道自己的容器名時哮独,就可以找到自己對應的配置了

接下來看下拳芙,我的默認配置類都干了什么

public class NamedHttpClientConfiguration {

    @Value("${http.client.name}")
    private String httpClientName; // 1.

    @Bean
    @ConditionalOnMissingBean
    public ClientConfig clientConfig(Environment env) {
        return new ClientConfig(httpClientName, env); // 2.
    }

    @Bean
    @ConditionalOnMissingBean
    public NamedHttpClient namedHttpClient(ClientConfig clientConfig) {
        return new NamedHttpClient(httpClientName, clientConfig); // 3.
    }

}
  1. @Value("${http.client.name}")察藐,結合上邊講的,這樣可以讀到當前子容器的 name
  2. ClientConfig舟扎,負責根據(jù)容器 name分飞,加載屬于自己的配置。代碼比較簡單就不貼出來了
  3. NamedHttpClient睹限,簡單的包裝一下 HttpClient譬猫,會根據(jù) ClientConfig 對 HttpClient 進行配置

NamedContextFactory.Specification

上面講的是可以手動編程來定制子容器的 Bean,NamedContextFactory 也提供了定制子容器的接口 NamedContextFactory.Specification羡疗。

public class NamedHttpClientSpec implements NamedContextFactory.Specification {

    private final String name;
    private final Class<?>[] configuration;

    public NamedHttpClientSpec(String name, Class<?>[] configuration) {
        this.name = name;
        this.configuration = configuration;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Class<?>[] getConfiguration() {
        return configuration;
    }
}

我們簡單的實現(xiàn)一下該接口染服,然后通過 NamedHttpClientFactory#setConfigurations,將 Specification 賦值給 NamedHttpClientFactory顺囊。

創(chuàng)建子容器時肌索,如果容器的 name 匹配了 Specification 的 name,則會加載 Specification 對應 Configuration 類特碳。

題外話: @RibbonClient 也是通過 NamedContextFactory.Specification 實現(xiàn)的

Run 一下

講到這诚亚,也許你還是沒懂,沒關系午乓,建議 Debug 一下這個單元測試

public class NamedContextFactoryTest {

    private void initEnv(AnnotationConfigApplicationContext parent) {
        Map<String, Object> map = new HashMap<>();
        map.put("baidu.socketTimeout", 123);
        map.put("google.socketTimeout", 456);
        parent.getEnvironment()
                .getPropertySources()
                .addFirst(new MapPropertySource("test", map));
    }

    @Test
    public void test() {
        // 創(chuàng)建 parent context
        AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
        // parent context 的 Bean站宗,可以被子容器繼承
        parent.register(ParentConfiguration.class);
        initEnv(parent);
        parent.refresh();

        // 容器 name = baidu 的 context 中會注冊 TestConfiguration
        NamedHttpClientSpec spec = new NamedHttpClientSpec("baidu", new Class[]{TestConfiguration.class});

        NamedHttpClientFactory namedHttpClientFactory = new NamedHttpClientFactory();
        // SpringBoot 中無需手動設置,會自動注入 parent
        namedHttpClientFactory.setApplicationContext(parent);
        namedHttpClientFactory.setConfigurations(List.of(spec));

        // 準備工作完成益愈,現(xiàn)在開始通過 NamedContextFactory get Bean
        ParentBean baiduParentBean = namedHttpClientFactory.getInstance("baidu", ParentBean.class);
        NamedHttpClient baidu = namedHttpClientFactory.getInstance("baidu", NamedHttpClient.class);
        TestBean baiduTestBean = namedHttpClientFactory.getInstance("baidu", TestBean.class);

        Assert.assertNotNull(baiduParentBean);
        Assert.assertEquals("baidu", baidu.getServiceName());
        Assert.assertEquals(123, baidu.getRequestConfig().getSocketTimeout());
        Assert.assertNotNull(baiduTestBean);

        ParentBean googleParentBean = namedHttpClientFactory.getInstance("google", ParentBean.class);
        NamedHttpClient google = namedHttpClientFactory.getInstance("google", NamedHttpClient.class);
        TestBean googleTestBean = namedHttpClientFactory.getInstance("google", TestBean.class);

        Assert.assertNotNull(googleParentBean);
        Assert.assertEquals("google", google.getServiceName());
        Assert.assertEquals(456, google.getRequestConfig().getSocketTimeout());
        Assert.assertNull(googleTestBean);
    }

    static class ParentConfiguration {
        @Bean
        public ParentBean parentBean() {
            return new ParentBean();
        }
    }

    static class TestConfiguration {
        @Bean
        public TestBean testBean() {
            return new TestBean();
        }
    }


    static class ParentBean {

    }

    static class TestBean {

    }

}

UT 完整運行參考??
https://github.com/TavenYin/taven-springcloud-learning/blob/master/springcloud-alibaba-nacos/nacos-discovery/src/test/java/com/github/taven/NamedContextFactoryTest.java

Spring 項目中使用子容器參考 ??
https://github.com/TavenYin/taven-springcloud-learning/tree/master/springcloud-alibaba-nacos/nacos-discovery/src/main/java/com/github/taven/namedcontext

如果某個 Configuration 類梢灭,只需要子容器加載,那么你可以不添加 @Configuration蒸其,這樣就不會被 Spring 容器(父容器)加載了敏释。

NamedContextFactory 源碼分析

使用該類的入口通常是 getInstance 方法

    public <T> T getInstance(String name, Class<T> type) {
        // 1. 獲取子容器
        AnnotationConfigApplicationContext context = getContext(name);
        try {  
            // 2. 從子容器中獲取 Bean
            return context.getBean(type);
        }
        catch (NoSuchBeanDefinitionException e) {
            // ignore
        }
        return null;
    }

這個方法內部邏輯很簡單

  1. 獲取子容器(如果不存在的話,會創(chuàng)建)
  2. 從(子)容器中獲取 Bean摸袁,這步就可理解為和常規(guī) Spring 操作一樣了钥顽,從容器中獲取 Bean(子容器只是概念上的一個東西,實際 API 都是一樣的)

所以下面我們重點看下 getContext 方法做了什么

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
        implements DisposableBean, ApplicationContextAware {

    private Map<String, C> configurations = new ConcurrentHashMap<>(); // 1.
    
    // 省略其他成員變量
    
    protected AnnotationConfigApplicationContext createContext(String name) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        if (this.configurations.containsKey(name)) { // 2.
            for (Class<?> configuration : this.configurations.get(name)
                    .getConfiguration()) {
                context.register(configuration);
            }
        }
        for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
            if (entry.getKey().startsWith("default.")) { // 3.
                for (Class<?> configuration : entry.getValue().getConfiguration()) {
                    context.register(configuration);
                }
            }
        }
        context.register(PropertyPlaceholderAutoConfiguration.class,
                this.defaultConfigType); // 4. 
        context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
                this.propertySourceName,
                Collections.<String, Object>singletonMap(this.propertyName, name))); // 5.
        if (this.parent != null) {
            // Uses Environment from parent as well as beans
            context.setParent(this.parent);
            // jdk11 issue
            // https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
            context.setClassLoader(this.parent.getClassLoader());
        }
        context.setDisplayName(generateDisplayName(name));
        context.refresh();
        return context;
    }

    // 省略其他方法
}   
  1. 該 Map 的 value 為 Specification 的實現(xiàn)(用于提供 Configuration)靠汁,name 為容器名蜂大,用于定制每個子容器的配置
  2. 如果 name 匹配,則加載 Configuration
  3. 如果 Specification 的 name 以 default. 開頭蝶怔,則每個子容器創(chuàng)建時奶浦,都會加載這些配置
  4. 子容器中注冊 PropertyPlaceholderAutoConfiguration,以及 defaultConfigType(PropertyPlaceholderAutoConfiguration 用于解析 Bean 或者 @Value 中的占位符踢星。defaultConfigType 是上文中提到過的澳叉,構造方法中提供的子容器默認配置類)
  5. 也是我們上文中說過的,子容器中寫入一條配置。以上文為例耳高,會在容器中寫入一條 http.client.name=容器name
  6. 剩下的就是扎瓶,設置父容器,以及初始化操作

最后

如果覺得我的文章對你有幫助泌枪,動動小手點下關注或者喜歡概荷,你的支持是對我最大的幫助

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市碌燕,隨后出現(xiàn)的幾起案子误证,更是在濱河造成了極大的恐慌,老刑警劉巖修壕,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件愈捅,死亡現(xiàn)場離奇詭異,居然都是意外死亡慈鸠,警方通過查閱死者的電腦和手機蓝谨,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來青团,“玉大人譬巫,你說我怎么就攤上這事《桨剩” “怎么了芦昔?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長娃肿。 經(jīng)常有香客問我咕缎,道長,這世上最難降的妖魔是什么料扰? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任凭豪,我火速辦了婚禮,結果婚禮上晒杈,老公的妹妹穿的比我還像新娘墅诡。我一直安慰自己,他們只是感情好桐智,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著烟馅,像睡著了一般说庭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上郑趁,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天刊驴,我揣著相機與錄音,去河邊找鬼。 笑死捆憎,一個胖子當著我的面吹牛舅柜,可吹牛的內容都是我干的。 我是一名探鬼主播躲惰,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼致份,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了础拨?” 一聲冷哼從身側響起氮块,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎诡宗,沒想到半個月后滔蝉,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡塔沃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年蝠引,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛀柴。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡螃概,死狀恐怖,靈堂內的尸體忽然破棺而出名扛,到底是詐尸還是另有隱情谅年,我是刑警寧澤,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布肮韧,位于F島的核電站融蹂,受9級特大地震影響,放射性物質發(fā)生泄漏弄企。R本人自食惡果不足惜超燃,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拘领。 院中可真熱鬧意乓,春花似錦、人聲如沸约素。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽圣猎。三九已至士葫,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間送悔,已是汗流浹背慢显。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工爪模, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荚藻。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓屋灌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親应狱。 傳聞我的和親對象是個殘疾皇子共郭,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355

推薦閱讀更多精彩內容