In the previous article, Ribbon Architecture Analysis , we have introduced Ribbon's architecture and many important objects. I believe you already have a clear understanding of Ribbon. This article studies the principle of Ribbon
First of all, we know that the use of Ribbon in ordinary projects is like this
@SpringBootApplication
@RibbonClient(name = "provider-demo", configuration = cn.org.config.LoadBalanced.class)
public class CloudDemoConsumerApplication {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(CloudDemoConsumerApplication.class, args);
}
}
The most eye-catching thing here is the annotation @RibbonClient
. Let's see what this annotation does.
@RibbonClient
The observed @RibbonClient
source code shows that this annotation uses the @Import
annotation to introduce the configuration class RibbonClientConfigurationRegistrar
. Let's take a look at the registerBeanDefinitions
method of this class.
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
Map<String, Object> attrs = metadata.getAnnotationAttributes(
RibbonClients.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"));
}
}
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(
RibbonClient.class.getName(), true);
String name = getClientName(client);
if (name != null) {
registerClientConfiguration(registry, name, client.get("configuration"));
}
}
- First, it will determine whether there is an annotation
@RibbonClients
. Note that there is an extra s here. - Then judge
@RibbonClients
whether there are attributesvalue
and sums on the annotation, anddefaultConfiguration
if so, register them respectively - The last step is to process
@RibbonClient
annotations - Here we can guess that
RibbonClientConfigurationRegistrar
this class should be able to process these two annotations at the same time. Looking at@RibbonClients
the source code of the annotation, we can see that it is indeed the class that was introduced. - The difference between these two annotations should also be guessed, singular and double
- Observe the last registered code, you can see that the types of the last registered bean are all
RibbonClientSpecification
, pay attention here
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());
}
automatic assembly
After reading these codes above, we understand @RibbonClients
and @RibbonClient
two annotations, we can still have some doubts about the overall process. Then let's take a look at what automatic assembly does.
Check the files under the Ribbon package spring.factories
and find that a configuration class has been introduced RibbonAutoConfiguration
, so let's start with this class
prerequisites
@ConditionalOnClass
, these classes must exist in the current environment:IClient
,RestTemplate
,AsyncRestTemplate
,Ribbon
@RibbonClients
, this annotation has been mentioned just now, let's not mention it for now@AutoConfigureAfter
, the load balancing must be done based on the registry, so the automatic assembly is initialized after Eureka is initialized@AutoConfigureBefore
, the two classes here aside, keep it mysterious@EnableConfigurationProperties
, two configuration classes, where:RibbonEagerLoadProperties
The class is about the properties of Ribbon's starvation loading modeServerIntrospectorProperties
The class is about the properties of the secure port
assembly bean
There are quite a few classes loaded in this configuration class, but the more important ones are:
SpringClientFactory
, we know that each microservice will call multiple microservices, and the configuration of calling each microservice may be different, so we need this factory class to create a client load balancer, which can be used for each ribbon client A different Spring context is generated, and observing theconfigurations
properties of this class also verifies this
@Autowired(required = false)
private List<RibbonClientSpecification> configurations = new ArrayList<>();
@Bean
public SpringClientFactory springClientFactory() {
SpringClientFactory factory = new SpringClientFactory();
factory.setConfigurations(this.configurations);
return factory;
}
RibbonLoadBalancerClient
, holds theSpringClientFactory
object, of course, it has other functions, which will not be mentioned here
load balancing
Although I saw the Ribbon's automatic assembly function above, it seems that there is still some distance from the truth. This is because although the Ribbon is ready, the load balancing has not yet been seen. SpringCloud puts the automatic configuration related to load balancing in the spring-cloud-commons package. The configuration class for load balancing isLoadBalancerAutoConfiguration
Several beans registered in this class are more core
LoadBalancerInterceptor
Client request interceptor
RestTemplateCustomizer
Used to RestTemplate
add interceptors to all
@Bean
@ConditionalOnMissingBean
public RestTemplateCustomizer restTemplateCustomizer(
final LoadBalancerInterceptor loadBalancerInterceptor) {
return restTemplate -> {
List<ClientHttpRequestInterceptor> list = new ArrayList<>(
restTemplate.getInterceptors());
list.add(loadBalancerInterceptor);
restTemplate.setInterceptors(list);
};
}
Load balancing core implementation
Now we can guess that the whole core should be on this interceptor, take a look at the core method of the interceptor:
public ClientHttpResponse intercept(final HttpRequest request, final byte[] body,
final ClientHttpRequestExecution execution) throws IOException {
final URI originalUri = request.getURI();
String serviceName = originalUri.getHost();
Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
return this.loadBalancer.execute(serviceName, requestFactory.createRequest(request, body, execution));
}
The requestFactory.createRequest(request, body, execution)
method is to encapsulate the request parameters as request. Focus on the execute
method
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
Create a load balancer
We know that the load balancer of each Ribbon client is unique, and the first line getLoadBalancer
will create this load balancer
protected ILoadBalancer getLoadBalancer(String serviceId) {
return this.clientFactory.getLoadBalancer(serviceId);
}
public ILoadBalancer getLoadBalancer(String name) {
return getInstance(name, ILoadBalancer.class);
}
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);
}
The final logic is to get it from the cache if it exists, and create it if it doesn't exist
static <C> C instantiateWithConfig(AnnotationConfigApplicationContext context,
Class<C> clazz, IClientConfig config) {
C result = null;
try {
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;
}
RibbonClientSpecification
The process of creating a big topic is to create several types of configurations registered by the two annotations mentioned at the beginning of the article.
Get service
getServer
The implementation of the method should be able to guess, using a specific load balancer combined with the corresponding load balancing algorithm plus service list filtering, service health detection and other operations will finally obtain an available service
call service
Here, the service is encapsulated asRibbonServer
private final String serviceId;
private final Server server;
private final boolean secure;
private Map<String, String> metadata;
In addition to these properties, RibbonServer
there is also a method
public URI getUri() {
return DefaultServiceInstance.getUri(this);
}
This method converts the service from an instance id to a callable url
public static URI getUri(ServiceInstance instance) {
String scheme = (instance.isSecure()) ? "https" : "http";
String uri = String.format("%s://%s:%s", scheme, instance.getHost(),
instance.getPort());
return URI.create(uri);
}
Then send the http request