spring-cloud-commons 中参考了 spring-cloud-netflix 的设计,引入了 NamedContextFactory 机制,一般用于对于不同微服务的客户端模块使用不同的 子 ApplicationContext 进行配置。

spring-cloud-commons 是 Spring Cloud 对于微服务基础组件的抽象。在一个微服务中,调用微服务 A 与调用微服务 B 的配置可能不同。比较简单的例子就是,A 微服务是一个简单的用户订单查询服务,接口返回速度很快,B 是一个报表微服务,接口返回速度比较慢。这样的话我们就不能对于调用微服务 A 和微服务 B 使用相同的超时时间配置。还有就是,我们可能对于服务 A 通过注册中心进行发现,对于服务 B 则是通过 DNS 解析进行服务发现,所以对于不同的微服务我们可能使用不同的组件,在 Spring 中就是使用不同类型的 Bean。

在这种需求下,不同微服务的客户端有不同的以及相同的配置有不同的 Bean,也有相同的 Bean。所以,我们可以针对每一个微服务将他们的 Bean 所处于 ApplicationContext 独立开来,不同微服务客户端使用不同的 ApplicationContext。NamedContextFactory 就是用来实现这种机制的。

通过实例了解 NamedContextFactory 的使用

编写源码:

package com.github.hashjang.spring.cloud.iiford.service.common;

