系列文章:
Spring Cloud LoadBalancer之负载均衡简介
Spring Cloud NamedContextFactory 原理分析
Ribbon 的替代品 Spring Cloud Loadbalancer 使用与原理分析
测试代码地址
1.1 NamedContextFactory 作用
ribbon 与 loadbalancer 都能做到对不同的调用服务使用不同的负载均衡配置。
例如:数据服务需要查询的时间较长,用户中心服务查询用户信息的时间较短,二者可以采用不同的负载策略。
要实现上述要求,可以使用 Spring Cloud 提供的 NamedContextFactory 实现;他的作用就是根据名称去存储不同的 ApplicationContext,这样就能实现不同的名称的 Bean 隔离了。
由于NamedContextFactory 类有一个字段(parent)保存了最外层的 Bean 上下文,因此可以获取到父级传过来的上下文内容。
以下图为例,在同一个 NamedContextFactory 中,parent有一个 paren bean,两个 children都有各自的 child bean,children 上下文是能够访问到父上下文的,并且访问到的父 bean 都是同一个类实例。
以下代码是校验从父容器获取到的 parentBean 是否为同一个,这个测试用例是能通过的:
@Test
public void context() {
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
parent.register(ParentConfig.class);
parent.refresh();
CustomNamedContextFactoryClient client = new CustomNamedContextFactoryClient(DefaultConfig.class);
// 子容器配置
CustomSpecification huaweiSpecification = new CustomSpecification("huawei", new Class[]{HuaweiConfig.class});
CustomSpecification appleSpecification = new CustomSpecification("apple", new Class[]{AppleConfig.class});
// 添加子容器到父容器中
client.setApplicationContext(parent);
client.setConfigurations(List.of(huaweiSpecification, appleSpecification));
// 父容器的 bean
ParentBean parentBean = parent.getBean(ParentBean.class);
// 根据名称获取子容器
ParentBean huaweiParentBean = client.getInstance("huawei", ParentBean.class);
// huawei 子容器能够获取父容器中的 bean
System.out.println(huaweiParentBean.getName());
// 根据名称获取子容器
ParentBean appleParentBean = client.getInstance("apple", ParentBean.class);
// apple 子容器能够获取父容器中的 bean
System.out.println(appleParentBean.getName());
// 获取到父容器的 bean 是否相同
Assertions.assertEquals(huaweiParentBean, appleParentBean);
}
1.2 NamedContextFactory 原理分析
NamedContextFactory 的构造方法有 3 个入参,实现类是必须要传过来的:
/**
* 构造方法
*
* @param defaultConfigType 默认配置类型,所有的子容器都会存在该默认配置
* @param propertySourceName 配置名称,暂未发现有什么用
* @param propertyName 在子容器使用该名称注入一个 String 就能获取子容器的名称;例如 子容器名称为 huawei,propertyName 定义为
phone.name,那么可以在子容器使用 @Value("${phone.name}") 注入 huawei 字符串
*/
public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName, String propertyName) {
this.defaultConfigType = defaultConfigType;
this.propertySourceName = propertySourceName;
this.propertyName = propertyName;
}
从 NamedContextFactory 中根据子容器名称与获取一个实例,不存在则新建:
/**
* 获取一个实例
*
* @param name 子容器名称
* @param type 实例的类型
* @param
* @return
*/
public <T> T getInstance(String name, Class<T> type) {
// 根据名称获取子容器
AnnotationConfigApplicationContext context = getContext(name);
try {
// 与普通的 ApplicationContext 一样,可以通过 getBean 方法获取实例
return context.getBean(type);
} catch (NoSuchBeanDefinitionException e) {
// ignore
}
return null;
}
getContext(String) 根据子容器名称获取上下文的方法,如下文已存在则直接返回,否则新建一个 AnnotationConfigApplicationContext 上下文:
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
protected AnnotationConfigApplicationContext getContext(String name) {
// 双重检查锁防止在多线程环境下重复创建上下文(单例模式)。
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
createContext(String) 作用是新建一个 name 对应的上下文,具体分析看代码中的注释:
/**
* NamedContextFactory.Specification 的实现类,保存子容器的配置类;
* 可以使用 client.setConfigurations(List.of(huaweiSpecification, appleSpecification)); 设置
*/
private Map<String, C> configurations = new ConcurrentHashMap<>();
/**
* 默认配置类型,所有的子容器都会存在该默认配置
*/
private Class<?> defaultConfigType;
/**
* 配置名称,暂未发现有什么用
*/
private final String propertySourceName;
/**
* 在子容器使用该名称注入一个 String 就能获取子容器的名称;例如 子容器名称为 huawei,propertyName 定义为
* phone.name,那么可以在子容器使用 @Value("${phone.name}") 注入 huawei 字符串
*/
private final String propertyName;
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 如果 configurations 中有对应的 name 配置,则将设置的配置类注册到子容器中
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
context.register(configuration);
}
}
// 如果 configurations 的 key 包含有 default. 开头则表示默认配置,所有的子容器都应该注册 bean
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// 注册配置文件自动配置类与前面构造方法的默认配置类
context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
// 构造方法的 propertyName 在这里会设置上对应的值,值为传过来的 name (子容器名称)
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;
}
总结:
从源码来看,NamedContextFactory 实现子容器隔离的功能并不复杂,说白了就是拿 Map 将容器名与其对应的 ApplicationContext 上下文存起来而已,但是在负载均衡中却能够有很多妙用,比如 ribbon 可以通过 子容器名.socketTimeout=1000 这样设置对应容器的超时时间等等。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)