SpringCloud NamedContextFactory 原理与使用

最近在阅读 Ribbon 的源码,发现 SpringCloud 中 NamedContextFactory 这个类可以实现子容器。Ribbon 为每个 ServiceName 都拥有自己的 Spring Context 和 Bean 实例(不同服务之间的 LoadBalancer 和其依赖的 Bean 都是完全隔离的)。

这么做有什么好处呢

  • 子容器之间数据隔离。不同的 LoadBalancer 只管理自己的服务实例,明确自己的职责。
  • 子容器之间配置隔离。不同的 LoadBalancer 可以使用不同的配置。例如报表服务需要统计和查询大量数据,响应时间可能很慢。而会员服务逻辑相对简单,所以两个服务的响应超时时间可能要求不同。
  • 子容器之间 Bean 隔离。可以让子容器之间注册不同的 Bean。例如订单服务的 LoadBalancer 底层通过 Nacos 获取实例,会员服务的 LoadBalancer 底层通过 Eureka 获取实例。也可以让不同的 LoadBalancer 采用不同的算法

NamedContextFactory 简介

NamedContextFactory 可以创建一个子容器(或者说子上下文),每个子容器可以通过 Specification 定义 Bean。 移植自 spring-cloud-netflix FeignClientFactory 和 SpringClientFactory

上面是对于 NamedContextFactory 类注释的翻译。

接下来,我会使用 NamedContextFactory 实现一个 demo,便于各位理解。

我实在没想到什么特别好的场景。所以我仿照 Ribbon 实现一个 HttpClient,每个子容器的 HttpClient 生效不同的配置,创建不同的 Bean

子容器的定制

NamedContextFactory

下面来进入正题

子容器需要通过 NamedContextFactory 来创建。首先我们先继承一下该类实现一个自己的 Factory

@Component
public class NamedHttpClientFactory extends NamedContextFactory<NamedHttpClientSpec> {

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

}

解释一下上述 super 构造方法三个参数的含义

  • 第一个参数,默认配置类。当使用 NamedHttpClientFactory 创建子容器时,NamedHttpClientConfiguration 一定会被加载
  • 第二个参数,我目前没发现有什么用,真的就是随便定义一个 name
  • 第三个参数,很重要。
    创建子容器时通常会提供子容器的容器 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,负责根据容器 name,加载属于自己的配置。代码比较简单就不贴出来了
  3. NamedHttpClient,简单的包装一下 HttpClient,会根据 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;
    }
}

我们简单的实现一下该接口,然后通过 NamedHttpClientFactory#setConfigurations,将 Specification 赋值给 NamedHttpClientFactory。

创建子容器时,如果容器的 name 匹配了 Specification 的 name,则会加载 Specification 对应 Configuration 类。

题外话: @RibbonClient 也是通过 NamedContextFactory.Specification 实现的

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() {
        // 创建 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));

        // 准备工作完成,现在开始通过 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. 获取子容器(如果不存在的话,会创建)
  2. 从(子)容器中获取 Bean,这步就可理解为和常规 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 的实现(用于提供 Configuration),name 为容器名,用于定制每个子容器的配置
  2. 如果 name 匹配,则加载 Configuration
  3. 如果 Specification 的 name 以 default. 开头,则每个子容器创建时,都会加载这些配置
  4. 子容器中注册 PropertyPlaceholderAutoConfiguration,以及 defaultConfigType(PropertyPlaceholderAutoConfiguration 用于解析 Bean 或者 @Value 中的占位符。defaultConfigType 是上文中提到过的,构造方法中提供的子容器默认配置类)
  5. 也是我们上文中说过的,子容器中写入一条配置。以上文为例,会在容器中写入一条 http.client.name=容器name
  6. 剩下的就是,设置父容器,以及初始化操作

最后

如果觉得我的文章对你有帮助,动动小手点下关注或者喜欢,你的支持是对我最大的帮助

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容