Open Feign
1. understand (what is open feign)
Open feign is a declarative HTTP request client, which is used to quickly initiate HTTP requests and reduce learning costs
Added annotation support for SpringMVC on the basis of Feign
2. simple use
2.1 importing jar s
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>2.1.0.RELEASE</version> </dependency>
2.2 develop service end and consumer end codes
2.2.1 server
package com.zy.more.controller; import com.zy.more.Page; import com.zy.more.Result; import com.zy.more.entity.InventoryDO; import com.zy.more.service.InventoryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.PageRequest; import org.springframework.web.bind.annotation.*; /** * @author: zhangyao * @create:2020-06-30 19:07 **/ @RestController @RequestMapping("/inventory") public class InventoryController { @Autowired InventoryService inventoryService @GetMapping("/{productId}") public Result<InventoryDO> getInventoryById(@PathVariable("productId") Integer productId){ return inventoryService.getInventoryById(productId); } }
2.2.2 consumer (just develop the interface)
Add @EnableFeignClients to the startup class
package com.zy.more; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients; /** * @author: zhangyao * @create:2020-07-01 11:37 **/ @SpringBootApplication @EnableDiscoveryClient @EnableFeignClients public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }
Development interface
package com.zy.more.feign; import com.zy.more.Result; import com.zy.more.entity.InventoryDO; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.*; import javax.persistence.GeneratedValue; /** * @author: zhangyao * @create:2020-07-01 16:42 **/ @FeignClient(name = "inventory",path = "/inventory") public interface InventoryFeign { /** * Query inventory information by commodity id * @Date: 2020/7/1 17:02 * @Author: zhangyao * @Description: * @param productId: * @return: com.zy.more.Result<com.zy.more.entity.InventoryDO> **/ @GetMapping("/{productId}") public Result<InventoryDO> getInventoryById(@PathVariable("productId") Integer productId); }
2.2.3 interpretation
The server provides services. The controller can be developed normally without any changes
The consumer only needs to add a development interface, which is consistent with the controller provided by the server, and then use @FeignClient @EnableFeignClients to enable Feign. When calling the interface, inject the developed Feign interface through @Autowired
3. principle analysis
The core principle of openFeign is the dynamic proxy of jdk
- Use @EnabledFeignClients to enable Feign and scan all feignclients
- Generate a Bean of dynamic proxy object for each FeignClient
- When this Bean is used, the Request is sent when it is converted to a Request
So the whole OpenFeign can be understood as two parts
- Dynamic proxy generates Request
- Send Request
Sending a Request uses Java by default Net package, which can be replaced with highly available http clients such as okhttp
4. source code analysis
4.1 scan registration
Enter the feignclientregister class through @EnableFeignClients, scan all feignclients, and register the corresponding feignclients into the Spring container
FeignClientsRegistrar registration class
See these two methods for details
//Enter here from the SpringBoot startup class and scan all FeignClient classes public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = this.getScanner(); scanner.setResourceLoader(this.resourceLoader); //Get the attribute value of the @EnableFeignClients annotation, such as basePackage Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName()); AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class); //If clients are configured in @EnableFeignClients, the corresponding clients will be loaded here Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients")); Object basePackages; //If there are corresponding clients, these clients will be configured if (clients != null && clients.length != 0) { final Set<String> clientClasses = new HashSet(); basePackages = new HashSet(); Class[] var9 = clients; int var10 = clients.length; for(int var11 = 0; var11 < var10; ++var11) { Class<?> clazz = var9[var11]; ((Set)basePackages).add(ClassUtils.getPackageName(clazz)); clientClasses.add(clazz.getCanonicalName()); } AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { protected boolean match(ClassMetadata metadata) { String cleaned = metadata.getClassName().replaceAll("\\$", "."); return clientClasses.contains(cleaned); } }; scanner.addIncludeFilter(new FeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); } else { //Go here without a client to get the configured basePackages scanner.addIncludeFilter(annotationTypeFilter); basePackages = this.getBasePackages(metadata); } //Cycle through all basePackages and scan client s under each package Iterator var17 = ((Set)basePackages).iterator(); while(var17.hasNext()) { String basePackage = (String)var17.next(); Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage); Iterator var21 = candidateComponents.iterator(); while(var21.hasNext()) { BeanDefinition candidateComponent = (BeanDefinition)var21.next(); if (candidateComponent instanceof AnnotatedBeanDefinition) { //Loop through each FeignClient and get the corresponding properties AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent; AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName()); String name = this.getClientName(attributes); this.registerClientConfiguration(registry, name, attributes.get("configuration")); //Register the corresponding FeignClient into the Spring container this.registerFeignClient(registry, annotationMetadata, attributes); } } } } //Configure the properties of each FeignClient. The properties here are the properties to obtain the @FeignClien annotation private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); this.validate(attributes); definition.addPropertyValue("url", this.getUrl(attributes)); definition.addPropertyValue("path", this.getPath(attributes)); String name = this.getName(attributes); definition.addPropertyValue("name", name); String contextId = this.getContextId(attributes); definition.addPropertyValue("contextId", contextId); definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(2); String alias = contextId + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); boolean primary = (Boolean)attributes.get("primary"); beanDefinition.setPrimary(primary); String qualifier = this.getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias}); BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
4.2 injection call
When using @Autowired injection to use FeignClient, a jdk dynamic proxy will be instantiated through the FeignClientFactory factory class to return. Finally, the http client will call the Ribbon's load balancing policy to generate a Request and send a network Request
4.2.1 step 1 FeignClientFactory
FeignClientFactory class
Factorybean is implemented. When spring calls getObject, it returns a jdk dynamic proxy object
public Object getObject() throws Exception { return this.getTarget(); } <T> T getTarget() { FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class); Builder builder = this.feign(context); //If feignClient does not specify a url, name is used as the url if (!StringUtils.hasText(this.url)) { if (!this.name.startsWith("http")) { this.url = "http://" + this.name; } else { this.url = this.name; } this.url = this.url + this.cleanPath(); //Returning a dynamic proxy object will be described in detail below return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url)); } else {//If there is a url if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { this.url = "http://" + this.url; } String url = this.url + this.cleanPath(); Client client = (Client)this.getOptional(context, Client.class); if (client != null) { if (client instanceof LoadBalancerFeignClient) { client = ((LoadBalancerFeignClient)client).getDelegate(); } builder.client(client); } //Also return the dynamic proxy object Targeter targeter = (Targeter)this.get(context, Targeter.class); return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url)); } }
4.2.2 step 2 Proxy
Let's take a look at the generation of dynamic proxy objects through the loadBalance method above
protected <T> T loadBalance(Builder builder, FeignContext context, HardCodedTarget<T> target) { Client client = (Client)this.getOptional(context, Client.class); if (client != null) { builder.client(client); Targeter targeter = (Targeter)this.get(context, Targeter.class); return targeter.target(this, builder, context, target); } else { throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); } }
We found that we need to obtain a client object first. By default, this client object uses the LoadBalanceFeignClient class that implements the client interface under Feign package. By default, this class uses the Ribbon's load balancing send request. The send request calls the execute method in this object (described in detail below), which can replace other high-performance clients. Then Feign wraps it and returns a Builder, Finally, use the target to generate dynamic proxy
Specify the target interface
There are two implementation classes
The default implementation of defaulttarget and hystrixtarger is the latter
public <T> T target(FeignClientFactoryBean factory, Builder feign, FeignContext context, HardCodedTarget<T> target) { if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { //If you don't use Hystrix, go here return feign.target(target); } else { feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder)feign; SetterFactory setterFactory = (SetterFactory)this.getOptional(factory.getName(), context, SetterFactory.class); if (setterFactory != null) { builder.setterFactory(setterFactory); } Class<?> fallback = factory.getFallback(); if (fallback != Void.TYPE) { return this.targetWithFallback(factory.getName(), context, target, builder, fallback); } else { Class<?> fallbackFactory = factory.getFallbackFactory(); return fallbackFactory != Void.TYPE ? this.targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory) : feign.target(target); } } }
Then call Feign Target construct Feign object
Feign class
public <T> T target(Target<T> target) { return this.build().newInstance(target); } public Feign build() { Factory synchronousMethodHandlerFactory = new Factory(this.client, this.retryer, this.requestInterceptors, this.logger, this.logLevel, this.decode404, this.closeAfterDecode, this.propagationPolicy); ParseHandlersByName handlersByName = new ParseHandlersByName(this.contract, this.options, this.encoder, this.decoder, this.queryMapEncoder, this.errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder); }
You can see that the final target method calls the newInstance method of ReflectiveFeign, an inherited class of Feign class
public <T> T newInstance(Target<T> target) { Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList(); //Get methods in feignClient Method[] var5 = target.type().getMethods(); int var6 = var5.length; //Loop the Method in the Method, establish the corresponding mapping relationship between the Method and the handler, and put it into a linkedMap for(int var7 = 0; var7 < var6; ++var7) { Method method = var5[var7]; if (method.getDeclaringClass() != Object.class) { if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, (MethodHandler)nameToHandler.get(Feign.configKey(target.type(), method))); } } } //InvocationHandler required to build dynamic proxy InvocationHandler handler = this.factory.create(target, methodToHandler); //Generate proxy object T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler); Iterator var12 = defaultMethodHandlers.iterator(); while(var12.hasNext()) { DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next(); defaultMethodHandler.bindTo(proxy); } return proxy; }
You can see that what is returned here is a proxy object of the jdk, which is also the object finally returned in the second step. Later, when calling, the specific method will be called through this dynamic proxy object
Take a closer look at this method
Each method corresponds to a defaultmethodhandle. The default implementation class of defaultmethodhandler is SynchronousMethodHandler
When a dynamic proxy object calls an interface, it calls the invoke method of SynchronousMethodHandler
4.2.3 step 3 call
The process when a request is actually sent:
invoke method of SynchronousMethodHandler
//Send the request through executeAndDecode in this method public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = this.buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer.clone(); while(true) { try { return this.executeAndDecode(template); } catch (RetryableException var8) { RetryableException e = var8; try { retryer.continueOrPropagate(e); } catch (RetryableException var7) { Throwable cause = var7.getCause(); if (this.propagationPolicy == ExceptionPropagationPolicy.UNWRAP && cause != null) { throw cause; } throw var7; } if (this.logLevel != Level.NONE) { this.logger.logRetry(this.metadata.configKey(), this.logLevel); } } } } //Send request Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = this.targetRequest(template); if (this.logLevel != Level.NONE) { this.logger.logRequest(this.metadata.configKey(), this.logLevel, request); } long start = System.nanoTime(); Response response; try { //The client here is the LoadBalanceFeignClient object we used in step 2 above response = this.client.execute(request, this.options); } catch (IOException var15) { if (this.logLevel != Level.NONE) { this.logger.logIOException(this.metadata.configKey(), this.logLevel, var15, this.elapsedTime(start)); } throw FeignException.errorExecuting(request, var15); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); boolean shouldClose = true; try { if (this.logLevel != Level.NONE) { response = this.logger.logAndRebufferResponse(this.metadata.configKey(), this.logLevel, response, elapsedTime); } if (Response.class == this.metadata.returnType()) { Response var18; if (response.body() == null) { var18 = response; return var18; } else if (response.body().length() != null && (long)response.body().length() <= 8192L) { byte[] bodyData = Util.toByteArray(response.body().asInputStream()); Response var20 = response.toBuilder().body(bodyData).build(); return var20; } else { shouldClose = false; var18 = response; return var18; } } else { Object result; Object var10; if (response.status() >= 200 && response.status() < 300) { if (Void.TYPE != this.metadata.returnType()) { result = this.decode(response); shouldClose = this.closeAfterDecode; var10 = result; return var10; } else { result = null; return result; } } else if (this.decode404 && response.status() == 404 && Void.TYPE != this.metadata.returnType()) { result = this.decode(response); shouldClose = this.closeAfterDecode; var10 = result; return var10; } else { throw this.errorDecoder.decode(this.metadata.configKey(), response); } } } catch (IOException var16) { if (this.logLevel != Level.NONE) { this.logger.logIOException(this.metadata.configKey(), this.logLevel, var16, elapsedTime); } throw FeignException.errorReading(request, response, var16); } finally { if (shouldClose) { Util.ensureClosed(response.body()); } } }
From here, we can find that the class that finally sends the request is LoadBalanceFeignClient
4.2.4 step 4 send request
Sending a request is about ribbon
public Response execute(Request request, Options options) throws IOException { try { URI asUri = URI.create(request.url()); String clientName = asUri.getHost(); URI uriWithoutHost = cleanUrl(request.url(), clientName); RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost); IClientConfig requestConfig = this.getClientConfig(options, clientName); return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse(); } catch (ClientException var8) { IOException io = this.findIOException(var8); if (io != null) { throw io; } else { throw new RuntimeException(var8); } } }
5. extension
5.1 extended client
Modify the default java Net package is HttpClient,OkHttpClient
OkHttpClient
Import jar package
<!-- extend openFeign httpClient okhttpClient--> <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> <version>11.0</version> </dependency>
Looking at the configuration class FeignAutoConfiguration of openFeign
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.cloud.openfeign; import feign.Client; import feign.Feign; import feign.httpclient.ApacheHttpClient; import feign.okhttp.OkHttpClient; import java.util.ArrayList; import java.util.List; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.TimeUnit; import javax.annotation.PreDestroy; import okhttp3.ConnectionPool; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.impl.client.CloseableHttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration @ConditionalOnClass({Feign.class}) @EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class}) public class FeignAutoConfiguration { @Autowired( required = false ) private List<FeignClientSpecification> configurations = new ArrayList(); public FeignAutoConfiguration() { } @Bean public HasFeatures feignFeature() { return HasFeatures.namedFeature("Feign", Feign.class); } @Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); context.setConfigurations(this.configurations); return context; } @Configuration @ConditionalOnClass({OkHttpClient.class}) @ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"}) @ConditionalOnMissingBean({okhttp3.OkHttpClient.class}) @ConditionalOnProperty({"feign.okhttp.enabled"}) protected static class OkHttpFeignConfiguration { private okhttp3.OkHttpClient okHttpClient; protected OkHttpFeignConfiguration() { } @Bean @ConditionalOnMissingBean({ConnectionPool.class}) public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, OkHttpClientConnectionPoolFactory connectionPoolFactory) { Integer maxTotalConnections = httpClientProperties.getMaxConnections(); Long timeToLive = httpClientProperties.getTimeToLive(); TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit); } @Bean public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) { Boolean followRedirects = httpClientProperties.isFollowRedirects(); Integer connectTimeout = httpClientProperties.getConnectionTimeout(); Boolean disableSslValidation = httpClientProperties.isDisableSslValidation(); this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation).connectTimeout((long)connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects).connectionPool(connectionPool).build(); return this.okHttpClient; } @PreDestroy public void destroy() { if (this.okHttpClient != null) { this.okHttpClient.dispatcher().executorService().shutdown(); this.okHttpClient.connectionPool().evictAll(); } } @Bean @ConditionalOnMissingBean({Client.class}) public Client feignClient(okhttp3.OkHttpClient client) { return new OkHttpClient(client); } } @Configuration @ConditionalOnClass({ApacheHttpClient.class}) @ConditionalOnMissingClass({"com.netflix.loadbalancer.ILoadBalancer"}) @ConditionalOnMissingBean({CloseableHttpClient.class}) @ConditionalOnProperty( value = {"feign.httpclient.enabled"}, matchIfMissing = true ) protected static class HttpClientFeignConfiguration { private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true); @Autowired( required = false ) private RegistryBuilder registryBuilder; private CloseableHttpClient httpClient; protected HttpClientFeignConfiguration() { } @Bean @ConditionalOnMissingBean({HttpClientConnectionManager.class}) public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory, FeignHttpClientProperties httpClientProperties) { final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit(), this.registryBuilder); this.connectionManagerTimer.schedule(new TimerTask() { public void run() { connectionManager.closeExpiredConnections(); } }, 30000L, (long)httpClientProperties.getConnectionTimerRepeat()); return connectionManager; } @Bean public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) { RequestConfig defaultRequestConfig = RequestConfig.custom().setConnectTimeout(httpClientProperties.getConnectionTimeout()).setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build(); this.httpClient = httpClientFactory.createBuilder().setConnectionManager(httpClientConnectionManager).setDefaultRequestConfig(defaultRequestConfig).build(); return this.httpClient; } @Bean @ConditionalOnMissingBean({Client.class}) public Client feignClient(HttpClient httpClient) { return new ApacheHttpClient(httpClient); } @PreDestroy public void destroy() throws Exception { this.connectionManagerTimer.cancel(); if (this.httpClient != null) { this.httpClient.close(); } } } @Configuration @ConditionalOnMissingClass({"feign.hystrix.HystrixFeign"}) protected static class DefaultFeignTargeterConfiguration { protected DefaultFeignTargeterConfiguration() { } @Bean @ConditionalOnMissingBean public Targeter feignTargeter() { return new DefaultTargeter(); } } @Configuration @ConditionalOnClass( name = {"feign.hystrix.HystrixFeign"} ) protected static class HystrixFeignTargeterConfiguration { protected HystrixFeignTargeterConfiguration() { } @Bean @ConditionalOnMissingBean public Targeter feignTargeter() { return new HystrixTargeter(); } } }
Obviously, two conditions are required to inject okhttpClient
- We need to import the jar package we started to import. feign's adaptation to okHttpClient
- Feign needs to be configured in the configuration file Okhttp Enabled = true
The configuration of HttpClient is the same
6. compare each client
openFeign | RestTemplate | httpClient | OkHttpClient | |
---|---|---|---|---|
load balancing | Support (embedded Ribbon) | Native is not supported (ribbon needs to be introduced) | Not supported | Not supported |