最近在阅读 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 {
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.
}
}
-
@Value("${http.client.name}")
,结合上边讲的,这样可以读到当前子容器的 name - ClientConfig,负责根据容器 name,加载属于自己的配置。代码比较简单就不贴出来了
- 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 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 getInstance(String name, Class type) {
// 1. 获取子容器
AnnotationConfigApplicationContext context = getContext(name);
try {
// 2. 从子容器中获取 Bean
return context.getBean(type);
}
catch (NoSuchBeanDefinitionException e) {
// ignore
}
return null;
}
这个方法内部逻辑很简单
- 获取子容器(如果不存在的话,会创建)
- 从(子)容器中获取 Bean,这步就可理解为和常规 Spring 操作一样了,从容器中获取 Bean(子容器只是概念上的一个东西,实际 API 都是一样的)
所以下面我们重点看下 getContext 方法做了什么
public abstract class NamedContextFactory
implements DisposableBean, ApplicationContextAware {
private Map 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 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.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;
}
// 省略其他方法
}
- 该 Map 的 value 为 Specification 的实现(用于提供 Configuration),name 为容器名,用于定制每个子容器的配置
- 如果 name 匹配,则加载 Configuration
- 如果 Specification 的 name 以
default.
开头,则每个子容器创建时,都会加载这些配置 - 子容器中注册 PropertyPlaceholderAutoConfiguration,以及 defaultConfigType(PropertyPlaceholderAutoConfiguration 用于解析 Bean 或者 @Value 中的占位符。defaultConfigType 是上文中提到过的,构造方法中提供的子容器默认配置类)
- 也是我们上文中说过的,子容器中写入一条配置。以上文为例,会在容器中写入一条
http.client.name=容器name
- 剩下的就是,设置父容器,以及初始化操作
最后
如果觉得我的文章对你有帮助,动动小手点下关注或者喜欢,你的支持是对我最大的帮助