Spring Cloud 组件原理系列 Feign篇

4,739 阅读13分钟

白菜Java自习室 涵盖核心知识

Spring Cloud 组件原理系列(一)Eureka篇
Spring Cloud 组件原理系列(二)Hystrix篇
Spring Cloud 组件原理系列(三)Feign篇

1. Feign 简介

Feign 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求。Spring Cloud 引入 Feign 并且集成了 Ribbon 实现客户端负载均衡调用。

Feign 解决了什么问题

封装了 Http 调用流程,更适合面向接口化的变成习惯。

6271376-285b0cf66bc1b24c.webp

2. Feign 工作原理

Feign 远程调用流程:

  1. 基于面向接口的动态代理方式生成实现类
  2. 根据 Contract 协议规则,解析接口类的注解信息,解析成内部表现
  3. 基于 RequestBean,动态生成 Request
  4. 使用 Encoder 将 Bean 转换成 Http 报文正文
  5. 拦截器负责对请求和返回进行装饰处理
  6. 日志记录器记录请求日志
  7. 基于重试器发送 HTTP 请求

6271376-7635e2dc9b32e3ec.png

2.1. 基于面向接口的动态代理方式生成实现类

在使用 Feign 时,会定义对应的接口类,在接口类上使用 Http 相关的注解,标识 Http 请求参数信息。 在 Feign 底层,通过基于面向接口的动态代理方式生成实现类,将请求调用委托到动态代理实现类,基本原理如下所示:

14126519-4949493085b0f547.png

2.2. 根据 Contract 协议规则,解析接口类的注解信息,解析成内部表现

6271376-cebfed0fa4f18190.png

2.3. 基于 RequestBean,动态生成 Request

根据传入的 Bean 对象和注解信息,从中提取出相应的值,来构造 Http Request 对象。

2.4. 使用 Encoder 将 Bean 转换成 Http 报文正文

Feign 最终会将请求转换成Http 消息发送出去,传入的请求对象最终会解析成消息体

14126519-b5c571b44f453707.png

2.5. 拦截器负责对请求和返回进行装饰处理

在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作,比如,如果希望 Http 消息传递过程中被压缩,可以定义一个请求拦截器。

2.6. 日志记录器记录请求日志

2.7. 基于重试器发送 HTTP 请求

Feign 内置了一个重试器,当 HTTP 请求出现 IO 异常时,Feign 会有一个最大尝试次数发送请求。

Feign 真正发送HTTP请求是委托给 feign.Client 来做的。

Feign 默认底层通过 JDK 的 java.net.HttpURLConnection 实现了 feign.Client 接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下 Feign 的性能很差的原因。可以通过拓展该接口,使用 Apache HttpClient 或者 OkHttp3 等基于连接池的高性能 Http 客户端。

Feign 整体框架非常小巧,在处理请求转换和消息解析的过程中,基本上没什么时间消耗。真正影响性能的,是处理 Http c请求的环节。

3. Feign 配置优化

3.1. GZIP 压缩

gzip 是一种数据格式,采用 deflate 算法压缩数据。当 gzip 压缩到一个纯文本数据时,可以减少 70% 以上的数据大小。

GZIP 作用:网络数据经过压缩后实际上降低了网络传输的字节数,最明显的好处就是可以加快网页加载的速度。

只配置 Feign 请求-应答的 gzip 压缩:

# feign gzip
# 局部配置。只配置feign技术相关的http请求-应答中的gzip压缩。
# 配置的是application client和application service之间通讯是否使用gzip做数据压缩。
# 和浏览器到application client之间的通讯无关。
# 开启feign请求时的压缩, application client -> application service
feign.compression.request.enabled=true
# 开启feign技术响应时的压缩,  application service -> application client
feign.compression.response.enabled=true
# 设置可以压缩的请求/响应的类型。
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 当请求的数据容量达到多少的时候,使用压缩。默认是2048字节。
feign.compression.request.min-request-size=512

配置全局的 gzip 压缩:

