最近在閱讀 Ribbon 的源碼禀晓,發(fā)現(xiàn) SpringCloud 中 NamedContextFactory 這個類可以實現(xiàn)子容器。Ribbon 為每個 ServiceName 都擁有自己的 Spring Context 和 Bean 實例(不同服務之間的 LoadBalancer 和其依賴的 Bean 都是完全隔離的)击纬。
這么做有什么好處呢
- 子容器之間數(shù)據(jù)隔離栅表。不同的 LoadBalancer 只管理自己的服務實例谓晌,明確自己的職責笤妙。
- 子容器之間配置隔離硅则。不同的 LoadBalancer 可以使用不同的配置淹父。例如報表服務需要統(tǒng)計和查詢大量數(shù)據(jù),響應時間可能很慢怎虫。而會員服務邏輯相對簡單暑认,所以兩個服務的響應超時時間可能要求不同。
- 子容器之間 Bean 隔離大审≌杭剩可以讓子容器之間注冊不同的 Bean。例如訂單服務的 LoadBalancer 底層通過 Nacos 獲取實例徒扶,會員服務的 LoadBalancer 底層通過 Eureka 獲取實例粮彤。也可以讓不同的 LoadBalancer 采用不同的算法
NamedContextFactory 簡介
NamedContextFactory 可以創(chuàng)建一個子容器(或者說子上下文),每個子容器可以通過
Specification
定義 Bean姜骡。 移植自 spring-cloud-netflix FeignClientFactory 和 SpringClientFactory
上面是對于 NamedContextFactory 類注釋的翻譯导坟。
接下來,我會使用 NamedContextFactory 實現(xiàn)一個 demo圈澈,便于各位理解惫周。
我實在沒想到什么特別好的場景。所以我仿照 Ribbon 實現(xiàn)一個 HttpClient康栈,每個子容器的 HttpClient 生效不同的配置闯两,創(chuàng)建不同的 Bean
子容器的定制
NamedContextFactory
下面來進入正題
子容器需要通過 NamedContextFactory 來創(chuàng)建。首先我們先繼承一下該類實現(xiàn)一個自己的 Factory
@Component
public class NamedHttpClientFactory extends NamedContextFactory<NamedHttpClientSpec> {
public NamedHttpClientFactory() {
super(NamedHttpClientConfiguration.class, "namedHttpClient", "http.client.name");
}
}
解釋一下上述 super 構造方法三個參數(shù)的含義
- 第一個參數(shù)谅将,默認配置類。當使用 NamedHttpClientFactory 創(chuàng)建子容器時重慢,NamedHttpClientConfiguration 一定會被加載
- 第二個參數(shù)饥臂,我目前沒發(fā)現(xiàn)有什么用,真的就是隨便定義一個 name
- 第三個參數(shù),很重要。
創(chuàng)建子容器時通常會提供子容器的容器 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舟扎,負責根據(jù)容器 name分飞,加載屬于自己的配置。代碼比較簡單就不貼出來了
- NamedHttpClient睹限,簡單的包裝一下 HttpClient譬猫,會根據(jù) 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;
}
}
我們簡單的實現(xiàn)一下該接口染服,然后通過 NamedHttpClientFactory#setConfigurations
,將 Specification 賦值給 NamedHttpClientFactory顺囊。
創(chuàng)建子容器時肌索,如果容器的 name 匹配了 Specification 的 name,則會加載 Specification 對應 Configuration 類特碳。
題外話: @RibbonClient 也是通過 NamedContextFactory.Specification 實現(xiàn)的
Run 一下
講到這诚亚,也許你還是沒懂,沒關系午乓,建議 Debug 一下這個單元測試
public class NamedContextFactoryTest {
private void initEnv(AnnotationConfigApplicationContext parent) {
Map<String, Object> 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() {
// 創(chuàng)建 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));
// 準備工作完成益愈,現(xiàn)在開始通過 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 {
}
}
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> T getInstance(String name, Class<T> type) {
// 1. 獲取子容器
AnnotationConfigApplicationContext context = getContext(name);
try {
// 2. 從子容器中獲取 Bean
return context.getBean(type);
}
catch (NoSuchBeanDefinitionException e) {
// ignore
}
return null;
}
這個方法內部邏輯很簡單
- 獲取子容器(如果不存在的話,會創(chuàng)建)
- 從(子)容器中獲取 Bean摸袁,這步就可理解為和常規(guī) Spring 操作一樣了钥顽,從容器中獲取 Bean(子容器只是概念上的一個東西,實際 API 都是一樣的)
所以下面我們重點看下 getContext 方法做了什么
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private Map<String, C> 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<String, C> 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.<String, Object>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 的實現(xiàn)(用于提供 Configuration)靠汁,name 為容器名蜂大,用于定制每個子容器的配置
- 如果 name 匹配,則加載 Configuration
- 如果 Specification 的 name 以
default.
開頭蝶怔,則每個子容器創(chuàng)建時奶浦,都會加載這些配置 - 子容器中注冊 PropertyPlaceholderAutoConfiguration,以及 defaultConfigType(PropertyPlaceholderAutoConfiguration 用于解析 Bean 或者 @Value 中的占位符踢星。defaultConfigType 是上文中提到過的澳叉,構造方法中提供的子容器默認配置類)
- 也是我們上文中說過的,子容器中寫入一條配置。以上文為例耳高,會在容器中寫入一條
http.client.name=容器name
- 剩下的就是扎瓶,設置父容器,以及初始化操作
最后
如果覺得我的文章對你有幫助泌枪,動動小手點下關注或者喜歡概荷,你的支持是對我最大的幫助