How Ribbon customizes client and global configurations

Keywords: PHP Spring SpringBoot Apache REST

cause

The reason for this is that to implement grayscale routing based on Zuul Gateway within the company and to perform grayscale tests when online, you need to configure metadata metadata registered with Eureka by Business Micro Services and customize Ribbon's load rules to access grayscale services only.This requires customizing Ribbon's IRule so that grayscale requests are only loaded on business microservices with grayscale tag metadata. When a custom IRule rule is developed, the problem is how to configure the IRule rule to a Ribbon Client or take effect globally.

Use Spring Cloud Dalston.SR5 version this time

In Official Documents There are some ways to target a Client or modify the default configuration, but there is no explanation why this is used

Following is an analysis that follows this line of thought:

  • Simply analyze how Spring Cloud Ribbon is automatically configured at startup to see how it fits into a Bean in Spring
  • Lazy Loading of Spring Cloud Ribbon Client
  • Configuration load for Spring Cloud Ribbon Client, including global configuration and client configuration
  • How to customize Client configuration, global configuration
  • Explain some considerations in official documents


Spring Cloud Ribbon AutoConfiguration

All automatic configurations for Netflix in the current version are in spring-cloud-netflix-core-xxx.jar, and according to the configuration in its META-INF/spring.factories, the automatic configuration class for Spring Cloud Ribbon is RibbonAutoConfiguration


RibbonAutoConfiguration

@Configuration
@ConditionalOnClass({ IClient.class, RestTemplate.class, AsyncRestTemplate.class, Ribbon.class})
@RibbonClients
@AutoConfigureAfter(name = "org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration")
@AutoConfigureBefore({LoadBalancerAutoConfiguration.class, AsyncLoadBalancerAutoConfiguration.class})
@EnableConfigurationProperties(RibbonEagerLoadProperties.class)
public class RibbonAutoConfiguration {

    // All configurations specified for a RibbonClient
    @Autowired(required = false)
    private List<RibbonClientSpecification> configurations = new ArrayList<>();
    
    // Is ribbon lazy to load profile
    @Autowired
    private RibbonEagerLoadProperties ribbonEagerLoadProperties;

    // Spring creates a separate ApplicationContext context for each RibbonClient
    // Create a RibbonClient Bean in its context: IClient, ILoadbalancer, etc.
    @Bean
    public SpringClientFactory springClientFactory() {
        SpringClientFactory factory = new SpringClientFactory();
        factory.setConfigurations(this.configurations);
        return factory;
    }

    // The Client with Load Balancing created by Spring uses Spring ClientFactory to create the corresponding Bean and Configuration
    @Bean
    @ConditionalOnMissingBean(LoadBalancerClient.class)
    public LoadBalancerClient loadBalancerClient() {
        return new RibbonLoadBalancerClient(springClientFactory());
    }

    // Load the core interface implementation class of Rubbon for a Client into the Spring environment
    @Bean
    @ConditionalOnMissingBean
    public PropertiesFactory propertiesFactory() {
        return new PropertiesFactory();
    }
    
    // If not lazy, load and initialize client configuration using RibbonApplicationContextInitializer at startup
    @Bean
    @ConditionalOnProperty(value = "ribbon.eager-load.enabled", matchIfMissing = false)
    public RibbonApplicationContextInitializer ribbonApplicationContextInitializer() {
        return new RibbonApplicationContextInitializer(springClientFactory(),
                ribbonEagerLoadProperties.getClients());
    }

    ......
}

Bean s created by Ribbon AutoConfiguration above fall into the following main categories:

  • Create environment and get configuration for Ribbon Client
    • SpringClientFactory: Creates a separate Spring application context ApplicationContext for each Ribbon Client and loads the corresponding configuration and implementation classes for the Ribbon core interface
    • PropertiesFactory: Used to obtain and instantiate a core interface implementation class for a Ribbon Client configuration from the Spring enviroment environment
  • Create the RibbonLoadBalancerClient and inject springClientFactory into it to get the corresponding configuration and implementation classes. RibbonLoadBalancerClient is Spring's implementation class for LoadBalancerClient interface, and its execute() method provides client load balancing capability
  • Lazy Load Related
    • RibbonEagerLoadProperties: Lazy Load Configuration Properties, which specifies whether to load lazily and which Client s are not lazy
    • RibbonApplicationContextInitializer: Load the initializer of the RibbonClient configuration (non-lazy loading) at startup

