在上一文EnableDiscoveryClient没用了?Zookeeper是怎么和springboot配合做服务注册中心的?讲过了zk是怎么做服务注册和服务发现的,同时在spring.factories中我们还发现了RibbonZookeeperAutoConfiguration配置类,于是接下来翻阅了相关代码,对ribbon一探究竟
RibbonZookeeperAutoConfiguration
@Configuration
@EnableConfigurationProperties
@ConditionalOnZookeeperEnabled
@ConditionalOnBean(SpringClientFactory.class)
@ConditionalOnRibbonZookeeper
@AutoConfigureAfter(RibbonAutoConfiguration.class)
@RibbonClients(defaultConfiguration = ZookeeperRibbonClientConfiguration.class)
public class RibbonZookeeperAutoConfiguration {
}
一些常见的注解就不再赘述了,这里主要搞明白三个点:
1. SpringClientFactory
SpringClientFactory:会给每个Ribbon Client创建一个独立的Spring应用上下文ApplicationContext,并在其中加载对应的配置及Ribbon核心接口的实现类
/**
* A factory that creates client, load balancer and client configuration instances. It
* creates a Spring ApplicationContext per client name, and extracts the beans that it
* needs from there.
*
* @author Spencer Gibb
* @author Dave Syer
*/
public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
该工厂提供getClient、getClientConfig、getLoadBalancer、getLoadBalancerContext以分别根据当前Ribbon客户端获取对应的负责方法执行的client、用于初始化客户端或负载均衡器以及方法执行的客户端配置(默认实现是 DefaultClientConfigImpl)、定义软件负载均衡器操作的接口以及负载均衡器的上下文接口。
以上四个方法都使用到了getInstance方法,name代表当前Ribbon客户端,type代表要获取的实例类型,如IClient、IRule
public <C> C getInstance(String name, Class<C> type) {
C instance = super.getInstance(name, type);
if (instance != null) {
return instance;
}
IClientConfig config = getInstance(name, IClientConfig.class);
return instantiateWithConfig(getContext(name), type, config);
}
先从父类NamedContextFactory中直接从客户端对应的ApplicationContext中获取实例,如果没有就根据IClientConfig中的配置找到具体的实现类,并通过反射初始化后放到Client对应的ApplicationContext中
// 使用IClientConfig实例化
static <C> C instantiateWithConfig(AnnotationConfigApplicationContext context,
Class<C> clazz, IClientConfig config) {
C result = null;
try {
// 通过以IClientConfig为参数的构造创建clazz类实例
Constructor<C> constructor = clazz.getConstructor(IClientConfig.class);
result = constructor.newInstance(config);
} catch (Throwable e) {
// Ignored
}
// 如果没创建成功,使用无惨构造
if (result == null) {
result = BeanUtils.instantiate(clazz);
// 调用初始化配置方法
if (result instanceof IClientConfigAware) {
((IClientConfigAware) result).initWithNiwsConfig(config);
}
// 处理自动织入
if (context != null) {
context.getAutowireCapableBeanFactory().autowireBean(result);
}
}
return result;
}
}
protected AnnotationConfigApplicationContext getContext(String name) {
if (!this.contexts.containsKey(name)) {
synchronized (this.contexts) {
if (!this.contexts.containsKey(name)) {
this.contexts.put(name, createContext(name));
}
}
}
return this.contexts.get(name);
}
此处会使用双关检索机制再次检查对应的上下文是否创建,若仍未创建,则调用createContext方法创建。
// 创建名为name的RibbonClient的ApplicationContext上下文
protected AnnotationConfigApplicationContext createContext(String name) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
// configurations集合中是否包含当前Client相关配置类,包含即注入到ApplicationContext
if (this.configurations.containsKey(name)) {
for (Class<?> configuration : this.configurations.get(name)
.getConfiguration()) {
context.register(configuration);
}
}
//configurations集合中是否包含default.开头的通过@RibbonClients(defaultConfiguration=xxx)配置的默认配置类
for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
if (entry.getKey().startsWith("default.")) {
for (Class<?> configuration : entry.getValue().getConfiguration()) {
context.register(configuration);
}
}
}
// 注册PropertyPlaceholderAutoConfiguration、RibbonClientConfiguration
context.register(PropertyPlaceholderAutoConfiguration.class,
this.defaultConfigType);
// 添加 ribbon.client.name=具体RibbonClient name的enviroment配置
context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
this.propertySourceName,
Collections.<String, Object> singletonMap(this.propertyName, name)));
// 设置父ApplicationContext,这样可以使得当前创建的子ApplicationContext可以使用父上下文中的Bean
if (this.parent != null) {
// Uses Environment from parent as well as beans
context.setParent(this.parent);
}
context.refresh(); //刷新Context
return context;
}
- 从configurations这个Map中根据RibbonClient name获取专门为其指定的configuration配置类,并注册到其对应的ApplicationContext上下文
- 从configurations这个Map中找到 default. 开头 的配置类,即为所有RibbonClient的默认配置,并注册到其对应的ApplicationContext上下文
- 如果不是开发者单独指定的话,前两项都是没有数据的,还会注册Spring Cloud的默认配置类RibbonClientConfiguration
那么为什么要引入SpringClientFactory会给每个Ribbon Client创建一个独立的Spring应用上下文ApplicationContext呢?
在一个微服务中,调用微服务 A 与调用微服务 B 的配置可能不同。比较简单的例子就是,A 微服务是一个简单的用户订单查询服务,接口返回速度很快,B 是一个报表微服务,接口返回速度比较慢。这样的话我们就不能对于调用微服务 A 和微服务 B 使用相同的超时时间配置。还有就是,我们可能对于服务 A 通过注册中心进行发现,对于服务 B 则是通过 DNS 解析进行服务发现,所以对于不同的微服务我们可能使用不同的组件,在 Spring 中就是使用不同类型的 Bean
2. RibbonAutoConfiguration
再讲SpringClientFactory时还剩下一个疑问:configurations这个map是怎么来的呢?
@Configuration
@Conditional(RibbonAutoConfiguration.RibbonClassesConditions.class)
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties({RibbonEagerLoadProperties.class, ServerIntrospectorProperties.class})
public class RibbonAutoConfiguration {
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Autowired
private RibbonEagerLoadProperties ribbonEagerLoadProperties;
@Bean
public HasFeatures ribbonFeature() {
return HasFeatures.namedFeature("Ribbon", Ribbon.class);
}
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
}
可以看到RibbonAutoConfiguration这个自动配置类中自动注入了RibbonClientSpecification的列表,就是我们要的configurations,同时这个配置类会创建一个springClientFactory出来,并且吧自动注入的configurations设置为springClientFactory的configurations属性。
那么,configurations和RibbonClientSpecification是何时被注册进容器的呢?
3. RibbonClients
@Configuration
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE })
@Documented
@Import(RibbonClientConfigurationRegistrar.class)
public @interface RibbonClients {
RibbonClient[] value() default {};
Class<?>[] defaultConfiguration() default {};
}
这里需要注意的也只有RibbonClientConfigurationRegistrar这个类
首先RibbonClientConfigurationRegistrar实现了ImportBeanDefinitionRegistrar接口,表明这里会调用registerBeanDefinitions方法向容器中注册一些组件,看代码:
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 1、@RibbonClients注解
Map<String, Object> attrs = metadata.getAnnotationAttributes(
RibbonClients.class.getName(), true);
figuration,即默认配置
// 注册成以default.Classname.RibbonClientSpecification为名的RibbonClientSpecification
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"));
}
// 2、@RibbonClient注解
// 注册某个具体Ribbon Client的configuration配置类
Map<String, Object> client = metadata.getAnnotationAttributes(
RibbonClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
如上可知,configurations配置类集合是根据@RibbonClient 和 @RibbonClients 注解配置的,分别有 针对具体某个RibbonClient的配置 和 default默认配置,而以上注册的实际类型就是RibbonClientSpecification。
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());
}
4. ZookeeperRibbonClientConfiguration (RibbonClientConfiguration)
上面说是如何创建RibbonClient相关的ApplicationContext上下文及注册Ribbon Client相关的配置类的逻辑,在确定配置类后,其中会用到Ribbon的IClientConfig相关的客户端配置来加载Ribbon客户端相关的配置信息,如超时配置、具体创建哪个核心接口的实现类等,因为上面的源码是用zk来来做注册中心,为了通用性,这里可以从Spring Cloud默认注册的 RibbonClientConfiguration来一探究竟
4.1 IClientConfig
@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;
}
可以看到先是创建了DefaultClientConfigImpl默认实现类,再config.loadProperties(this.name)加载当前Client相关的配置
@Override
public void loadProperties(String restClientName){
enableDynamicProperties = true;
setClientName(restClientName);
// 1、使用Netflix Archaius的ConfigurationManager从Spring env中加载“ribbon.配置项”这类默认配置
// 如没加载到有默认静态配置
loadDefaultValues();
// 2、使用Netflix Archaius的ConfigurationManager从Spring env中加载“client名.ribbon.配置项”这类针对某个Client的配置信息
Configuration props = ConfigurationManager.getConfigInstance().subset(restClientName);
for (Iterator<String> keys = props.getKeys(); keys.hasNext(); ){
String key = keys.next();
String prop = key;
try {
if (prop.startsWith(getNameSpace())){
prop = prop.substring(getNameSpace().length() + 1);
}
setPropertyInternal(prop, getStringValue(props, key));
} catch (Exception ex) {
throw new RuntimeException(String.format("Property %s is invalid", prop));
}
}
}
如果你没有在项目中指定ribbon相关配置,那么会使用DefaultClientConfigImpl中的默认静态配置,如果Spring enviroment中包含“ribbon.配置项”这类针对所有Client的配置会被加载进来,有“client名.ribbon.配置项”这类针对某个Client的配置信息也会被加载进来,以下是静态默认配置
public static final Boolean DEFAULT_PRIORITIZE_VIP_ADDRESS_BASED_SERVERS = Boolean.TRUE;
public static final String DEFAULT_NFLOADBALANCER_PING_CLASSNAME = "com.netflix.loadbalancer.DummyPing"; // DummyPing.class.getName();
public static final String DEFAULT_NFLOADBALANCER_RULE_CLASSNAME = "com.netflix.loadbalancer.AvailabilityFilteringRule";
public static final String DEFAULT_NFLOADBALANCER_CLASSNAME = "com.netflix.loadbalancer.ZoneAwareLoadBalancer";
public static final boolean DEFAULT_USEIPADDRESS_FOR_SERVER = Boolean.FALSE;
public static final String DEFAULT_CLIENT_CLASSNAME = "com.netflix.niws.client.http.RestClient";
public static final String DEFAULT_VIPADDRESS_RESOLVER_CLASSNAME = "com.netflix.client.SimpleVipAddressResolver";
public static final String DEFAULT_PRIME_CONNECTIONS_URI = "/";
public static final int DEFAULT_MAX_TOTAL_TIME_TO_PRIME_CONNECTIONS = 30000;
public static final int DEFAULT_MAX_RETRIES_PER_SERVER_PRIME_CONNECTION = 9;
public static final Boolean DEFAULT_ENABLE_PRIME_CONNECTIONS = Boolean.FALSE;
public static final int DEFAULT_MAX_REQUESTS_ALLOWED_PER_WINDOW = Integer.MAX_VALUE;
public static final int DEFAULT_REQUEST_THROTTLING_WINDOW_IN_MILLIS = 60000;
public static final Boolean DEFAULT_ENABLE_REQUEST_THROTTLING = Boolean.FALSE;
public static final Boolean DEFAULT_ENABLE_GZIP_CONTENT_ENCODING_FILTER = Boolean.FALSE;
public static final Boolean DEFAULT_CONNECTION_POOL_CLEANER_TASK_ENABLED = Boolean.TRUE;
public static final Boolean DEFAULT_FOLLOW_REDIRECTS = Boolean.FALSE;
public static final float DEFAULT_PERCENTAGE_NIWS_EVENT_LOGGED = 0.0f;
public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
public static final int DEFAULT_MAX_AUTO_RETRIES = 0;
public static final int DEFAULT_BACKOFF_INTERVAL = 0;
public static final int DEFAULT_READ_TIMEOUT = 5000;
public static final int DEFAULT_CONNECTION_MANAGER_TIMEOUT = 2000;
public static final int DEFAULT_CONNECT_TIMEOUT = 2000;
public static final Boolean DEFAULT_ENABLE_CONNECTION_POOL = Boolean.TRUE;
@Deprecated
public static final int DEFAULT_MAX_HTTP_CONNECTIONS_PER_HOST = 50;
@Deprecated
public static final int DEFAULT_MAX_TOTAL_HTTP_CONNECTIONS = 200;
public static final int DEFAULT_MAX_CONNECTIONS_PER_HOST = 50;
public static final int DEFAULT_MAX_TOTAL_CONNECTIONS = 200;
public static final float DEFAULT_MIN_PRIME_CONNECTIONS_RATIO = 1.0f;
public static final String DEFAULT_PRIME_CONNECTIONS_CLASS = "com.netflix.niws.client.http.HttpPrimeConnection";
public static final String DEFAULT_SEVER_LIST_CLASS = "com.netflix.loadbalancer.ConfigurationBasedServerList";
public static final String DEFAULT_SERVER_LIST_UPDATER_CLASS = "com.netflix.loadbalancer.PollingServerListUpdater";
public static final int DEFAULT_CONNECTION_IDLE_TIMERTASK_REPEAT_IN_MSECS = 30000; // every half minute (30 secs)
public static final int DEFAULT_CONNECTIONIDLE_TIME_IN_MSECS = 30000; // all connections idle for 30 secs
4.2 IRule & IPing & ribbonServerList &ILoadBalancer
以IRule为例,查看propertiesFactory是否有关于当前接口的配置,如有就使用,并创建实例返回,否则返回一个默认的实现
@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
// 查看propertiesFactory是否有关于当前接口的配置,如有就使用,并创建实例返回
if (this.propertiesFactory.isSet(IRule.class, name)) {
return this.propertiesFactory.get(IRule.class, config, name);
}
// spring cloud 默认配置
ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
rule.initWithNiwsConfig(config);
return rule;
}
那么接下来看看this.propertiesFactory是何方神圣
public class PropertiesFactory {
@Autowired
private Environment environment;
private Map<Class, String> classToProperty = new HashMap<>();
public PropertiesFactory() {
classToProperty.put(ILoadBalancer.class, "NFLoadBalancerClassName");
classToProperty.put(IPing.class, "NFLoadBalancerPingClassName");
classToProperty.put(IRule.class, "NFLoadBalancerRuleClassName");
classToProperty.put(ServerList.class, "NIWSServerListClassName");
classToProperty.put(ServerListFilter.class, "NIWSServerListFilterClassName");
}
// 查看当前clazz是否在classToProperty管理的几个核心接口之一
// 如是,查看Spring environment中是否能找到 “clientName.ribbon.核心接口配置项”的配置信息
public boolean isSet(Class clazz, String name) {
return StringUtils.hasText(getClassName(clazz, name));
}
public String getClassName(Class clazz, String name) {
if (this.classToProperty.containsKey(clazz)) {
String classNameProperty = this.classToProperty.get(clazz);
String className = environment.getProperty(name + "." + NAMESPACE + "." + classNameProperty);
return className;
}
return null;
}
// 也是先调用getClassName()获取Spring enviroment中配置的核心接口实现类名
// 再使用IClientConfig配置信息创建其实例
@SuppressWarnings("unchecked")
public <C> C get(Class<C> clazz, IClientConfig config, String name) {
String className = getClassName(clazz, name);
if (StringUtils.hasText(className)) {
try {
Class<?> toInstantiate = Class.forName(className);
return (C) instantiateWithConfig(toInstantiate, config);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException("Unknown class to load "+className+" for class " + clazz + " named " + name);
}
}
return null;
}
}
如何自定义RibbonClient配置、全局配置
首先,Spring Cloud Netflix provides the following beans by default for ribbon (BeanType beanName: ClassName):
Spring Cloud 已经提供以下ribbon组件
- IClientConfig ribbonClientConfig: DefaultClientConfigImpl
- IRule ribbonRule: ZoneAvoidanceRule
- IPing ribbonPing: NoOpPing
- ServerList ribbonServerList: ConfigurationBasedServerList
- ServerListFilter ribbonServerListFilter: ZonePreferenceServerListFilter
- ILoadBalancer ribbonLoadBalancer: ZoneAwareLoadBalancer
- ServerListUpdater ribbonServerListUpdater: PollingServerListUpdater
如何自定义?
- @RibbonClient(name = “foo”, configuration = FooConfiguration.class) 是针对名为 foo 的RibbonClient的配置类
- 也可以使用@RibbonClients({@RibbonClient数组}) 的形式给某几个RibbonClient设置配置类
- @RibbonClients( defaultConfiguration = { xxx.class } ) 是针对所有RIbbonClient的默认配置
特别注意:
配置类必须是必须是@Configuration的,这样就必须注意,SpringBoot主启动类不能扫描到FooConfiguration,否则针对某个RibbonClient的配置就会变成全局的,原因是在创建每个RibbonClient时会为其创建ApplicationContext上下文,其parent就是主启动类创建的ApplicationContext,子ApplicationContext中可以使用父ApplicationContext中的Bean,且创建Bean时都使用了@ConditionalOnMissingBean,所以FooConfiguration如果被主启动类的上下文加载,且创建了比如IRule的实现类,在某个RIbbonClient创建其子ApplicationContext并@Bean想创建其自定义IRule实现类时,会发现parent ApplicationContext已经存在,就不会创建了,配置就失效了
如何解决?
- 配置类在主启动扫描路径之外
- 自定义注解,在扫描的时候显示排除
- 使用@RibbonClients{数组}指定配置类,配置类上不添加@Configuration的