# spring boot gzip
# 开启spring boot中的gzip压缩。就是针对和当前应用所有相关的http请求-应答的gzip压缩。
server.compression.enabled=true
# 哪些客户端发出的请求不压缩,默认是不限制
server.compression.excluded-user-agents=gozilla,traviata
# 配置想压缩的请求/应答数据类型,默认是 text/html,text/xml,text/plain
server.compression.mime-types=application/json,application/xml,text/html,text/xml,text/plain
# 执行压缩的阈值,默认为2048
server.compression.min-response-size=512

3.2. 使用 HTTP 连接池提供性能

Feign 的 HTTP 客户端支持3种框架,分别是;HttpURLConnection、HttpClient、OKHttp。Feign 中默认使用 HttpURLConnection。

  • HttpURLConnection 是 JDK 自带的 HTTP 客户端技术,并不支持连接池,如果要实现连接池的机制,还需要自己来管理连接对象。对于网络请求这种底层相对复杂的操作,如果有可用的其他方案,也没有必要自己去管理连接对象。
  • Apache 提供的 HttpClient 框架相比传统 JDK 自带的 HttpURLConnection,它封装了访问http的请求头,参数,内容体,响应等等;它不仅使客户端发送HTTP请求变得容易,而且也方便了开发人员测试接口(基于Http协议的),即提高了开发的效率,也方便提高代码的健壮性;另外高并发大量的请求网络的时候,还是用“HTTP连接池”提升吞吐量
  • OKHttp 是一个处理网络请求的开源项目,是安卓端最火热的轻量级框架。OKHttp拥有共享Socket,减少对服务器的请求次数,通过连接池,减少了请求延迟等技术特点

通过替换 Feign 底层的 HTTP 客户端实现为 HttpClient,来提升 Feign 的通讯性能。

feign.httpclient.enabled=true

4. Feign 和 Ribbon 结合

Feign 就是对于 Ribbon 的一个简单封装。

Ribbon 简单点讲就是通过 @LoadBalanced 去负载到相应的服务中去,其中通过 Http(一般配合RestTemplate)请求去访问提供者接口,Ribbon 通过拦截器/代理将服务名等替换为 IP 等信息进行访问,如果提供者存在集群情况,Ribbon 会根据相应的算法(默认轮询、随机、权重等)进行负载均衡。

4.1. Ribbon 源码分析

  1. 以 LoadBalancerAutoConfiguration 为入口

这个配置类就是初始化几个 Bean。然后用 RestTemplateCustomizers 去定制所有我们加了 @LoadBalanced注解的 RestTemplate。简单点就是将使用了 @LoadBalanced 的 RestTemplate 的拦截器放进拦截器集合中,使得其拦截器在使用 RestTemplate 时生效。

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RestTemplate.class})
@ConditionalOnBean({LoadBalancerClient.class})
@EnableConfigurationProperties({LoadBalancerRetryProperties.class})
public class LoadBalancerAutoConfiguration {
 
    // 存放了所有被 @LoadBalanced 修饰的 restTemplate
    @LoadBalanced
    @Autowired(required = false )
    private List<RestTemplate> restTemplates = Collections.emptyList();
 
    @Autowired(required = false)
    private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();
 
    public LoadBalancerAutoConfiguration() {
    }
    
    // 1、SmartInitializingSingleton
    // 该 bean 会用 restTemplateCustomizers 定制所有的 restTemplate
    @Bean
    public SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {
        return () -> {
            restTemplateCustomizers.ifAvailable((customizers) -> {
                Iterator var2 = this.restTemplates.iterator();
 
                while(var2.hasNext()) {
                    RestTemplate restTemplate = (RestTemplate)var2.next();
                    Iterator var4 = customizers.iterator();
 
                    while(var4.hasNext()) {
                        RestTemplateCustomizer customizer = (RestTemplateCustomizer)var4.next();
                        customizer.customize(restTemplate);
                    }
                }
 
            });
        };
    }
 