You can see that the default startup process does not load RibbonClient's context and configuration information, but rather loads when in use, i.e., lazily loads


Lazy Loading of Spring Cloud RibbonClient

Since it is loaded when in use, a RibbonCommand is created in the RibbonRoutingFilter for Zuul Gateway, which includes Ribbon load balancing

//## RibbonRoutingFilter Zuul's Filter for Routing
public class RibbonRoutingFilter extends ZuulFilter {

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        this.helper.addIgnoredHeaders();
        try {
            RibbonCommandContext commandContext = buildCommandContext(context);
            ClientHttpResponse response = forward(commandContext);
            setResponse(response);
            return response;
        }
        catch (ZuulException ex) {
            throw new ZuulRuntimeException(ex);
        }
        catch (Exception ex) {
            throw new ZuulRuntimeException(ex);
        }
    }

    protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
        Map<String, Object> info = this.helper.debug(context.getMethod(),
                context.getUri(), context.getHeaders(), context.getParams(),
                context.getRequestEntity());

        // Create RibbonCommand using ribbonCommandFactory
        RibbonCommand command = this.ribbonCommandFactory.create(context);
        try {
            ClientHttpResponse response = command.execute();
            this.helper.appendDebug(info, response.getStatusCode().value(),
                    response.getHeaders());
            return response;
        }
        catch (HystrixRuntimeException ex) {
            return handleException(info, ex);
        }
    }
}

The forward() method is executed when the RibbonRoutingFilter#run() is executed for routing. Since the Ribbon Load Balancing call is executed inside HystrixCommand here, RibbonCommand is created using ribbonCommandFactory, in which the lazy loading of Ribbon clients is contained, let's see the HttpClientRibbonCommandFactory factPresent Class

//## org.springframework.cloud.netflix.zuul.filters.route.apache.HttpClientRibbonCommandFactory
public class HttpClientRibbonCommandFactory extends AbstractRibbonCommandFactory {
    @Override
    public HttpClientRibbonCommand create(final RibbonCommandContext context) {
        ZuulFallbackProvider zuulFallbackProvider = getFallbackProvider(context.getServiceId());
        final String serviceId = context.getServiceId();
        // Get IClient interface instances from SpringClientFactory
        final RibbonLoadBalancingHttpClient client = this.clientFactory.getClient(
                serviceId, RibbonLoadBalancingHttpClient.class);
        client.setLoadBalancer(this.clientFactory.getLoadBalancer(serviceId));

        return new HttpClientRibbonCommand(serviceId, client, context, zuulProperties, zuulFallbackProvider,
                clientFactory.getClientConfig(serviceId));
    }
}

The logic to create the RibbonLoadBalancingHttpClient is SpringClientFactory#getClient(serviceId, RibbonLoadBalancingHttpClient.class), as follows:

  • SpringClientFactory#getInstance(name, clientClass)
    • NamedContextFactory#getInstance(name, type):
      • Gets the corresponding ApplicationContext for the Client, and calls createContext() if not, to create one that contains logic to register the configuration class set by the uniform default configuration class RibbonClientConfiguration or by the @RibbonClient, @RibbonClients(defaultConfiguration=xxx)
      • Get instances from the ApplicationContext by type, such as not created using reflection, and configure them via IClientConfig

Once the RibbonClient is finished with basic lazy loading, you can go to the corresponding ApplicationContext of the RibbonClient and continue to get the implementation classes of the other core interfaces that were created according to the default/global/Client custom configuration

//## org.springframework.cloud.netflix.ribbon.SpringClientFactory
public class SpringClientFactory extends NamedContextFactory<RibbonClientSpecification> {
    static final String NAMESPACE = "ribbon";

