使用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…