Spring Cloud NamedContextFactory 原理分析

Spring Cloud NamedContextFactory 原理分析,第1张

系列文章:
Spring Cloud LoadBalancer之负载均衡简介
Spring Cloud NamedContextFactory 原理分析
Ribbon 的替代品 Spring Cloud Loadbalancer 使用与原理分析

测试代码地址

1.1 NamedContextFactory 作用

ribbon 与 loadbalancer 都能做到对不同的调用服务使用不同的负载均衡配置。

例如:数据服务需要查询的时间较长,用户中心服务查询用户信息的时间较短,二者可以采用不同的负载策略。

要实现上述要求,可以使用 Spring Cloud 提供的 NamedContextFactory 实现;他的作用就是根据名称去存储不同的 ApplicationContext,这样就能实现不同的名称的 Bean 隔离了。

由于NamedContextFactory 类有一个字段(parent)保存了最外层的 Bean 上下文,因此可以获取到父级传过来的上下文内容。

以下图为例,在同一个 NamedContextFactory 中,parent有一个 paren bean,两个 children都有各自的 child bean,children 上下文是能够访问到父上下文的,并且访问到的父 bean 都是同一个类实例。

以下代码是校验从父容器获取到的 parentBean 是否为同一个,这个测试用例是能通过的:

@Test
public void context() {
    AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
    parent.register(ParentConfig.class);
    parent.refresh();

    CustomNamedContextFactoryClient client = new CustomNamedContextFactoryClient(DefaultConfig.class);

    // 子容器配置
    CustomSpecification huaweiSpecification = new CustomSpecification("huawei", new Class[]{HuaweiConfig.class});
    CustomSpecification appleSpecification = new CustomSpecification("apple", new Class[]{AppleConfig.class});

    // 添加子容器到父容器中
    client.setApplicationContext(parent);
    client.setConfigurations(List.of(huaweiSpecification, appleSpecification));

    // 父容器的 bean
    ParentBean parentBean = parent.getBean(ParentBean.class);

    // 根据名称获取子容器
    ParentBean huaweiParentBean = client.getInstance("huawei", ParentBean.class);
    // huawei 子容器能够获取父容器中的 bean
    System.out.println(huaweiParentBean.getName());

    // 根据名称获取子容器
    ParentBean appleParentBean = client.getInstance("apple", ParentBean.class);
    // apple 子容器能够获取父容器中的 bean
    System.out.println(appleParentBean.getName());

    // 获取到父容器的 bean 是否相同
    Assertions.assertEquals(huaweiParentBean, appleParentBean);
}

1.2 NamedContextFactory 原理分析

NamedContextFactory 的构造方法有 3 个入参,实现类是必须要传过来的:

  /**
    * 构造方法
    *
    * @param defaultConfigType  默认配置类型,所有的子容器都会存在该默认配置
    * @param propertySourceName 配置名称,暂未发现有什么用
    * @param propertyName       在子容器使用该名称注入一个 String 就能获取子容器的名称;例如 子容器名称为 huawei,propertyName 定义为 		
    							phone.name,那么可以在子容器使用 @Value("${phone.name}") 注入 huawei 字符串
    */
    public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName, String propertyName) {
        this.defaultConfigType = defaultConfigType;
        this.propertySourceName = propertySourceName;
        this.propertyName = propertyName;
    }

从 NamedContextFactory 中根据子容器名称与获取一个实例,不存在则新建:

 /**
     * 获取一个实例
     *
     * @param name 子容器名称
     * @param type 实例的类型
     * @param 
     * @return
     */
    public <T> T getInstance(String name, Class<T> type) {
        // 根据名称获取子容器
        AnnotationConfigApplicationContext context = getContext(name);
        try {
            // 与普通的 ApplicationContext 一样,可以通过 getBean 方法获取实例
            return context.getBean(type);
        } catch (NoSuchBeanDefinitionException e) {
            // ignore
        }
        return null;
    }

getContext(String) 根据子容器名称获取上下文的方法,如下文已存在则直接返回,否则新建一个 AnnotationConfigApplicationContext 上下文:


    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
    
    protected AnnotationConfigApplicationContext getContext(String name) {
        // 双重检查锁防止在多线程环境下重复创建上下文(单例模式)。
        if (!this.contexts.containsKey(name)) {
            synchronized (this.contexts) {
                if (!this.contexts.containsKey(name)) {
                    this.contexts.put(name, createContext(name));
                }
            }
        }
        return this.contexts.get(name);
    }

createContext(String) 作用是新建一个 name 对应的上下文,具体分析看代码中的注释:

    /**
     * NamedContextFactory.Specification 的实现类,保存子容器的配置类;
     * 可以使用 client.setConfigurations(List.of(huaweiSpecification, appleSpecification)); 设置
     */
    private Map<String, C> configurations = new ConcurrentHashMap<>();
    
    /**
     * 默认配置类型,所有的子容器都会存在该默认配置
     */
    private Class<?> defaultConfigType;
    
    /**
     * 配置名称,暂未发现有什么用
     */
    private final String propertySourceName;
    
    /**
     * 在子容器使用该名称注入一个 String 就能获取子容器的名称;例如 子容器名称为 huawei,propertyName 定义为 		
     * phone.name,那么可以在子容器使用 @Value("${phone.name}") 注入 huawei 字符串
     */
    private final String propertyName;
    
    protected AnnotationConfigApplicationContext createContext(String name) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        // 如果 configurations 中有对应的 name 配置,则将设置的配置类注册到子容器中
        if (this.configurations.containsKey(name)) {
            for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
                context.register(configuration);
            }
        }
        // 如果 configurations 的 key 包含有 default. 开头则表示默认配置,所有的子容器都应该注册 bean
        for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
            if (entry.getKey().startsWith("default.")) {
                for (Class<?> configuration : entry.getValue().getConfiguration()) {
                    context.register(configuration);
                }
            }
        }
        // 注册配置文件自动配置类与前面构造方法的默认配置类
        context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
        // 构造方法的 propertyName 在这里会设置上对应的值,值为传过来的 name (子容器名称)
        context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,
                Collections.<String, Object>singletonMap(this.propertyName, name)));
        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;
    }

总结:
从源码来看,NamedContextFactory 实现子容器隔离的功能并不复杂,说白了就是拿 Map 将容器名与其对应的 ApplicationContext 上下文存起来而已,但是在负载均衡中却能够有很多妙用,比如 ribbon 可以通过 子容器名.socketTimeout=1000 这样设置对应容器的超时时间等等。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/724838.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-26
下一篇 2022-04-26

发表评论

登录后才能评论

评论列表(0条)

保存