    public SpringClientFactory() {
        super(RibbonClientConfiguration.class, NAMESPACE, "ribbon.client.name");
    }
    
    /**
     * Get the rest client associated with the name.
     * @throws RuntimeException if any error occurs
     */
    public <C extends IClient<?, ?>> C getClient(String name, Class<C> clientClass) {
        return getInstance(name, clientClass);
    }
    
    // name represents the current Ribbon client and type represents the type of instance to get, such as IClient, IRule
    @Override
    public <C> C getInstance(String name, Class<C> type) {
        // Get an instance directly from the client's corresponding ApplicationContext from the parent NamedContextFactory first
        // If no specific implementation class is found based on the configuration in IClientConfig and initialized by reflection, placed in the corresponding ApplicationContext for Client
        C instance = super.getInstance(name, type);
        if (instance != null) {
            return instance;
        }
        IClientConfig config = getInstance(name, IClientConfig.class);
        return instantiateWithConfig(getContext(name), type, config);
    }
    
    // Instantiate using IClientConfig
    static <C> C instantiateWithConfig(AnnotationConfigApplicationContext context,
                                        Class<C> clazz, IClientConfig config) {
        C result = null;
        try {
            // Create clazz class instance by constructing with IClientConfig as parameter
            Constructor<C> constructor = clazz.getConstructor(IClientConfig.class);
            result = constructor.newInstance(config);
        } catch (Throwable e) {
            // Ignored
        }
        
        // If not, use tragic construction
        if (result == null) {
            result = BeanUtils.instantiate(clazz);
            
            // Initialization Configuration Method Invoked
            if (result instanceof IClientConfigAware) {
                ((IClientConfigAware) result).initWithNiwsConfig(config);
            }
            
            // Handle automatic weaving
            if (context != null) {
                context.getAutowireCapableBeanFactory().autowireBean(result);
            }
        }
        return result;
    }
    
}


//##Parent org.springframework.cloud.context.named.NamedContextFactory
public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
        implements DisposableBean, ApplicationContextAware {
    // Maintain the ApplicationContext context for the Ribbon client
    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

    // Maintain the @Configuration configuration class for Ribbon clients
    private Map<String, C> configurations = new ConcurrentHashMap<>();

    private ApplicationContext parent;

    private Class<?> defaultConfigType;  // The default configuration class is RibbonClientConfiguration
    private final String propertySourceName;  // ribbon by default
    private final String propertyName;  // The default property to read RibbonClient names is ribbon.client.name

    public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
            String propertyName) {
        this.defaultConfigType = defaultConfigType;
        this.propertySourceName = propertySourceName;
        this.propertyName = propertyName;
    }

    // Return directly if containing Client context
    // If not, call createContext(name) and place it in the contexts collection
    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);
    }

    // Create the ApplicationContext context for the RubbonClient named name
    protected AnnotationConfigApplicationContext createContext(String name) {
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        
        // Whether the current Client-related configuration class, which is injected into the ApplicationContext, is included in the configurations collection
        if (this.configurations.containsKey(name)) {
            for (Class<?> configuration : this.configurations.get(name)
                    .getConfiguration()) {
                context.register(configuration);
            }
        }
        
        //Does the configurations collection contain default. default configuration classes that start with @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);
                }
            }
        }
        
        // Register PropertyPlaceholderAutoConfiguration, RibbonClientConfiguration
        context.register(PropertyPlaceholderAutoConfiguration.class,
                this.defaultConfigType);
        // Add enviroment configuration for ribbon.client.name=specific RibbonClient name       
        context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
                this.propertySourceName,
                Collections.<String, Object> singletonMap(this.propertyName, name)));
        
        // Set the parent ApplicationContext so that the currently created child ApplicationContext can use beans in the parent context
        if (this.parent != null) {
            // Uses Environment from parent as well as beans
            context.setParent(this.parent);
        }
        context.refresh();  //Refresh Context
        return context;
    }

    public <T> T getInstance(String name, Class<T> type) {
        AnnotationConfigApplicationContext context = getContext(name);
        if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
                type).length > 0) {
            return context.getBean(type);
        }
        return null;
    }   
}

