@Author:zxw
@Email:502513206@qq.com


目录

  1. Feign源码分析(一) - 初探Feign
  2. Feign源码分析(二) - builder构建
  3. Feign源码分析(三) - Client调用
  4. Feign源码分析(四) - 自定义扩展点

    1.前言

    在上一篇文章中已经分析了Feign有哪些常见的扩展点供我们使用,其中比较关键的几个点有如下

  5. 对Client的封装,整合ribbon和hytrix

  6. contract解析接口注解

    2.扩展

    2.1 Client

    通过之前的分析了解到Feign默认提供的Client访问网络请求是使用原生java的网络请求。对于原生的HttpConnection听说性能上做的没有第三方工具包好,所以这边feign也提供了两种第三方的Client分别为OKHTTP和apache的HTTPCLIENT,spring则是提供了LoadBalancerFeignClientFeignBlockingLoadBalancerClient。我们先看下Spring提供的Feign配置

    1. @FeignClient(name = "order", url = "127.0.0.1:8082")
    2. public interface OrderFeignClient {}

    这里有个注意的点就是,如果配置了url,那么spring就会使用OKHTTPHTTPCLIENT其中一种,就不会使用ribbon了,所以如果想使用spring提供的Client,那么只需要name属性即可。
    spring为Client创建提供了一个工厂类FeignClientFactoryBean,在该类的getObject()方法会构建我们的builder对象,最后调用target()生成接口的实例,对于client使用的判断如下

    1. // 判断url是否存在
    2. if (!StringUtils.hasText(this.url)) {
    3. if (!this.name.startsWith("http")) {
    4. this.url = "http://" + this.name;
    5. }
    6. else {
    7. this.url = this.name;
    8. }
    9. this.url += cleanPath();
    10. // 如果不存在则使用loadBalance客户端
    11. return (T) loadBalance(builder, context,
    12. new HardCodedTarget<>(this.type, this.name, this.url));
    13. }
    14. if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
    15. this.url = "http://" + this.url;
    16. }
    17. String url = this.url + cleanPath();
    18. Client client = getOptional(context, Client.class);
    19. if (client != null) {
    20. if (client instanceof LoadBalancerFeignClient) {
    21. // not load balancing because we have a url,
    22. // but ribbon is on the classpath, so unwrap
    23. client = ((LoadBalancerFeignClient) client).getDelegate();
    24. }
    25. if (client instanceof FeignBlockingLoadBalancerClient) {
    26. // not load balancing because we have a url,
    27. // but Spring Cloud LoadBalancer is on the classpath, so unwrap
    28. client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
    29. }
    30. builder.client(client);
    31. }

    2.1.1 ApacheHttpClient

    对于调用,无非就是将内部的请求客户端换成httpClient那一套。不过spring默认情况下使用的是ApacheHttpClient来进行调用。可以看到默认的配置值为true,然后注入Client对象为ApacheHttpClient ```java @Configuration(proxyBeanMethods = false) @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnProperty(value = “feign.httpclient.enabled”, matchIfMissing = true) @Import(HttpClientFeignConfiguration.class) class HttpClientFeignLoadBalancedConfiguration {

    @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,

         SpringClientFactory clientFactory, HttpClient httpClient) {
     ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
     return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
    

    }

}

当然可以通过配置文件显示指定是否启用httpclient
```java
feign:
  httpclient:
    enabled: true

2.1.2 OkHttpClient

如果要使用okhttp,则使用如下配置

feign:
  okhttp:
    enabled: true

2.1.3 LoadBalancerFeignClient

该client是由spring的loadbalancer提供的实现,想要使用该客户端则可以在配置文件中加入以下配置即可。该配置默认为true,也就是服务调用默认是使用该Client。

spring:
    cloud:
        loadbalancer:
              ribbon:
                enabled: true

那么接下来看看该类的元数据有哪些

private final Client delegate;

private CachingSpringLoadBalancerFactory lbClientFactory;

private SpringClientFactory clientFactory;

可以看到内部还有一个client,那么猜测一下LoadBalancerFeignClient只是一个代理类,而内部的client才是远程请求真正发起的地方。对于lbClientFactory则是用来创建FeignLoadBalancer实例的。
那么思路现在大概也清晰了,就是先通过clientFactory拿到配置信息,随后通过lbClientFactory生成对应的LoadBalancer实现在发起请求

