Ribbon原理分析之NamedContextFactory
在跟踪Ribbon源码的过程中看到了 NamedContextFactory,不懂其存在的精髓,特此记录下。
在SpringCloud中,微服务之间由于系统的不同,可能对于远程调用来说可能需要不同的配置,比如订单系统 A 和库存系统 B,ribbon请求A,B可能需要的连接超时时间重试次数是不一致的,这个时候怎么做到ribbon请求A,B系统
的时候使用不同的配置呢。这就引入了NamedContextFactory。
从注释上来看:
它可以创建一系列的子容器,允许一系列的Specification 在每个子容器中定义自己的bean。可以参见FeignClientFactory,SpringClientFactory。就是从netflix借鉴来的。
Ribbon使用的是SpringClientFactory。
在这个NamedContextFactory 内部还有一个内部接口:Specification
注释的意思是:使用name,configuration区分的规范。
总的来说:NamedContextFactory 维护某个客户端使用的子容器的集合, 配置,获取容器中的bean等。
其实这个内容有点类似nacos的group,Specification用于配置分组,一个Specification实例是一个配置组,按name区分。NamedContextFactory 里面维护一系列的Specification实例中的configuration。
NamedContextFactory 中会根据这些Specification中的配置创建创建一些列的子容器,这个子容器的也按这个name分组。这样调用NamedContextFactory#getInstance的时候是带着name去获取的,就是看要获取
那个子容器内的bean。
我们先看下NamedContextFactory 里面的一些属性。
1:propertySourceName,propertyName 构造函数传递进来赋值,会在每个子容器中生成一条配置propertySourceName=propertyName
2: Map<String, AnnotationConfigApplicationContext> contexts 每个分组创建的子容器,都放在这个map中,key就是分组的name。就是Specification实例中的那个name。
3:Map<String, C> configurations 存储一系列Specification实例,key就是Specification中的name。
4:ApplicationContext parent 每个子容器的父容器,一般都是当前启动的spring项目上下文。
4:defaultConfigType 每个子容器都会使用的一个配置类。默认配置类。
1:构造函数。
第一个参数:是默认的配置类,这个配置类,是个公共的,所有的子容器都会加载它,在下面createContext方法中可以看到。
第二个参数,第三个参数:也是公共的,在每个子容器中增加一条配置属性:propertySourceName=propertyName 在下面createContext方法中可以看到。
2:设置所有子容器的父容器,在创建子容器的时候,如果其不为空都会把这个parent设置为自己的父容器。一般这个parent都是当前的spring项目。
3:这个是用来把创建的一系列Specification实例传递进来,用里面的configuration用来创建子容器的。
4: 获取所有子容器的分组name。
5:获取某个子容器,可以被重写。
6:创建子容器
protected AnnotationConfigApplicationContext createContext(String name) { AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); if (this.configurations.containsKey(name)) {
//this.configurations.get(name) 得到的就是对应的Specification实例
for (Class<?> configuration : this.configurations.get(name) .getConfiguration()) {
// 输入名称为name的子容器的配置类 context.register(configuration); } } for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
// 自定义的Specification实例也可以命名为default.开头,这样每个子容器也会使用注入。 if (entry.getKey().startsWith("default.")) { for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } }
// 子容器使用的默认配置类。 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
// 子容器可以获取父容器的bean
context.setParent(this.parent);
// 解决java 11的问题 // 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; }
7:根据name获取一个实例,先根据name获取某个子容器然后再获取bean。
实践
我们自定义一套Client,和Specification来感受下这个机制。
作为客户端 public class MytestClientNamedContextFactory extends NamedContextFactory<MytestClientSpecification> { public MytestClientNamedContextFactory() { super(BeanBaseConfig.class, "test", "myTest"); } } Specification实例 public class MytestClientSpecification implements NamedContextFactory.Specification { private String name; private Class<?>[] configuration; public MytestClientSpecification(){ } public MytestClientSpecification(String name, Class<?>[] configuration){ this.name=name; this.configuration=configuration; } @Override public String getName() { return name; } @Override public Class<?>[] getConfiguration() { return configuration; } } 默认配置类 @Configuration public class BeanBaseConfig { @Bean public TestBean testBeanCommon(){ TestBean testBean = new TestBean(); testBean.setBeanName("byBeanBaseConfig1"); return testBean; } } @Configuration public class BeanBaseConfig1 { @Bean public TestBean testBean(){ TestBean testBean = new TestBean(); testBean.setBeanName("byBeanBaseConfig1"); return testBean; } } @Configuration public class BeanBaseConfig2 { @Bean public TestBean testBean1(){ TestBean testBean = new TestBean(); testBean.setBeanName("byBeanBaseConfig1"); return testBean; } } public class TestBean { private String beanName="testBean"; public String getBeanName() { return beanName; } public void setBeanName(String beanName) { this.beanName = beanName; } }
测试:
@SpringBootTest public class SpringRunTest { @Test public void test(){ AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); parent.register(BeanBaseConfig.class); parent.refresh(); // 声明一个客户端 使用默认的配置 BeanBaseConfig MytestClientNamedContextFactory testClient=new MytestClientNamedContextFactory(); // 声明客户端的 specification 其实这个specification 相当于一个nacos group 把配置分了组 // 比如在ribbon client中 可能需要请求订单微服务和库存微服务的配置不一样 就需要到它 MytestClientSpecification mytestClientSpecification1 = new MytestClientSpecification("specification1",new Class[]{BeanBaseConfig1.class}); MytestClientSpecification mytestClientSpecification2 = new MytestClientSpecification("specification2",new Class[]{BeanBaseConfig1.class, BeanBaseConfig2.class}); testClient.setConfigurations(Arrays.asList(mytestClientSpecification1,mytestClientSpecification2)); testClient.setApplicationContext(parent); System.out.println(testClient.getInstances("specification1", TestBean.class)); System.out.println("====================================="); System.out.println(testClient.getInstances("specification2", TestBean.class)); } }
结果如下:
{testBean=com.tuling.mall.user.TestBean@11c9af63, testBeanCommon=com.tuling.mall.user.TestBean@757acd7b} ===================================== {testBean=com.tuling.mall.user.TestBean@55f616cf, testBean1=com.tuling.mall.user.TestBean@1356d4d4, testBeanCommon=com.tuling.mall.user.TestBean@c03cf28}
可以看到通过Specification实例可以起到子容器分离的效果。
Ribbon也是用这个机制。后面分析Ribbon源码的时候再提。