More important than that is the createContext(name) method for creating each RibbonClient's ApplicationContext, which contains the logic for creating an implementation class of the Ribbon core interface based on which @Configuration configuration class, so focus on analysis (Ribbon core interface explanation) Reference resources)

When the createContext(name) method creates the current Ribbon Client-related context and injects the configuration class, how do other configuration classes, such as the default global configuration class, configure for a Ribbon Client other than the default configuration class RibbonClientConfiguration, which is dead?


Configuration load for Spring Cloud RibbonClient, including global configuration and Client configuration

Create RibbonClient corresponding ApplicationContext and register all available Configuration configuration classes

//## org.springframework.cloud.context.named.NamedContextFactory#createContext()
protected AnnotationConfigApplicationContext createContext(String name) {
    AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
    
    // 1. Register the configuration class specified specifically for RibbonClient, @RibbonClient comment
    if (this.configurations.containsKey(name)) {
        for (Class<?> configuration : this.configurations.get(name)
                .getConfiguration()) {
            context.register(configuration);
        }
    }
    
    // 2. All RibbonClient configuration s will be registered with the ApplicationContext
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
        if (entry.getKey().startsWith("default.")) {
            for (Class<?> configuration : entry.getValue().getConfiguration()) {
                context.register(configuration);
            }
        }
    }
    
    // 3. Register defaultConfigType, Spring's default configuration class RibbonClientConfiguration
    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.refresh();  // Refresh Context
    return context;
}

From the logic above, you can see that the Ribbon-related Configuration Configuration class is registered in three places in the ApplicationContext context specifically prepared for it, and the implementation class for the Ribbon core interface is created from the configuration class, that is, to configure the RibbonClient

  1. Obtain the configurationConfiguration class specifically specified for RibbonClient name from the configurations Map and register it with its corresponding ApplicationContext context
  2. From the configurations Map, find the default. configuration class that starts with, which is the default configuration for all RibbonClient s, and register it with its corresponding ApplicationContext context
  3. If not specified separately by the developer, neither of the first two items have data and the default configuration class RibbonClientConfiguration for Spring Cloud is registered

Where did the configuration class data in the Map come from?Here's a step-by-step analysis

//## RibbonAutoConfiguration
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();

@Bean
public SpringClientFactory springClientFactory() {
    SpringClientFactory factory = new SpringClientFactory();
    factory.setConfigurations(this.configurations);
    return factory;
}

First, the creation of the SpringClientFactory in the RibbonAutoConfiguration auto configuration class is set. This configuration collection is the RibbonClientSpecification collection in the Spring container of @Autowired. When was the RibbonClientSpecification collection registered?

//## org.springframework.cloud.netflix.ribbon.RibbonClientConfigurationRegistrar
public class RibbonClientConfigurationRegistrar implements ImportBeanDefinitionRegistrar {

    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        // 1. @RibbonClients comment
        Map<String, Object> attrs = metadata.getAnnotationAttributes(
                RibbonClients.class.getName(), true);
        // 1.1 value is a RibbonClient[], traverses the configurationConfiguration class for a specific RibbonClient configuration, and registers
        if (attrs != null && attrs.containsKey("value")) {
            AnnotationAttributes[] clients = (AnnotationAttributes[]) attrs.get("value");
            for (AnnotationAttributes client : clients) {
                registerClientConfiguration(registry, getClientName(client),
                        client.get("configuration"));
            }
        }
        // 1.2 Found defaultConfiguration for the @RibbonClients annotation, which is the default configuration
        //     Register as RibbonClientSpecification under default.Classname.RibbonClientSpecification
        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 comment
        // Register a specific Ribbon Client's configuration configuration class
        Map<String, Object> client = metadata.getAnnotationAttributes(
                RibbonClient.class.getName(), true);
        String name = getClientName(client);
        if (name != null) {
            registerClientConfiguration(registry, name, client.get("configuration"));
        }
    }

    private String getClientName(Map<String, Object> client) {
        if (client == null) {
            return null;
        }
        String value = (String) client.get("value");
        if (!StringUtils.hasText(value)) {
            value = (String) client.get("name");
        }
        if (StringUtils.hasText(value)) {
            return value;
        }
        throw new IllegalStateException(
                "Either 'name' or 'value' must be provided in @RibbonClient");
    }

    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());
    }
}

