我们在编程的时候,经常是面向接口编程的。假设有这样的一个需求,在一个 Spring 应用中,我们有一个接口,在不同的场景下,使用不同的实现类去执行逻辑,如何可以办到呢?常规情况下,我们可以使用 @Bean(name = “名称”)或者@Bean(“名称”),给 bean 指定一个名称,然后再使用 @Qualifier 来配合 @Autowired 注入指定名称的类即可。
假设有如下场景:
我们有10个场景,每个场景对应10个接口,每个场景对应的实现类都有可能不一样,这样怎么办呢?
Spring Cloud 提供了 NamedContextFactory 来解决这样的问题。正如其名字,是一个 Spring 容器上下文的创建工厂,是命名式的。是从 spring-cloud-netflix 引进来的。
首先看下类的定义:
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
}
这是一个抽象类,看到 ApplicationContextAware 就知道,这个注入了父容器的 ApplicationContext 。那这个类怎么使用呢?其常用的方法如下,其中可以从 T getInstance 开始看起:
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
implements DisposableBean, ApplicationContextAware {
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
private Map<String, C> configurations = new ConcurrentHashMap<>();
// 以追加的形式配置类
public void setConfigurations(List<C> configurations) {
for (C client : configurations) {
this.configurations.put(client.getName(), client);
}
}
// 常用的入口方法,其中 name 为容器的名字, type 为需要返回的实例类型
public <T> T getInstance(String name, Class<T> type) {
AnnotationConfigApplicationContext context = getContext(name);
try {
return context.getBean(type);
} catch (NoSuchBeanDefinitionException e) {
// ignore
}
return null;
}
// 实际获取上下文的方法,是根据名字去获取的
protected AnnotationConfigApplicationContext getContext(String name) {
// contexts 是一个 map 容器,用来缓存已经生成的容器,可以看到这是一个懒加载的过程,只有第一次用到,才去生成。
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
// 核心在于 createContext 这个方法
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
// 实际创建上下文的方法,可以看到这个是返回一个 AnnotationConfigApplicationContext 上下文。
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context;
if (this.parent != null) {
DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
if (parent instanceof ConfigurableApplicationContext) {
beanFactory.setBeanClassLoader(
((ConfigurableApplicationContext) parent).getBeanFactory().getBeanClassLoader());
}
else {
beanFactory.setBeanClassLoader(parent.getClassLoader());
}
context = new AnnotationConfigApplicationContext(beanFactory);
context.setClassLoader(this.parent.getClassLoader());
}
else {
context = new AnnotationConfigApplicationContext();
}
// 以上代码,new 了一个 AnnotationConfigApplicationContext。如果有父上下文,则设置相同的类加载器,
if (this.configurations.containsKey(name)) {
// 这里是获取配置的关键, configurations 是一个全局的配置,包含所有命名的配置类,每个配置都是NamedContextFactory.Specification的实现了,里面可以包含多个配置类,遍历所有的配置类,注册到对应的容器中即可。
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.")) {
// 默认配置,所有的容器都同意注册
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// 注册默认配置项
context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
// 将当前的容器的名字,注册到上下文中。方便当前上下文的 bean,可以通过 getInstance 方法获取相同容器中的bean
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);
}
context.setDisplayName(generateDisplayName(name));
context.refresh();
return context;
}
public interface Specification {
String getName();
Class<?>[] getConfiguration();
}
}
总体而言,NamedContextFactory 并不复杂,通过创建 Spring 子容器,可以获取独立的上下文,每个上下文的类是隔离的。应用场景比如open-feign中,每一个接口都对应一个子容器,可以拥有不同的配置。这篇文章同样是 Spring Cloud Loadbalancer 模块学习的前置文章,对学习 Spring Cloud Loadbalancer 有帮助。