【OpenFeign】【NamedContextFactory】深入剖析 NamedContextFactory 的原理以及使用

1  前言

这几天看 OpenFeign 的源码,发现一个类 NamedContextFactory(带命名的上下文容器工厂),简单的说就是根据 name 隔离出来不同的 Context ,单看这个的话这个类还是比较重的哈,你比如说我有 10个名字,100个名字,那它岂不是要创建 10个上下文,100个上下文呀,是的,我 debug 的时候,确实是这样,比如我有很多个 feignClient 它就有好多个上下文,好啦,不多说了,我们就看看它的实现原理。

2  示例

首先我们从一个例子,先来感受下它的一个基本使用:

我先自定义一个命名的上下文 NamedContextFactory:

package com.virtuous.base;

import org.springframework.cloud.context.named.NamedContextFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

/**
 * 自定义的命名上下文
 */
class MyNamedContext extends NamedContextFactory {
    public MyNamedContext() {
        // MyDefaultConfig 是公共的默认配置
        super(MyDefaultConfig.class, "kuku", "kuku.name");
    }

    /**
     * 给 name 中的容器,注入类型
     * @param name
     * @param clazz
     */
    public void registerBean(String name, Class clazz) {
        AnnotationConfigApplicationContext context = getContext(name);
        context.registerBean(clazz);
    }
}

这是共享的公共的配置:

package com.virtuous.base;

import com.virtuous.base.bean.Demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author: kuku
 * @description
 */
@Configuration
public class MyDefaultConfig {

    @Bean
    public Demo demo() {
        return new Demo("default", "男");
    }
}

还有两个基础的类作为 Bean:

@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class Demo {

    private String name;

    private String gender;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
@ToString
public class Person {

    private String name;

    private String age;
}

这是我的测试类:

package com.virtuous.base;

import com.virtuous.base.bean.Demo;
import com.virtuous.base.bean.Person;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;

@SpringBootTest(classes = VirtuousBaseServiceApplication.class)
class MyTest {

    @Autowired
    private ApplicationContext applicationContext;

    @Test
    public void doTest() {
        MyNamedContext myNamedContext = new MyNamedContext();
        myNamedContext.setApplicationContext(applicationContext);

        Demo oneDemo = (Demo) myNamedContext.getInstance("one", Demo.class);
        Demo twoDemo = (Demo) myNamedContext.getInstance("two", Demo.class);
        System.out.println(String.format("oneDemo=%s", oneDemo));
        System.out.println(String.format("twoDemo=%s", twoDemo));
        Assertions.assertEquals(oneDemo, twoDemo);

        // 给 one 注册一个新的 Person
        myNamedContext.registerBean("one", Person.class);

        Person onePerson = (Person) myNamedContext.getInstance("one", Person.class);
        Person twoPerson = (Person) myNamedContext.getInstance("two", Person.class);
        System.out.println(String.format("onePerson=%s", onePerson));
        System.out.println(String.format("twoPerson=%s", twoPerson));
    }
}

测试结果:

并且我们来看下 MyNamedContext,是不是像我说的那样,每个命名都会创建一个上下文对象呢:

3  源码分析

3.1  类整体概览

首先我们先看下 NamedContextFactory,它是属于 SpringCloud 公共包里的内容:

 然后我们看下类里边大概有哪些内容:

3.2  构造方法

构造方法由三个参数构成:默认配置类、属性源名字、属性名字,默认配置好理解,就是每个容器都共享的,后边的两个名字主要是给每个容器设置名字,可能是方便每个容器处理哈,从我们上边的例子 MyNamedContext :

public MyNamedContext() {
    // MyDefaultConfig 是公共的默认配置
    super(MyDefaultConfig.class, "kuku", "kuku.name");
}

那么带来的效果就是:

那么这个属性源是什么时候设置进去的呢?我这里先直接说了,其实就是创建每个命名的上下文时设置进去的:

好啦,关于构造方法我们就到看这里。

3.3  获取名称上下文-getContext

我们继续看看 getContext :

/**
 * 获取指定名称的上下文
 * @param name 名称
 * @return 上下文对象
 * private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
 */
protected AnnotationConfigApplicationContext getContext(String name) {
    // 如果 contexts 中没有 name 的上下文
    if (!this.contexts.containsKey(name)) {
        // 加锁
        synchronized (this.contexts) {
            // 再拿一次  双重检查机制哈  源码中这样的写法看到过好多次  单单 spring 中就好多
            if (!this.contexts.containsKey(name)) {
                // 然后调用创建方法进行生成,然后放进 map 里
                this.contexts.put(name, createContext(name));
            }
        }
    }
    return this.contexts.get(name);
}

3.4  创建名称上下文-createContext

我们继续看看createContext,这个就比较重要了哈:

/**
 * 创建指定名称的上下文对象
 * @param name 名称
 * @return 上下文对象
 * private Map<String, C> configurations = new ConcurrentHashMap<>();
 */
protected AnnotationConfigApplicationContext createContext(String name) {
    // 可以看到创建的类型是 AnnotationConfigApplicationContext
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    // 判断当前名称下是否含有指定的配置 
    if (this.configurations.containsKey(name)) {
        // 有的话 就都注册到该 context 中
        for (Class<?> configuration : this.configurations.get(name)
                .getConfiguration()) {
            context.register(configuration);
        }
    }
    // C 就是 C extends NamedContextFactory.Specification 这个接口比较简单 一个名称方法 一个数组的配置
    // 判断是否包含默认的配置 即名字是 default. 开头的
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
        if (entry.getKey().startsWith("default.")) {
            for (Class<?> configuration : entry.getValue().getConfiguration()) {
                // 注册默认配置到 context 中
                context.register(configuration);
            }
        }
    }
    // 还有就是注册 NamedContextFactory 时指定的默认配置
    context.register(PropertyPlaceholderAutoConfiguration.class,
            this.defaultConfigType);
    // 添加名称的属性源(可能我调试的版本有小差异哈,但不影响大体哈)
    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;
}

看注释应该就能理解了哈。

3.5  获取某个类型-getInstance

从指定的名称上下文重获取指定的类型:

public <T> T getInstance(String name, Class<?> clazz, Class<?>... generics) {
    ResolvableType type = ResolvableType.forClassWithGenerics(clazz, generics);
    // 调用重载的方法
    return getInstance(name, type);
}
@SuppressWarnings("unchecked")
public <T> T getInstance(String name, ResolvableType type) {
    // 先获取到 name 的上下文对象
    AnnotationConfigApplicationContext context = getContext(name);
    // 从上下文中获取
    String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
            type);
    if (beanNames.length > 0) {
        // 如果有多个的话,这里应该返回的第一个匹配的
        for (String beanName : beanNames) {
            if (context.isTypeMatch(beanName, type)) {
                return (T) context.getBean(beanName);
            }
        }
    }
    return null;
}
public <T> Map<String, T> getInstances(String name, Class<T> type) {
    // 获取上下文
    AnnotationConfigApplicationContext context = getContext(name);
    return BeanFactoryUtils.beansOfTypeIncludingAncestors(context, type);
}

4  应用

最近在看 OpenFeign 的源码,所以我知道它是用到的,比如 FeignContext 每个客户端@FeignClient,都会创建一个自己的上下文对象进行隔离。

搜了搜好像 Ribbon 负载均衡也用到了,这个等后续看的时候再补充哈。

5  小结

好啦,基本类中的内容看的差不多了,有理解不对的地方欢迎指正哈。

posted @ 2024-01-17 08:00  酷酷-  阅读(137)  评论(0编辑  收藏  举报