Feign源码分析(五) - spring对Feign的扩展

595 阅读5分钟

@Author:zxw

@Email:502513206@qq.com


目录

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

1.前言

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

  1. 对Client的封装,整合ribbon和hytrix
  2. contract解析接口注解

2.扩展

2.1 Client

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

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

这里有个注意的点就是,如果配置了url,那么spring就会使用OKHTTPHTTPCLIENT其中一种,就不会使用ribbon了,所以如果想使用spring提供的Client,那么只需要name属性即可。

spring为Client创建提供了一个工厂类FeignClientFactoryBean,在该类的getObject()方法会构建我们的builder对象,最后调用target()生成接口的实例,对于client使用的判断如下

		// 判断url是否存在
	   if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
           // 如果不存在则使用loadBalance客户端
			return (T) loadBalance(builder, context,
					new HardCodedTarget<>(this.type, this.name, this.url));
		}
		if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
			this.url = "http://" + this.url;
		}
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				// not load balancing because we have a url,
				// but ribbon is on the classpath, so unwrap
				client = ((LoadBalancerFeignClient) client).getDelegate();
			}
			if (client instanceof FeignBlockingLoadBalancerClient) {
				// not load balancing because we have a url,
				// but Spring Cloud LoadBalancer is on the classpath, so unwrap
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}

2.1.1 ApacheHttpClient

对于调用,无非就是将内部的请求客户端换成httpClient那一套。不过spring默认情况下使用的是ApacheHttpClient来进行调用。可以看到默认的配置值为true,然后注入Client对象为ApacheHttpClient

@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

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