使用Spring Boot及Spring Cloud全家桶,Eureka,Feign,Ribbon通常是必选套餐。在咱们无脑使用了一段时间后,发现有的配置方式和预期不符,因而便进行了一番研究。本文将介绍Ribbon和Feign一些重要而不常据说的细节。java
在阅读本文以前,你须要了解Spring Boot自动配置的原理。能够参考我前面一篇文章:Spring Boot Starter自动配置的加载原理react
Ribbon是Netflix微服务体系中的一个核心组件。甚至是Java领域中很少见的客户端负载均衡组件,恕我孤陋寡闻。关于Ribbon的原理,其实不复杂。Github的文档倒也还算完整,只是咱们通常不会直接使用Ribbon,而是使用Spring Cloud提供的Netflix Ribbon Starter,所以文档会有很多对不上的地方。git
Ribbon五大组件:github
ServerList
:定义获取服务器列表ServerListFilter
:对ServerList服务器列表进行二次过滤ServerListUpdater
:定义服务更新策略IPing
:检查服务列表是否存活IRule
:根据算法中从服务列表中选取一个要访问的服务Ribbon的主要接口:算法
ILoadBalancer
:软件负载平衡器入口,整合以上全部的组件实现负载功能Ribbon原生代码有两个包特别重要,com.netflix.loadbalancer
包和com.netflix.client
包spring
loadbalancer
包核心类图: apache
client
包核心类图:安全
总结:bash
loadblancer
包中最外层及最重要的接口就是ILoadBalancer
,但它只具备LB的功能,不具备发请求的功能,所以最终仍是须要有包含ILoadBlancer
的clientIClient
接口,在client
包中定义LoadBalancerContext
及其继承类AbstractLoadBalancerAwareClient
是实现全部带LB功能的IClient
子类的父类。而谁会实现这种client?答案是Spring Cloud的代码!LoadBalancerContext
的继承类,除了AbstractLoadBalancerAwareClient
,全是Spring Cloud包的。
AbstractLoadBalancerAwareClient
的实现又用到了com.netflix.loadbalancer.reactive
包里面的LoadBalancerCommand
,后者利用RxJava封装了Retry逻辑,而Retry配置由RetryHandler
配置。
上文极为概况地总结了Ribbon的重要组件。无论你看没看懂,我反正是懂了…… (啊,其实不是很重要,重要的是这一节)服务器
关于Ribbon,你须要记住的是它是个中间层组件,只提供Load Balance功能。而咱们使用Ribbon的缘由通常都是发送客户端请求。在Spring Cloud环境下,每每就这么两种外层组件:
RestTemplate
和Feign
。所以,它们必然是封装了Ribbon的功能,才能实现负载均衡,(以及基于Eureka的服务发现)。
直接来看Ribbon Starter这个包。
META-INF/spring.factories
文件以下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration
复制代码
太好了,就这么一个自动配置类。来看看它作了什么:
// 省略不重要代码
@Configuration
@RibbonClients
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
@Bean
@ConditionalOnMissingBean(LoadBalancerClient.class)
public LoadBalancerClient loadBalancerClient() {
return new RibbonLoadBalancerClient(springClientFactory());
}
@Bean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
@ConditionalOnMissingBean
public LoadBalancedRetryFactory loadBalancedRetryPolicyFactory( final SpringClientFactory clientFactory) {
return new RibbonLoadBalancedRetryFactory(clientFactory);
}
@Configuration
@ConditionalOnClass(HttpRequest.class)
@ConditionalOnRibbonRestClient
protected static class RibbonClientHttpRequestFactoryConfiguration {
@Autowired
private SpringClientFactory springClientFactory;
@Bean
public RestTemplateCustomizer restTemplateCustomizer( final RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory) {
return restTemplate -> restTemplate
.setRequestFactory(ribbonClientHttpRequestFactory);
}
@Bean
public RibbonClientHttpRequestFactory ribbonClientHttpRequestFactory() {
return new RibbonClientHttpRequestFactory(this.springClientFactory);
}
}
// TODO: support for autoconfiguring restemplate to use apache http client or okhttp
}
复制代码
捡重点的说,这段代码主要干了这么几件事:
@RibbonClients
注解,等会咱们再看它。SpringClientFactory
,这是个Spring Cloud增长的功能,至关于一个Map,里面放的是client名到Application Context的映射。也就是说,对于Ribbon,一个client名就对应一组bean,这样方能实现配置隔离。LoadBalancerClient
Bean,这个类是对原生Ribbon的封装,提供负载均衡功能。彩蛋:代码末尾还有个
TODO
注释:配置RestTemlate支持http client 或 okhttp,可见目前并无实现。通过断点调试我验证了这一点。
好,接下来咱们看看RibbonClients
注解是何方神圣。
//省略部分代码
@Configuration
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
RibbonClient[] value() default {};
Class<?>[] defaultConfiguration() default {};
}
复制代码
RibbonClientConfigurationRegistrar
, 这显然是个ImportBeanDefinitionRegistrar
的实现类。嗯,基本上全部的@EnableXYZ
注解都是经过它实现的。RibbonClientConfigurationRegistrar
的代码再也不贴了,它自动建立了全部声明的Ribbon client的配置bean。
到这里你应该提出疑问了:怎么没看到哪里建立Ribbon原生类的Bean?
很好,咱们来看看starter包里还有什么。 很容易就找到了RibbonClientConfiguration
这个Java配置类:
@Configuration
@EnableConfigurationProperties
@Import({ HttpClientConfiguration.class, OkHttpRibbonConfiguration.class,
RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class })
public class RibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public IClientConfig ribbonClientConfig() {
DefaultClientConfigImpl config = new DefaultClientConfigImpl();
config.loadProperties(this.name);
config.set(CommonClientConfigKey.ConnectTimeout, DEFAULT_CONNECT_TIMEOUT);
config.set(CommonClientConfigKey.ReadTimeout, DEFAULT_READ_TIMEOUT);
config.set(CommonClientConfigKey.GZipPayload, DEFAULT_GZIP_PAYLOAD);
return config;
}
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, name)) {
return this.propertiesFactory.get(IPing.class, config, name);
}
return new DummyPing();
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("unchecked")
public ServerList<Server> ribbonServerList(IClientConfig config) {
if (this.propertiesFactory.isSet(ServerList.class, name)) {
return this.propertiesFactory.get(ServerList.class, config, name);
}
ConfigurationBasedServerList serverList = new ConfigurationBasedServerList();
serverList.initWithNiwsConfig(config);
return serverList;
}
@Bean
@ConditionalOnMissingBean
public ServerListUpdater ribbonServerListUpdater(IClientConfig config) {
return new PollingServerListUpdater(config);
}
@Bean
@ConditionalOnMissingBean
public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
if (this.propertiesFactory.isSet(ILoadBalancer.class, name)) {
return this.propertiesFactory.get(ILoadBalancer.class, config, name);
}
return new ZoneAwareLoadBalancer<>(config, rule, ping, serverList,
serverListFilter, serverListUpdater);
}
@Bean
@ConditionalOnMissingBean
@SuppressWarnings("unchecked")
public ServerListFilter<Server> ribbonServerListFilter(IClientConfig config) {
if (this.propertiesFactory.isSet(ServerListFilter.class, name)) {
return this.propertiesFactory.get(ServerListFilter.class, config, name);
}
ZonePreferenceServerListFilter filter = new ZonePreferenceServerListFilter();
filter.initWithNiwsConfig(config);
return filter;
}
@Bean
@ConditionalOnMissingBean
public RibbonLoadBalancerContext ribbonLoadBalancerContext(ILoadBalancer loadBalancer, IClientConfig config, RetryHandler retryHandler) {
return new RibbonLoadBalancerContext(loadBalancer, config, retryHandler);
}
}
复制代码
显然,这个类就是用来建立Ribbon原生类的Bean的。然而它是在哪触发的呢? 这个问题解释起来有点费劲,不如打个断点调试一下:
解释:
RibbonLoadBalancerClient
就是本节一开始那个LoadBalancerClient
的实现类,上文说过,它是Spring Cloud对Ribbon的封装。其持有一个SpringClientFactory
。RibbonClientConfiguration
。RibbonLoadBalancerClient#execute()
方法是从SpringClientFactory
得到真正的Ribbon原生类,从而实现负载均衡功能。SpringClientFactory
,前文说过,它是一个Application Context的map容器。也就是说,对于一个ribbon client,就有一组隔离的bean,包括IRule, IPing, ServerList这些。SpringClientFactory
获取原生Ribbon类的Bean时,前者须要建立新的Application。 Context,天然就须要传入Java配置类。建立后刷新Application Context,RibbonClientConfiguration
就被导入了。@RibbonClient(configuration=XXX.class)
这种方式自定义Ribbon的配置了原理吧,就是替换了默认的RibbonClientConfiguration
。若是你看了spring-cloud-netflix-eureka-client-starter就明白了。Eureka client的自动配置会自动建立基于Eureka服务发现的Ribbon ServerList等一系列Ribbon组件bean。这样你不用作任何事就自动具备了Eureka服务发现功能。
@Configuration
public class EurekaRibbonClientConfiguration {
@Bean
@ConditionalOnMissingBean
public IPing ribbonPing(IClientConfig config) {
if (this.propertiesFactory.isSet(IPing.class, serviceId)) {
return this.propertiesFactory.get(IPing.class, config, serviceId);
}
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 discoveryServerList = new DiscoveryEnabledNIWSServerList(
config, eurekaClientProvider);
DomainExtractingServerList serverList = new DomainExtractingServerList(
discoveryServerList, config, this.approximateZoneFromHostname);
return serverList;
}
...
}
复制代码
可是这也带来了另外一个问题:若是你有一个服务在eureka以外,想经过CLIENT-NAME.ribbon.serverList=adress1,adress2
这种方式就不能生效了。由于Eureka给你建立的ServerList实现是DiscoveryEnabledNIWSServerList
,不支持配置方式。你须要再加上一条配置:CLIENT-NAME.ribbon.NIWSServerListClassName=com.netflix.loadbalancer.ConfigurationBasedServerList
。
在原生Ribbon被开发的年代,Netflix并无使用Spring Boot(那时固然尚未)和Spring,而是采用了本身的框架。在配置方面,他们有本身的动态配置框架Archaius。好比你能够从原生Ribbon的文档里看到一些配置示例。而在Spring Cloud中,咱们也可使用一样的配置定制Ribbon,这是为何?
在Spring Cloud Netflix的文档中,有过解释:
Spring applications should generally not use Archaius directly, but the need to configure the Netflix tools natively remains. Spring Cloud has a Spring Environment Bridge so that Archaius can read properties from the Spring Environment. This bridge allows Spring Boot projects to use the normal configuration toolchain while letting them configure the Netflix tools as documented (for the most part).
也就是说Spring Cloud先用本身的Configuration Properties功能封装Archaius的配置,获取到配置后,再在必要时传递给Archaius,这样Netflix的原生组件就能够无缝使用。
修改Spring-Cloud-Netflix-Ribbon配置有几种方式:
@RibbonClients(defaultConfiguration=xxx.class)
,替换默认的全局配置。@RibbonClient(configuration=xxx.class)
,替换单个client的配置。CLIENT-NAME.ribbon.
的方式配置。ribbon.xyz
指定全部client的默认配置。这个并无在官方文档中介绍。而它起做用的原理在这个方法中:com.netflix.client.config.DefaultClientConfigImpl#getProperty(java.lang.String)
。当找不到ClientName开头的配置时,会直接使用ribbon前缀的配置。Feign 最初也是Netflix的,只是后来他们本身再也不使用了,开源出来后就改了个名字,叫OpenFeign。这个故事能够今后GitHub issue看到。
OpenFeign的官方文档上声称他们是受了Retrofit的启发。因此这两个框架不管是使用仍是设计都是很像的。
用脚趾头想一下,原理就是在运行时根据声明的Api接口,生成动态代理。代码可见feign.ReflectiveFeign#newInstance
。
几个重要的组件:
这些组件能够在下面这个Spring Cloud OpenFeign的配置里头看出个大概
其实和Ribbon的配置很像了。也有@FeignClient
和@FeignClients
注解。 @EnableFeignClients
则导入了FeignClientsRegistrar
,后者是一个ImportBeanDefinitionRegistrar
的实现类。在其接口方法中,作两件事:
@FeignClient
的接口都找出来,而后为它生成实现类的bean。@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
复制代码
因此,若是Retrofit也要集成进Spring Boot,天然也须要在Starter中建立@EnableRetrofit
这样的注解,而后作一样的事情。
另外,Feign要作到分client配置独立,也会使用到相似Ribbon的SpringClientFactory
类型,而在Feign这边叫FeignContext
,二者都是继承自NamedContextFactory
的。原理可见org.springframework.cloud.context.named.NamedContextFactory#createContext
。
下面仍是说下原理吧,仍是挺费解的。
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification> implements DisposableBean, ApplicationContextAware {
// 保存局部应用上下文的map,好比定义了App1, App2两个client,就保存两个entry
private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();
// 保存配置的map,每一个client能够单独有一个
private Map<String, C> configurations = new ConcurrentHashMap<>();
// 此NamedContextFactory所要建立的具体client的默认Java配置类,构造时传入
private Class<?> defaultConfigType;
protected AnnotationConfigApplicationContext createContext(String name) {
// 新建一个局部的ApplicationContext,由注解驱动。
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// 把client独有的配置注册进去
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
// 把default.开头的配置注册进去
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// 把默认的Java配置类注册进去,就是Ribbon
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);
// 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
会在构造时接受三个参数,第一个参数指定默认配置。而这个类目前也只有两个实现:
NamedContextFactory#getInstance(String name, ResolvableType type)
这个方法来得到这个局部上下文内部的Bean,其中第一个方法是client名,第二个方法是Bean的类型名。而此方法首先会检查对应的client名在不在contexts
这个map里面,若是没有,就要调用createContext(String name)
建立。OpenFeign比Ribbon更好地支持了Spring的Properties外置化配置,缘由是Ribbon使用了Archaius,和Spring兼容不够好,而OpenFeign没有。Spring Cloud OpenFeign的外置化配置可见FeignClientProperties
,其使用之处则在FeignClientFactoryBean#configureFeign
。
使用这种方式,咱们能够方便地在配置文件中使用feign.client.config.CLIENT-NAME.xyz=blabla
来指定某个Feign Client的具体配置,甚至能够用feign.client.config.default.xyz=blabla
来指定全部client的默认配置,好评!
在后面的 Spring Cloud Netflix Hystrix中,咱们能够看到相似的配置设计。
OpenFeign的retry功能是利用的Spring Retry框架。而要使用retry基本上须要的也就是个Retry Policy配置。OpenFeign并无默认配置,而是利用了Ribbon的配置。具体配置参见com.netflix.client.config.CommonClientConfigKey
。
修改OpenFeign配置有几种方式:
@EnableFeignClients(defaultConfiguration=xxx.class)
,替换默认的全局配置。@RibbonClient(configuration=xxx.class)
,替换单个client的配置。feign.client.config.default.
的方式配置client默认设置。feign.client.config.CLIENT-NAME.
的方式配置单个client。补充:
feign.Request.Options
。若是配置了feign的超时,就会按feign的超时;若没配feign的超时,则会按ribbon的超时,此时若ribbon也没配置,则会默认Connect-timeout和Read-timeout都是1秒;若配置了ribbon的超时,则会按ribbon的超时。关注下异步的Feign Client:github.com/kptfh/feign…