import org.junit.Assert;
import org.junit.Test;
import org.springframework.cloud.context.named.NamedContextFactory;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment; import java.util.List;
import java.util.Objects; public class CommonNameContextTest { private static final String PROPERTY_NAME = "test.context.name"; @Test
public void test() {
//创建 parent context
AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext();
//添加 BaseConfig 相关配置
parent.register(BaseConfig.class);
//初始化 parent
parent.refresh();
//创建 testClient1,默认配置使用 ClientCommonConfig
TestClient testClient1 = new TestClient(ClientCommonConfig.class);
//创建 service1 与 service2 以及指定对应额外的配置类
TestSpec testSpec1 = new TestSpec("service1", new Class[]{Service1Config1.class, Service1Config2.class});
TestSpec testSpec2 = new TestSpec("service2", new Class[]{Service2Config.class});
//设置 parent ApplicationContext 为 parent
testClient1.setApplicationContext(parent);
//将 service1 与 service2 的配置加入 testClient1
testClient1.setConfigurations(List.of(testSpec1, testSpec2));
BaseBean baseBean = testClient1.getInstance("service1", BaseBean.class);
System.out.println(baseBean);
//验证正常获取到了 baseBean
Assert.assertNotNull(baseBean);
ClientCommonBean commonBean = testClient1.getInstance("service1", ClientCommonBean.class);
System.out.println(commonBean);
//验证正常获取到了 commonBean
Assert.assertNotNull(commonBean);
Service1Bean1 service1Bean1 = testClient1.getInstance("service1", Service1Bean1.class);
System.out.println(service1Bean1);
//验证正常获取到了 service1Bean1
Assert.assertNotNull(service1Bean1);
Service1Bean2 service1Bean2 = testClient1.getInstance("service1", Service1Bean2.class);
System.out.println(service1Bean2);
//验证正常获取到了 service1Bean2
Assert.assertNotNull(service1Bean2);
BaseBean baseBean2 = testClient1.getInstance("service2", BaseBean.class);
System.out.println(baseBean2);
//验证正常获取到了 baseBean2 并且 baseBean2 就是 baseBean
Assert.assertEquals(baseBean, baseBean2);
ClientCommonBean commonBean2 = testClient1.getInstance("service2", ClientCommonBean.class);
System.out.println(commonBean2);
//验证正常获取到了 commonBean2 并且 commonBean 和 commonBean2 不是同一个
Assert.assertNotNull(commonBean2);
Assert.assertNotEquals(commonBean, commonBean2);
Service2Bean service2Bean = testClient1.getInstance("service2", Service2Bean.class);
System.out.println(service2Bean);
//验证正常获取到了 service2Bean
Assert.assertNotNull(service2Bean);
} @Configuration(proxyBeanMethods = false)
static class BaseConfig {
@Bean
BaseBean baseBean() {
return new BaseBean();
}
} static class BaseBean {} @Configuration(proxyBeanMethods = false)
static class ClientCommonConfig {
@Bean
ClientCommonBean clientCommonBean(Environment environment, BaseBean baseBean) {
//在创建 NamedContextFactory 里面的子 ApplicationContext 的时候,会指定 name,这个 name 对应的属性 key 即 PROPERTY_NAME
return new ClientCommonBean(environment.getProperty(PROPERTY_NAME), baseBean);
}
} static class ClientCommonBean {
private final String name;
private final BaseBean baseBean; ClientCommonBean(String name, BaseBean baseBean) {
this.name = name;
this.baseBean = baseBean;
} @Override
public String toString() {
return "ClientCommonBean{" +
"name='" + name + '\'' +
", baseBean=" + baseBean +
'}';
}
} @Configuration(proxyBeanMethods = false)
static class Service1Config1 {
@Bean
Service1Bean1 service1Bean1(ClientCommonBean clientCommonBean) {
return new Service1Bean1(clientCommonBean);
}
} static class Service1Bean1 {
private final ClientCommonBean clientCommonBean; Service1Bean1(ClientCommonBean clientCommonBean) {
this.clientCommonBean = clientCommonBean;
} @Override
public String toString() {
return "Service1Bean1{" +
"clientCommonBean=" + clientCommonBean +
'}';
}
} @Configuration(proxyBeanMethods = false)
static class Service1Config2 {
@Bean
Service1Bean2 service1Bean2() {
return new Service1Bean2();
}
} static class Service1Bean2 {
} @Configuration(proxyBeanMethods = false)
static class Service2Config {
@Bean
Service2Bean service2Bean(ClientCommonBean clientCommonBean) {
return new Service2Bean(clientCommonBean);
}
} static class Service2Bean {
private final ClientCommonBean clientCommonBean; Service2Bean(ClientCommonBean clientCommonBean) {
this.clientCommonBean = clientCommonBean;
} @Override
public String toString() {
return "Service2Bean{" +
"clientCommonBean=" + clientCommonBean +
'}';
}
} static class TestSpec implements NamedContextFactory.Specification {
private final String name;
private final Class<?>[] configurations; public TestSpec(String name, Class<?>[] configurations) {
this.name = name;
this.configurations = configurations;
} @Override
public String getName() {
return name;
} @Override
public Class<?>[] getConfiguration() {
return configurations;
}
} static class TestClient extends NamedContextFactory<TestSpec> { public TestClient(Class<?> defaultConfigType) {
super(defaultConfigType, "testClient", PROPERTY_NAME);
}
}
}

结果输出为:

com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$BaseBean@3faf2e7d
ClientCommonBean{name='service1', baseBean=com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$BaseBean@3faf2e7d}
Service1Bean1{clientCommonBean=ClientCommonBean{name='service1', baseBean=com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$BaseBean@3faf2e7d}}
com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$Service1Bean2@4648ce9
com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$BaseBean@3faf2e7d
ClientCommonBean{name='service2', baseBean=com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$BaseBean@3faf2e7d}
Service2Bean{clientCommonBean=ClientCommonBean{name='service2', baseBean=com.github.hashjang.spring.cloud.iiford.service.common.CommonNameContextTest$BaseBean@3faf2e7d}}

代码中实现了这样一个 Context 结构:

图中的被包含的 ApplicationContext 可以看到外层 ApplicationContext 的 Bean,也就是通过对被包含的 ApplicationContext 调用 getBean(xxx) 可以获取到外层 ApplicationContext 的 Bean (其实外层就是 parent ApplicationContext),但是外层的看不到内层私有的 Bean。