        // 2、RestTemplateCustomizer 
        // 该 bean 用于定制 restTemplate。定制的逻辑是将 LoadBalancerInterceptor 拦截器封装到 restTemplate 的拦截器集合去(之后 restTemplate 会对拦截器集合进行统一拦截处理处理)
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final RetryLoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }
 
    // 3、LoadBalancerInterceptor ,这个拦截器就是我们去替换 IP等请求路径的拦截器    
    @Configuration(
        proxyBeanMethods = false
    )
    @ConditionalOnMissingClass({"org.springframework.retry.support.RetryTemplate"})
    static class LoadBalancerInterceptorConfig {
        LoadBalancerInterceptorConfig() {
        }
 
        @Bean
        public LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient, LoadBalancerRequestFactory requestFactory) {
            return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);
        }
 
        @Bean
        @ConditionalOnMissingBean
        public RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {
            return (restTemplate) -> {
                List<ClientHttpRequestInterceptor> list = new ArrayList(restTemplate.getInterceptors());
                list.add(loadBalancerInterceptor);
                restTemplate.setInterceptors(list);
            };
        }
    }
}
  1. 拦截器的实现 intercept() 方法

我们在使用 RestTemplate 时:

restTemplate.postForObject("http://cloud-payment-service"+"/payment/create", payment, CommonResult.class)

其中 URI 即截取上面的请求路径,而 servername 则是获取服务名。然后将服务名称放进 execute() 方法,后面会根据注册中心获取到相应服务名的真实 IP 端口等信息。

public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException {
        URI originalUri = request.getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri);
        return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution));
    }

我们进入 excute() 方法,看看是怎么进行获取真实服务 IP 等信息的。

 public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint) throws IOException {
        // 1、获取一个负载均衡器
        ILoadBalancer loadBalancer = this.getLoadBalancer(serviceId);
        // 2、多个实例时,根据负载均衡器的算法选择出一个实例出来
        Server server = this.getServer(loadBalancer, hint);
        if (server == null) {
            throw new IllegalStateException("No instances available for " + serviceId);
        } else {
            RibbonLoadBalancerClient.RibbonServer ribbonServer = new RibbonLoadBalancerClient.RibbonServer(serviceId, server, this.isSecure(server, serviceId), this.serverIntrospector(serviceId).getMetadata(server));
            return this.execute(serviceId, (ServiceInstance)ribbonServer, (LoadBalancerRequest)request);
        }
    }

我们需要进去看看他是怎么获取到一个负载均衡器的(这里我们不进去该方法,因为最后调用的是一个接口,所以我们直接去找他的实现类),查找 RibbonClientConfiguration 配置类。

@Configuration
@EnableConfigurationProperties
@Import({HttpClientConfiguration.class, OkHttpRibbonConfiguration.class, RestClientRibbonConfiguration.class, HttpClientRibbonConfiguration.class})
public class RibbonClientConfiguration {
   
    @Bean
    @ConditionalOnMissingBean
    public ILoadBalancer ribbonLoadBalancer(IClientConfig config, ServerList<Server> serverList, ServerListFilter<Server> serverListFilter, IRule rule, IPing ping, ServerListUpdater serverListUpdater) {
        // 如果配置文件有配置使用哪个负载均衡器就优先使用配置文件的。
        // 否则则使用ZoneAwareLoadBalancer
        return (ILoadBalancer)(this.propertiesFactory.isSet(ILoadBalancer.class, this.name) ? (ILoadBalancer)this.propertiesFactory.get(ILoadBalancer.class, config, this.name) : new ZoneAwareLoadBalancer(config, rule, ping, serverList, serverListFilter, serverListUpdater));
    }
 
    // 加载负载均衡规则rule
    @Bean
    @ConditionalOnMissingBean
    public IRule ribbonRule(IClientConfig config) {
        // 先从配置文件中拿
        if (this.propertiesFactory.isSet(IRule.class, this.name)) {
            return (IRule)this.propertiesFactory.get(IRule.class, config, this.name);
        } else {
            // 没有则使用默认的负载均衡规则
            ZoneAvoidanceRule rule = new ZoneAvoidanceRule();
            rule.initWithNiwsConfig(config);
            return rule;
        }
    }
 
    @Bean
    @ConditionalOnMissingBean
    public IPing ribbonPing(IClientConfig config) {
        return (IPing)(this.propertiesFactory.isSet(IPing.class, this.name) ? (IPing)this.propertiesFactory.get(IPing.class, config, this.name) : new DummyPing());
    }
    
}

此时我们已经找到了默认的负载均衡器 ZoneAwareLoadBalancer ,来看该负载均衡器是怎么进行负载均衡的。

  1. ZoneAwareLoadBalancer 中 chooseServer()方法实现负载均衡