IClientConfig requestConfig = getClientConfig(options, clientName);
            return lbClient(clientName)
                    .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();

前面说过,内部的client才是发起http请求真正的地方,那么在executeWithLoadBalancer方法中肯定会获取client调用execute方法
AbstractLoadBalancerAwareClient#executeWithLoadBalancer 93Line

  return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));

接着execute往下,调用FeignLoadBalancer类的方法,可以看到从request参数中拿出了我们的client对象,最后调用的execute方法。

@Override
    public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
            throws IOException {
        Request.Options options;
        if (configOverride != null) {
            RibbonProperties override = RibbonProperties.from(configOverride);
            options = new Request.Options(override.connectTimeout(this.connectTimeout),
                    override.readTimeout(this.readTimeout));
        }
        else {
            options = new Request.Options(this.connectTimeout, this.readTimeout);
        }
        Response response = request.client().execute(request.toRequest(), options);
        return new RibbonResponse(request.getUri(), response);
    }

该client对象就是我们之前将的ApacheHttpClient或者OkHttpClient,另外我们看到Options的参数是根据ribbon的配置类来的,我们知道ribbon有全局的配置,不过那个配置在这里是不会生效的,如果想配置该ribbon应该像如下配置,以前经常会碰到配置不生效的问题,这下就能知道具体的解决方法了。

# 第一种方式
# 服务名
storage:
  #ribbon
  ribbon:
    #建立连接超时时间
    ConnectTimeout: 6000
    #建立连接之后,读取响应资源超时时间
    ReadTimeout: 6000

# 第二种方式
feign:
  client:
    config:
      storage:
        connect-timeout: 6000
        ReadTimeout: 6000

当然如果想使用全局配置的话,我们之前在分析Feign源码时,知道有个Options可以提供我们配置,那么我们在配置类中加上该参数即可,不过需要注意一点上面的配置文件优先级要高于下面这个

 @Bean
    public Request.Options options() {
        return new Request.Options(5000, 5000);
    }

2.1.4 FeignBlockingLoadBalancerClient

对于该客户端的话,其工作方式就是先选择一个服务实例,然后获取实例的请求地址发起调用,想要使用该类的话只需增加如下配置

spring:
    cloud:
        loadbalancer:
              ribbon:
                enabled: false

通过选择算法,从实例列表中获取一个客户端,熟悉ribbon的应该都了解这个逻辑

public Response execute(Request request, Request.Options options) throws IOException {
        final URI originalUri = URI.create(request.url());
        String serviceId = originalUri.getHost();
        Assert.state(serviceId != null,
                "Request URI does not contain a valid hostname: " + originalUri);
        ServiceInstance instance = loadBalancerClient.choose(serviceId);
        if (instance == null) {
            String message = "Load balancer does not contain an instance for the service "
                    + serviceId;
            if (LOG.isWarnEnabled()) {
                LOG.warn(message);
            }
            return Response.builder().request(request)
                    .status(HttpStatus.SERVICE_UNAVAILABLE.value())
                    .body(message, StandardCharsets.UTF_8).build();
        }
        String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri)
                .toString();
        Request newRequest = Request.create(request.httpMethod(), reconstructedUrl,
                request.headers(), request.body(), request.charset(),
                request.requestTemplate());
        return delegate.execute(newRequest, options);
    }

FeignBlockingLoadBalancerClient的逻辑相对来说简单一点,对于超时配置可以看到使用的Feign默认的Options,如果想配置超时的话那么就像上面一样配置全局的Options的bean或者下面这种方式

feign:
  client:
    config:
      storage:
        connect-timeout: 6000
        ReadTimeout: 6000

那么接下来总结一下,如果@FeignClient注解配置了url那么直接就是使用httpclient或者okhttp进行调用,如果是用的服务名,那么就会使用loadbalancer执行负载均衡策略,当然这个策略只是外面用来选择服务实例的,真正进行网络请求的还是内部的client,即httpclient或者okhttp。

2.2 contract

在spring中使用feign,可以使用spring提供的注解,例如@GetMapping("/order-tbl/create")等,之前分析过,对于接口上的方法解析是通过contract接口解析的,那么spring同样实现了该类的接口来实现接口解析。
SpringMvcContract感兴趣可以看下该接口的实现

2.3 其余

spring还提供了其他的封装,比如拦截器BaseRequestInterceptor