在我们的测试代码中,首先,创建了一个 AnnotationConfigApplicationContext。这个其实就是模拟了我们平常使用 Spring 框架的时候的根核心 ApplicationContext,所以我们将其命名为 parent。我们向里面注册了 BaseConfigBaseConfig 里面的 BaseBean 会注册到 parent。之后我们 建 testClient1,默认配置使用 ClientCommonConfig。如果我们指定了 testClient1 的 parent ApplicationContext 为 parent,那么 parent 里面的 Bean 都能被 testClient1 里面的子 ApplicationContext 访问到。然后,我们创建 service1 与 service2 以及指定对应额外的配置类。service1 会创建 ClientCommonConfigService1Config1Service1Config2 里面配置的 Bean。service2 会创建 ClientCommonConfigService2Config 里面配置的 Bean。

NamedContextFactory 的基本原理以及源码

NamedContextFactory 的核心方法是 public <T> T getInstance(String name, Class<T> type),通过这个方法获取 NamedContextFactory 里面的子 ApplicationContext 里面的 Bean。源码是:

NamedContextFactory.java

/**
* 获取某个 name 的 ApplicationContext 里面的某个类型的 Bean
* @param name 子 ApplicationContext 名称
* @param type 类型
* @param <T> Bean 类型
* @return Bean
*/
public <T> T getInstance(String name, Class<T> type) {
//获取或者创建对应名称的 ApplicationContext
AnnotationConfigApplicationContext context = getContext(name);
try {
//从对应的 ApplicationContext 获取 Bean,如果不存在则会抛出 NoSuchBeanDefinitionException
return context.getBean(type);
}
catch (NoSuchBeanDefinitionException e) {
//忽略 NoSuchBeanDefinitionException
}
//没找到就返回 null
return null;
} protected AnnotationConfigApplicationContext getContext(String name) {
//如果 map 中不存在,则创建
if (!this.contexts.containsKey(name)) {
//防止并发创建多个
synchronized (this.contexts) {
//再次判断,防止有多个等待锁
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
} //根据名称创建对应的 context
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//如果 configurations 中有对应名称的配置类,则注册之
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
context.register(configuration);
}
}
//如果 configurations 中有名称开头为 default. 的配置类,则注册之
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
//注册 PropertyPlaceholderAutoConfiguration,这样可以解析 spring boot 相关的 application 配置
//注册默认的配置类 defaultConfigType
context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
//将当前 context 的名称,放入对应的属性中,在配置类中可能会用到
//我们上面举得例子,就是通过 environment.getProperty() 获取了这个属性
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);
//spring boot 可以打包成一种 fatjar 的形式,将依赖的 jar 包都打入同一个 jar 包中
//fatjar 中的依赖,通过默认的类加载器是加载不正确的,需要通过定制的类加载器
//由于 JDK 11 LTS 相对于 JDK 8 LTS 多了模块化,通过 ClassUtils.getDefaultClassLoader() 有所不同
//在 JDK 8 中获取的就是定制的类加载器,JDK 11 中获取的是默认的类加载器,这样会有问题
//所以,这里需要手动设置当前 context 的类加载器为父 context 的类加载器
context.setClassLoader(this.parent.getClassLoader());
}
//生成展示名称
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}