多个实例时,根据负载均衡器的算法选择出一个实例出来。 进入 getServer() 方法, 这里调用了负载均衡器的 loadBalancer.chooseServer() 方法,即选择一个实例出来。该 chooseServer() 方法中会配合 注册中心 的信息去获取一个实例。

protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
        return loadBalancer == null ? null : loadBalancer.chooseServer(hint != null ? hint : "default");
    }

我们要搞清楚是怎么匹配注册中、怎么进行负载均衡的,这是就要回去负载均衡器中查看 chooseServer() 方法是怎么实现的了。

public Server chooseServer(Object key) {
        if (ENABLED.get() && this.getLoadBalancerStats().getAvailableZones().size() > 1) {
            // 省略
            }
        } else {
            // 进入该方法
            logger.debug("Zone aware logic disabled or there is only one zone");
            return super.chooseServer(key);
        }
    }

进入父类方法 super.chooseServer(key) 看看实现:

public Server chooseServer(Object key) {
        if (this.counter == null) {
            this.counter = this.createCounter();
        }
 
        this.counter.increment();
        if (this.rule == null) {
            return null;
        } else {
            try {
                return this.rule.choose(key);
            } catch (Exception var3) {
                logger.warn("LoadBalancer [{}]:  Error choosing server for key {}", new Object[]{this.name, key, var3});
                return null;
            }
        }
    }

进入 rule.choose() 方法。发现是一个接口,打开实现类发现就是所谓的8个负载均衡算法实现类。

public interface IRule {

    Server choose(Object var1);
 
    void setLoadBalancer(ILoadBalancer var1);
 
    ILoadBalancer getLoadBalancer();
    
}

而我们默认的是走 PredicateBasedRule 这个实现类(轮询),(其实从 RibbonClientConfiguration 中注入的 ribbonRule 中使用的是默认的 ZoneAvoidanceRule,而其实现又是 PredicateBasedRule 所以也可以再一次判断默认就是走 PredicateBasedRule 规则),进入看下该类:

 
public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule {
    public PredicateBasedRule() {
    }
 
    public abstract AbstractServerPredicate getPredicate();
 
    public Server choose(Object key) {
        ILoadBalancer lb = this.getLoadBalancer();
        // 这里使用轮询的算法
        Optional<Server> server = this.getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
        return server.isPresent() ? (Server)server.get() : null;
    }
}

进入轮询方法:

public Optional<Server> chooseRoundRobinAfterFiltering(List<Server> servers, Object loadBalancerKey) {
 
        // 获取可以实例集合。这里在讲怎么配合nacos那里再讲怎么用
        List<Server> eligible = this.getEligibleServers(servers, loadBalancerKey);
        // incrementAndGetModulo进行轮询算法
        return eligible.size() == 0 ? Optional.absent() : Optional.of(eligible.get(this.incrementAndGetModulo(eligible.size())));
    }

进去 incrementAndGetModulo() 方法看看:

 private int incrementAndGetModulo(int modulo) {
        int current;
        int next;
        do {
            current = this.nextIndex.get();
            next = (current + 1) % modulo;
        } while(!this.nextIndex.compareAndSet(current, next) || current >= modulo);
 
        return current;
    }

计算方法

current = this.nextIndex.get();      
next = (current + 1) % modulo;  

这里我们拿到的是上一个 next 值,然后把我们现在的值 current+1 后取模然后 cas 存进去。下一次取的就是这次的 next 值。

例如我们有 2 台机器(分别代表0、1),则此时传进去的 modulo 是 2。

第一次时:current 为 0,next 为 1。此时返回 0 回去,则代表访问第一台机器(0 号机器)。

第二次时:corrent 为 1(上次的 next),next 为 0,此时返回 1,则代表访问第一台机器。

4.2. Feign 源码分析

Feign 的源码非常简单,就是使用代理将 Ribbon 进行了封装。

@Component
@FeignClient(value = "cloud-payment-service",fallback = PaymentHystrixServiceImpl.class)
public interface PaymentFeignService {
 
    @GetMapping(value = "/payment/get/{id}")
    public CommonResult getPaymentById(@PathVariable("id") Long id);
 
