客户端配置延迟加载和启用饥饿加载
该博文为Ribbon与Eureka整合分析系列文章中的第四篇,主要介绍客户端所需配置,默认情况下,如何在创建客户端时,才加载配置,以及如何在启动时,加载客户端配置(即饥饿加载)。
一、客户端配置加载
最近在调试和研究Ribbon源码的时候,发现,客户端所需配置,默认情况下,并不是随着容器启动而加载,而是在使用时(请求经过LoadBalancerInterceptor拦截器处理),才进行加载。之所以能达到这个效果,是借助于Spring的AnnotationConfigApplicationContext类。该类的注释信息如下:
Standalone application context, accepting annotated classes as input - in particular {@link Configuration @Configuration}-annotated classes, but also plain {@link org.springframework.stereotype.Component @Component} types and JSR-330 compliant classes using {@code javax.inject} annotations. Allows for registering classes one by one using {@link #register(Class…)} as well as for classpath scanning using {@link #scan(String…)}.
In case of multiple {@code @Configuration} classes, @{@link Bean} methods defined in later classes will override those defined in earlier classes. This can be leveraged to deliberately override certain bean definitions via an extra {@code @Configuration} class.
即通过AnnotationConfigApplicationContext,达到Bean注册效果。另外,通过其构造方法上注释,需要先调用register方法,然后调用refresh方法。
/**
* Create a new AnnotationConfigApplicationContext that needs to be populated
* through {@link #register} calls and then manually {@linkplain #refresh refreshed}.
*/
public AnnotationConfigApplicationContext() {
this.reader = new AnnotatedBeanDefinitionReader(this);
this.scanner = new ClassPathBeanDefinitionScanner(this);
}
如下代码,是Ribbon根据客户端serviceId,创建AnnotationConfigApplicationContext的方法。其中register,都是从configurations属性获取需要注册的内容,且分为两部分,key为客户端serviceId的注册以及以default开头的注册。
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
if (this.configurations.containsKey(name)) {
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);
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;
}
那么,configurations这个属性值,是如何生成的呢?这里涉及到ImportBeanDefinitionRegistrar接口,该接口注释信息如下
Interface to be implemented by types that register additional bean definitions when processing @{@link Configuration} classes. Useful when operating at the bean definition level (as opposed to {@code @Bean} method/instance level) is desired or necessary.
Along with {@code @Configuration} and {@link ImportSelector}, classes of this type may be provided to the @{@link Import} annotation (or may also be returned from an {@code ImportSelector}).
An {@link ImportBeanDefinitionRegistrar} may implement any of the following {@link org.springframework.beans.factory.Aware Aware} interfaces, and their respective methods will be called prior to {@link #registerBeanDefinitions}:
RIbbon针对于该接口,提供RibbonClientConfigurationRegistrar实现类,用于处理RibbonClients以及RibbonClient注解。然后将收集到的数据信息,注册为RibbonClientSpecification bean对象。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata.getAnnotationAttributes(
RibbonClients.class.getName(), true);
if (attrs != null && attrs.containsKey("value")) {
AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
for (AnnotationAttributes client : clients) {
registerClientConfiguration(registry, getClientName(client),
client.get("configuration"));
}
}
if (attrs != null && attrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
} else {
name = "default." + metadata.getClassName();
}
registerClientConfiguration(registry, name,
attrs.get("defaultConfiguration"));
}
Map<String, Object> client = metadata.getAnnotationAttributes(
RibbonClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
private void registerClientConfiguration(BeanDefinitionRegistry registry,
Object name, Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(RibbonClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".RibbonClientSpecification",
builder.getBeanDefinition());
}
在解析这里,基于RibbonClients注解的defaultConfiguration属性,增加default前缀进行标识,其余的通过value(或name属性,对应于客户端serviceId),作为RibbonClientSpecification的name属性。
经过上述分析,默认情况下,创建客户端配置时,会通过RibbonAutoConfiguration和RibbonEurekaAutoConfiguration两个配置类,完成客户端注册操作。
在上述getInstance处,在AnnotationConfigApplicationContext创建完成之后,能根据指定类型,从客户端上下文环境中,获取指定的配置bean。
二、客户端饥饿加载
默认情况下,客户端所需配置,是在第一次请求到来之后,才进行创建。这种情况下,会增加第一次请求处理时长,从而增加请求处理超时的风险。为此,Ribbon增加RibbonEagerLoadProperties配置类,用于指定是否需要在容器启动时加载指定客户端所需配置信息。默认是关闭的。
@ConfigurationProperties(prefix = "ribbon.eager-load")
public class RibbonEagerLoadProperties {
private boolean enabled = false;
private List<String> clients;
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public List<String> getClients() {
return clients;
}
public void setClients(List<String> clients) {
this.clients = clients;
}
}
该配置类,在RibbonAutoConfiguration配置类中使用。当ribbon.eager-load.enabled设置为true,将开启如下配置。而RibbonApplicationContextInitializery为Spring事件监听处理类,用于监听ApplicationReadyEvent事件。
@Bean
@ConditionalOnProperty(value = "ribbon.eager-load.enabled")
public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
return new RibbonApplicationContextInitializer(springClientFactory(),
ribbonEagerLoadProperties.getClients());
}
在接收到ApplicationReadyEvent事件后,调用init方法,先创建客户端所需上下文环境。
protected void initialize() {
if (clientNames != null) {
for (String clientName : clientNames) {
this.springClientFactory.getContext(clientName);
}
}
}
到此,Ribbon的懒加载配置项如下:
ribbon.eager-load.enabled=true
ribbon.eager-load.clients=service_id1,service_id2
总结
客户端配置加载
通过RibbonClientConfigurationRegistrar收集RibbonClients和RibbonClient注解信息,并将其配置为RibbonClientSpecification Bean。然后基于AnnotationConfigApplicationContext,分别针对于客户端,构建其上下文环境信息,然后在获取IRule等配置。
客户端饥饿加载
默认情况下,客户端所需配置,是在接收到请求之后,才会进行创建。这种模式,会增加第一个请求处理时长,从而增加第一个请求处理超时的风险,Ribbon增加饥饿加载配置,通过ribbon.eager-load.enabled=true,开启饥饿加载,同时通过ribbon.eager-load.clients配置项指定,那些客户端需要饥饿加载。