本章,我将讲解与Eureka整合使用时,Spring Cloud Netflix Ribbon的初始化流程。掌握了Spring Cloud Netflix Ribbon的客户端配置的底层原理及其初始化流程,我们再学习Eureka的整合流程其实就很轻松了。读者可以先自己思考下,如果要与Eureka整合,需要对Ribbon做哪些改造?
其实核心就是一点: 如何在Spring Boot应用启动时,创建与Eureka功能相关的Ribbon客户端?
一、初始化流程
我们已经知道,Spring Cloud会为我们创建一个RibbonLoadBalancerClient
对象,作为默认的Ribbon客户端实现类,与此同时会根据RibbonClientConfiguration
创建很多默认的Ribbon核心组件,并与RibbonLoadBalancerClient绑定。
所以,当Ribbon与Eureka结合使用时,Spring Cloud肯定会 创建与Eureka功能相关的Ribbon组件 。
1.1 Spring Boot自动装配
当我们在项目中引入Spring Cloud Netflix Eureka相关的jar依赖(spring-cloud-netflix-eureka-client
)后,应用启动时就会触发Eureka的自动装配:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.DiscoveryClientOptionalArgsConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\
org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\
org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration
org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapConfiguration
上述与Ribbon相关的自动装配类中,最重要的就是RibbonEurekaAutoConfiguration
和LoadBalancerEurekaAutoConfiguration
。
1.2 RibbonEurekaAutoConfiguration
RibbonEurekaAutoConfiguration是Eureka和Ribbon结合使用的”胶水“:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnRibbonAndEurekaEnabled
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = EurekaRibbonClientConfiguration.class)
public class RibbonEurekaAutoConfiguration {
}
RibbonEurekaAutoConfiguration的核心就是@RibbonClients
注解,它会指定全局的Ribbon客户端配置类EurekaRibbonClientConfiguration
,Spring Cloud会利用这个配置类创建一些自定义的Ribbon核心组件,并覆盖掉RibbonAutoConfiguration
中的默认配置。
1.3 EurekaRibbonClientConfiguration
EurekaRibbonClientConfiguration就是自定义了一些Ribbon组件,这些组件可以将Ribbon与Eureka整合起来:
@Configuration(proxyBeanMethods = false)
public class EurekaRibbonClientConfiguration {
@Value("${ribbon.eureka.approximateZoneFromHostname:false}")
private boolean approximateZoneFromHostname = false;
@RibbonClientName
private String serviceId = "client";
@Autowired(required = false)
private EurekaClientConfig clientConfig;
@Autowired(required = false)
private EurekaInstanceConfig eurekaConfig;
@Autowired
private PropertiesFactory propertiesFactory;
public EurekaRibbonClientConfiguration() {
}
public EurekaRibbonClientConfiguration(EurekaClientConfig clientConfig,
String serviceId, EurekaInstanceConfig eurekaConfig,
boolean approximateZoneFromHostname) {
this.clientConfig = clientConfig;
this.serviceId = serviceId;
this.eurekaConfig = eurekaConfig;
this.approximateZoneFromHostname = approximateZoneFromHostname;
}
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, serviceId)) {
return this.propertiesFactory.get(IPing.class, config, serviceId);
}
// NIWSDiscoveryPing会覆盖掉默认的Ribbon客户端配置——ZoneAvoidanceRule
NIWSDiscoveryPing ping = new NIWSDiscoveryPing();
ping.initWithNiwsConfig(config);
return ping;
}
@Bean
@ConditionalOnMissingBean
public ServerList<?> ribbonServerList(IClientConfig config,
Provider<EurekaClient> eurekaClientProvider) {
if (this.propertiesFactory.isSet(ServerList.class, serviceId)) {
return this.propertiesFactory.get(ServerList.class, config, serviceId);
}
// DiscoveryEnabledNIWSServerList会覆盖掉默认的Ribbon客户端配置——ConfigurationBasedServerList
DiscoveryEnabledNIWSServerList discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
@Bean
public ServerIntrospector serverIntrospector() {
// 这里会覆盖掉默认的Ribbon客户端配置——ConfigurationBasedServerList
return new EurekaServerIntrospector();
}
@PostConstruct
public void preprocess() {
String zone = ConfigurationManager.getDeploymentContext()
.getValue(ContextKey.zone);
if (this.clientConfig != null && StringUtils.isEmpty(zone)) {
if (this.approximateZoneFromHostname && this.eurekaConfig != null) {
String approxZone = ZoneUtils
.extractApproximateZone(this.eurekaConfig.getHostName(false));
log.debug("Setting Zone To " + approxZone);
ConfigurationManager.getDeploymentContext().setValue(ContextKey.zone, approxZone);
}
else {
String availabilityZone = this.eurekaConfig == null ? null
: this.eurekaConfig.getMetadataMap().get("zone");
if (availabilityZone == null) {
String[] zones = this.clientConfig
.getAvailabilityZones(this.clientConfig.getRegion());
// Pick the first one from the regions we want to connect to
availabilityZone = zones != null && zones.length > 0 ? zones[0] : null;
}
if (availabilityZone != null) {
ConfigurationManager.getDeploymentContext().setValue(ContextKey.zone,
availabilityZone);
}
}
}
RibbonUtils.initializeRibbonDefaults(serviceId);
}
}
1.4 LoadBalancerEurekaAutoConfiguration
我们再来看下LoadBalancerEurekaAutoConfiguration,它是负责组装Spring Cloud通用的负载均衡组件:
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
@ConditionalOnClass(LoadBalancerClientConfigurationRegistrar.class)
@LoadBalancerClients(defaultConfiguration = EurekaLoadBalancerClientConfiguration.class)
@ConditionalOnProperty(name = "eureka.client.enabled", matchIfMissing = true)
public class LoadBalancerEurekaAutoConfiguration {
public static final String LOADBALANCER_ZONE = "spring.cloud.loadbalancer.zone";
@Bean
@ConditionalOnMissingBean
EurekaLoadBalancerProperties eurekaLoadBalancerProperties() {
return new EurekaLoadBalancerProperties();
}
@Bean
@ConditionalOnMissingBean
LoadBalancerZoneConfig zoneConfig(Environment environment) {
return new LoadBalancerZoneConfig(environment.getProperty(LOADBALANCER_ZONE));
}
}
LoadBalancerEurekaAutoConfiguration的核心是下面这个注解,它声明了一个全局的Ribbon客户端配置类EurekaLoadBalancerClientConfiguration
:
@LoadBalancerClients(defaultConfiguration = EurekaLoadBalancerClientConfiguration.class)
那这个配置类是什么时候注入到Spring IoC容器中的呢?我们看下LoadBalancerClients
注解:
@Configuration(proxyBeanMethods = false)
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(LoadBalancerClientConfigurationRegistrar.class)
public @interface LoadBalancerClients {
LoadBalancerClient[] value() default {};
Class<?>[] defaultConfiguration() default {};
}
通过注解我们可以看到,在Spring Boot应用启动时,会触发LoadBalancerClientConfigurationRegistrar.registerBeanDefinitions()
方法的执行,此时会将EurekaLoadBalancerClientConfiguration
这个默认配置类注入容器。这个思路和@RibbonClients
完全相同,只不过不是特定针对Ribbon这个负载均衡组件而已,Spring Cloud把它通用化了:
public class LoadBalancerClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {
private static void registerClientConfiguration(BeanDefinitionRegistry registry,
Object name, Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder
.genericBeanDefinition(LoadBalancerClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + ".LoadBalancerClientSpecification",
builder.getBeanDefinition());
}
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata
.getAnnotationAttributes(LoadBalancerClients.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"));
}
}
// 将默认配置类注入到Spring容器
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(LoadBalancerClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
}
1.5 EurekaLoadBalancerClientConfiguration
EurekaLoadBalancerClientConfiguration其实就是定义了一些与Eureka相关的负载均衡核心组件的初始化:
@Configuration
@ConditionalOnBean({ LoadBalancerZoneConfig.class, EurekaLoadBalancerProperties.class })
public class EurekaLoadBalancerClientConfiguration {
private final EurekaClientConfig clientConfig;
private final EurekaInstanceConfig eurekaConfig;
private final LoadBalancerZoneConfig zoneConfig;
private final EurekaLoadBalancerProperties eurekaLoadBalancerProperties;
public EurekaLoadBalancerClientConfiguration(
@Autowired(required = false) EurekaClientConfig clientConfig,
@Autowired(required = false) EurekaInstanceConfig eurekaInstanceConfig,
LoadBalancerZoneConfig zoneConfig,
EurekaLoadBalancerProperties eurekaLoadBalancerProperties) {
this.clientConfig = clientConfig;
this.eurekaConfig = eurekaInstanceConfig;
this.zoneConfig = zoneConfig;
this.eurekaLoadBalancerProperties = eurekaLoadBalancerProperties;
}
@PostConstruct
public void postprocess() {
if (!StringUtils.isEmpty(zoneConfig.getZone())) {
return;
}
String zone = getZoneFromEureka();
if (!StringUtils.isEmpty(zone)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Setting the value of '" + LOADBALANCER_ZONE + "' to " + zone);
}
zoneConfig.setZone(zone);
}
}
private String getZoneFromEureka() {
String zone;
boolean approximateZoneFromHostname = eurekaLoadBalancerProperties
.isApproximateZoneFromHostname();
if (approximateZoneFromHostname && eurekaConfig != null) {
return ZoneUtils.extractApproximateZone(this.eurekaConfig.getHostName(false));
}
else {
zone = eurekaConfig == null ? null
: eurekaConfig.getMetadataMap().get("zone");
if (StringUtils.isEmpty(zone)) {
String[] zones = clientConfig
.getAvailabilityZones(clientConfig.getRegion());
// Pick the first one from the regions we want to connect to
zone = zones != null && zones.length > 0 ? zones[0] : null;
}
return zone;
}
}
}
二、总结
本章,我主要讲解了与Eureka整合使用时,Spring Cloud Netflix Ribbon的初始化流程,整体流程还是比较清晰的,就是初始化并注入了一些与Eureka相关的Ribbon核心组件,整个流程我用下面这张图表示:
Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。
它的内容包括:
- 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
- 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
- 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
- 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
- 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
- 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
- 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
- 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw
目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:
想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询
同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。