【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 小结
好啦,基本类中的内容看的差不多了,有理解不对的地方欢迎指正哈。