As you can see above, the configurations configuration class collection is configured according to the @RibbonClient and @RibbonClients annotations, with a specific RibbonClient configuration and default default default configuration, respectively

To summarize, how the Ribbon-related @Configuration configuration class is loaded

  1. After creating the AnnotationConfigApplicationContext corresponding to the RibbonClient, first find the configuration class corresponding to the current RibbonClient name from the set of configurations loaded according to the @RibbonClient and @RibbonClients annotations, and if so register it with the context
  2. Then, from the configurations collection, find the default configuration class that starts with default. loaded according to the @RibbonClients annotation, and if so, register it with the context
  3. Finally register the default RubbonClientConfiguration for Spring Cloud


The above is how to create the RibbonClient-related ApplicationContext context and the logic to register Ribbon Client-related configuration classes. After determining the configuration class, Ribbon's IClientConfig-related client configuration is used to load Ribbon client-related configuration information, such as timeout configuration, which core is created specifically.The implementation class of the interface, etc., can be explored from the RubbonClientConfiguration registered by default by Spring Cloud


RibbonClientConfiguration Configuration Loading and Ribbon Core Interface Implementation Class Creation

//## org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration
@Import({OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {

    @Value("${ribbon.client.name}")
    private String name = "client";

    // TODO: maybe re-instate autowired load balancers: identified by name they could be
    // associated with ribbon clients

    @Autowired
    private PropertiesFactory propertiesFactory;

    @Bean
    @ConditionalOnMissingBean
    public IClientConfig ribbonClientConfig() {
        DefaultClientConfigImpl config = new DefaultClientConfigImpl();
        config.loadProperties(this.name);
        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;
    }

Only one piece of code was intercepted above to show how Ribbon-related IClientConfig client configurations and a core interface IRule implementation class load configurations and create them

IClientConfig

IClientConfig is the interface for the Ribbon client configuration. You can see that the DefaultClientConfigImpl default implementation class is created, then config.loadProperties(this.name) loads the current Client-related configuration

//## com.netflix.client.config.DefaultClientConfigImpl#loadProperties()
/**
 * Load properties for a given client. It first loads the default values for all properties,
 * and any properties already defined with Archaius ConfigurationManager.
 */
@Override
public void loadProperties(String restClientName){
    enableDynamicProperties = true;
    setClientName(restClientName);
    
    // 1. Use Configuration Manager of Netflix Archaius to load default configurations such as ribbon.configuration items from Spring env elope
    //   If not loaded into default static configuration
    loadDefaultValues();
    
    // 2. Load configuration information for a client from Spring env elope using Configuration Manager for Netflix Archaius such as "clientname.ribbon.configuration item"
    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));
        }
    }
}

According to the note above, if you do not specify ribbon-related configurations in your project, the default static configurations in DefaultClientConfigImpl will be used. If the Spring enviroment contains "ribbon.configurations" for all clients, such as "clientname.ribbon.configurations" for oneConfiguration information for Clients will also be loaded

The static configuration is as follows:


RibbonClient Core Interface Implements Class Configuration Loading and Creation

After describing how the IClientCOnfig configuration item was loaded, it is reasonable to say that it already contains which core interface RibbonClient currently uses to implement the class configuration, but Spring Cloud defines its own implementation logic here

@Autowired
private PropertiesFactory propertiesFactory;

@Bean
@ConditionalOnMissingBean
public IRule ribbonRule(IClientConfig config) {
    // Check to see if propertiesFactory has a configuration for the current interface, use it if it does, and create an instance to return
    if (this.propertiesFactory.isSet(IRule.class, name)) {
        return this.propertiesFactory.get(IRule.class, config, name);
    }
    
    // spring cloud default configuration
    ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
    rule.initWithNiwsConfig(config);
    return rule;
}