Spring Cloud 升级之路 - 2020.0.x - 5. 理解 NamedContextFactory的更多相关文章

  1. Spring Cloud 升级之路 - 2020.0.x - 1. 背景知识、需求描述与公共依赖

    1. 背景知识.需求描述与公共依赖 1.1. 背景知识 & 需求描述 Spring Cloud 官方文档说了,它是一个完整的微服务体系,用户可以通过使用 Spring Cloud 快速搭建一个 ...

  2. Spring Cloud 升级之路 - 2020.0.x - 4. 使用 Eureka 作为注册中心

    Eureka 目前的状态:Eureka 目前 1.x 版本还在更新,但是应该不会更新新的功能了,只是对现有功能进行维护,升级并兼容所需的依赖. Eureka 2.x 已经胎死腹中了.但是,这也不代表 ...

  3. Spring Cloud 升级之路 - 2020.0.x - 3. Undertow 的 accesslog 配置

    上一节我们讲述了如何使用 Undertow 作为我们的 Web 服务容器,本小节我们来分析使用 Undertow 的另一个问题,也就是如何配置 accesslog,以及 accesslog 的各种占位 ...

  4. Spring Cloud 升级之路 - 2020.0.x - 6. 使用 Spring Cloud LoadBalancer (1)

    本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...

  5. Spring Cloud 升级之路 - 2020.0.x - 7. 使用 Spring Cloud LoadBalancer (2)

    本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...

  6. Spring Cloud 升级之路 - 2020.0.x - 2. 使用 Undertow 作为我们的 Web 服务容器

    本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 在我们的项目中,我 ...

  7. SpringCloud升级之路2020.0.x版-9.如何理解并定制一个Spring Cloud组件

    本系列为之前系列的整理重启版,随着项目的发展以及项目中的使用,之前系列里面很多东西发生了变化,并且还有一些东西之前系列并没有提到,所以重启这个系列重新整理下,欢迎各位留言交流,谢谢!~ 我们实现的 S ...

  8. SpringCloud升级之路2020.0.x版-21.Spring Cloud LoadBalancer简介

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...

  9. SpringCloud升级之路2020.0.x版-23.订制Spring Cloud LoadBalancer

    本系列代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spri ...

随机推荐

  1. JS基础学习第一天

    JavaScript JavaScript负责页面中的的行为. 它是一门运行在浏览器端的脚本语言. JS的编写的位置 1.可以编写到标签的指定属性中 12 <button onclick=&qu ...

  2. 【Java】7.0 进制转换

    [二进制转十进制] public static void main(String args[]) { Scanner sc = new Scanner(System.in); System.out.p ...

  3. OOJML系列总结

    目录 0x0 JML理论相关 0.0 概念及作用 0.1 JML语法学习 0x1 使用openJml以及JMLUnitNG 1.0 使用openjml 1.1使用JMLUnitNG 0x2 作业架构设 ...

  4. Spring(七)SpringMVC的文件上传

    1-SpringMVC的请求-文件上传-客户端表单实现(应用) 表单项type="file" 表单的提交方式是post 表单的enctype属性是多部分表单形式,及enctype= ...

  5. 树结构系列(四):MongoDb 使用的到底是 B 树,还是 B+ 树?

    文章首发于「陈树义」公众号及个人博客 shuyi.tech 文章首发于「陈树义」公众号及个人博客 shuyi.tech,欢迎访问更多有趣有价值的文章. 关于 B 树与 B+ 树,网上有一个比较经典的问 ...

  6. 【笔记】《Redis设计与实现》chapter20 Lua脚本

    chapter20 Lua脚本 Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令 20.1 ...

  7. matlab文件管理

    当前文价夹浏览器以及路径管理器   在主页面左侧有单独的窗口进行显示,可以显示当前目录下的文件并提供文件搜索功能. 搜索路径 搜索先后步骤 输入字符串"polyfit" (1)检查 ...

  8. Day16_94_IO_读取文件字节流read()方法(三)

    读取文件字节流read()方法(三) int read(byte[] bytes) 返回值为int类型, 该int类型数据表示每一次读取到的有效字节数,也就是读取到了几个字节, 一个都没读取到返回-1 ...

  9. BLUENRG-LP 学习笔记

    在学习 BLUENRG-LP 的过程中,及时地把碰到的问题记录下来,并作解答,形成本文档. 该文档会时时更新,并且当某些章节内容过多时,会独立出来,形成新的文章. BLUENRG-LP 的特性 芯片内 ...

  10. 『动善时』JMeter基础 — 2、JMeter的安装和启动

    1.安装Java环境 由于JMeter是纯Java的桌面应用程序,因此它的运行环境需要Java环境,即需要安装JDK或JRE.(也就是安装JDK环境) 步骤简要说明: 下载并安装JDK 配置环境变量 ...