    @GetMapping(value="/payment/lb")
    public String getPaymentLB() throws InterruptedException;
}
 
启动类再加上个 @EnableFeignClients 注解

Feign 简单的讲就是根据 Ribbon 拿到的实例信息,然后把上面的服务名+接口地址,然后帮我们把 uri 替换拼接,最后用 http 请求去访问。

特别思考

看到这里,是否就觉得,Ribbon 不是帮我们做了吗?没错,Ribbon 是有做这些,但是如果使用 Ribbon,我们需要去注入一个带有 @LoadBalanced 的 restTemplate,然后再用这个 restTemplate 去自己发送请求(然后 Ribbon 内部帮我们去修改 uri)。当使用时 Feign 不用我们去写这么多了,Feign 使用了声明式接口,帮我们把注入 restTemplate 和手动调用 restTemplate 的方法都给封装好了,我们只需要告诉他我们要调用哪个服务的哪个接口即可完成调用

简单点讲,Feign 对于 Ribbon 进行进一步封装,简化了我们的使用。

Feign 用的是 okHttpclient 。

以 @EnableFeignClients 注解为入口:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
    String[] value() default {};
 
    String[] basePackages() default {};
 
    Class<?>[] basePackageClasses() default {};
 
    Class<?>[] defaultConfiguration() default {};
 
    Class<?>[] clients() default {};
}

进入 @Import({FeignClientsRegistrar.class}) 这个类,看到 registerBeanDefinitions() 方法,该方法将 @EnableFeignClients 里配置的信息和 @FeignClients 的接口都注入 Spring 容器中:

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
 
        // 扫描@EnableFeignClients里配置的信息并注册
        this.registerDefaultConfiguration(metadata, registry);
 
        // 看名字即注册FeignClients,即我们写的feign的接口注册到spring容器中
        this.registerFeignClients(metadata, registry);
    }

进入下面的方法 registerFeignClients(metadata, registry):

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
        // 扫描器
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
 
        // 扫描所有feignclient注解的信息并放到容器中
        Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
        AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
        Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
        Object basePackages;
        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 {
            scanner.addIncludeFilter(annotationTypeFilter);
            basePackages = this.getBasePackages(metadata);
        }
 
        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) {
                    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"));
 
                    // 进入registerFeignClient方法
                    this.registerFeignClient(registry, annotationMetadata, attributes);
                }
            }
        }
 
    }

进入 registerFeignClient(registry, annotationMetadata, attributes) 方法:

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
 
        // 生成代理的工厂bean(FeignClientFactoryBean),
        // 并且将feignclient注解的相关信息给到该工厂bean,并且将其放进spring容器
        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);
    }

进入该工厂 FeignClientFactoryBean 中,其中有个 loadBalance() 方法利用 jdk 动态代理生成代理对象。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
 
 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?");
        }
    }
 
}

最后 jdk 动态代理会生成一个 LoadBalancerFeignClient 的代理对象。该类中有个 execute() 方法,我们在使用 feign 接口调用时,实际是在使用 LoadBalancerFeignClient 的代理对象调用它的 execute() 方法。

public class LoadBalancerFeignClient implements Client {
 
    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去调用整合的ribbon从而实现的负载均衡
            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);
            }
        }
    }
 
}

Feign 和 Ribbon 结合原理

  1. Feign 通过动态代理得到一个代理工厂 bean:FeignClientFactoryBean,该工厂 bean 最后会生成一个 LoadBalancerFeignClient 的代理对象;
  2. 当我们去调用 Feign 接口时,Feign 的代理对像会调用 execute() 方法,将 url 进行拼接(含服务名的 url,url 的替换还是需要 Ribbon 来完成),然后使用RibbonRequest 去调用整合的 Ribbon 实现负载均衡;
  3. 最后 Feign 拿到了拼接好的 url(替换了服务名的url)进行 http 请求(Ribbon 接到调用时,会通过拦截器去调用 Ribbon 自身的 execute() 方法从而实现负载均衡整个过程)(Feign 最后还是要用 Ribbon 来替换服务名、服务列表信息等)。

Spring Cloud 组件原理系列(一)Eureka篇
Spring Cloud 组件原理系列(二)Hystrix篇
Spring Cloud 组件原理系列(三)Feign篇