Let's look at the logic of 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");
    }

    // See if the current clazz is one of several core interfaces managed by classToProperty
    // If so, check to see if the configuration information for "clientName.ribbon.Core Interface Configuration Item" can be found in the Spring environment
    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;
    }

    // Also call getClassName() first to get the core interface implementation class name configured in Spring enviroment
    // Reuse IClientConfig configuration information to create its instance
    @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;
    }
}

So create the IRule interface above to implement the logic of the class

  • First check through propertiesFactory if the configuration information for the IRule core interface implementation class for the current Ribbon Client is configured in Spring enviroment, and if so, create its instance return (related configuration format: clientName.ribbon.NFLoadBalancerRuleClassName=specific IRule implementation class)
  • If not, instead of using Netflix's static configuration in its DefaultClientConfigImpl directly, use Spring Cloud's custom default implementation class, which is ZoneAvoidanceRule with IRule rule interface

Summary:

The ApplicationContext context for the RibbonClient is created first, and the Configuration configuration class used is determined

1. @RibbonClients registered global default configuration class

2. @RibbonClient registered Client configuration class

3. Spring Cloud default RubbonClientConfiguration configuration class

Once a configuration class is determined, the Client-related IClientConfig configuration information is loaded and a core interface implementation class is created

If there is no custom global/client configuration class, then RibbonClientConfiguration is used, and its rules are

For configurations such as timeouts (in addition to core interface implementation classes): use Netflix's configuration logic, use ribbon.xxx as the default configuration, and clientName.ribbon.xxx as the client-customized configuration

For core interface implementation class configuration: Client customization configuration still uses clientName.ribbon.xxx, but the default configuration is the default implementation class that Spring Cloud writes to death in the RibbonClientConfiguration method

Now that you know the general logic, let's see how to customize the Client configuration, global configuration


How to customize RibbonClient configuration, global configuration

This section is described in the official reference of Spring Cloud 16.2 Customizing the Ribbon Client

The general meaning is as follows:

  • Some configurations (configurations of non-core interface implementation classes) can be configured using the way provided by the Netflix native API, even if configurations such as.ribbon. *, refer to com.netflix.client.config.CommonClientConfigKey for specific configuration items.

  • If you want to take full control of the RibbonClient and add some additional configurations, you can use the @RibbonClient or @RibbonClients annotations and configure a configuration class such as FooConfiguration above

    • @RibbonClient(name = "foo", configuration = FooConfiguration.class) is a configuration class for RibbonClient named foo, or you can set a configuration class for several RibbonClients in the form of @RibbonClients({@RibbonClient array})

    • @RibbonClients (defaultConfiguration = {xxx.class}) is the default configuration for all RIbbonClient s

      • The official document says that the FooConfiguration configuration class must be @Configuration, so it must be noted that the SpringBoot main startup class cannot scan into FooConfiguration, otherwise the configuration for a RibbonClient becomes global because the ApplicationContext context is created for each RibbonClient when it is created.The parent is the ApplicationContext created by the main startup class. Beans in the parent ApplicationContext can be used in the child ApplicationContext, and @ConditionalOnMissingBean is used when creating beans, so if FooConfiguration is loaded by the context of the main startup class and an implementation class such as IRule is created, in a RIbboWhen nClient creates its child ApplicationContext and @Bean wants to create its custom IRule implementation class, it finds that the parent ApplicationContext already exists, it will not be created, and the configuration will fail

        However, in my experiment, even FooConfiguration could be loaded as a RibbonClient configuration without the @Configuration annotation, and since there was no @Configuration, it would not be scanned by the main boot class

So there are two main configurations:

(1) Static configuration such as timeout, configure all Clients with ribbon. * and configure a Client with.Ribbon. *

(2) Which core interface to use for class configuration, default configuration using the @RibbonClients annotation, and Client configuration using @RibbonClient (note @Configuration should not be scanned by the SpringBoot main boot class)

Posted by Jumba on Sun, 21 Jul 2019 19:48:27 -0700