Spring Cloud OpenFeign源码分析,为什么不导入Ribbon应用会启动不起来?

原创 吴就业 132 0 2020-06-27

本文为博主原创文章,未经博主允许不得转载。

本文链接:https://wujiuye.com/article/7c7eb3472aef4d049f9bd7dc21dcfa37

作者:吴就业
链接:https://wujiuye.com/article/7c7eb3472aef4d049f9bd7dc21dcfa37
来源:吴就业的网络日记
本文为博主原创文章,未经博主允许不得转载。

本篇文章写于2020年06月27日,从公众号|掘金|CSDN手工同步过来(博客搬家),本篇为原创文章。

Spring Cloud Kubernetes微服务实战与源码分析

本篇内容:

为什么使用feign

因为我们想像dubbo调用远程服务一样,节省构建请求body并发送http请求,还要手动反序列化响应结果的步骤。使用feign能够让我们像同进程的接口方法调用一样调用远程进程的接口。

feignspring cloud组件中的一个轻量级restfulhttp服务客户端,内置了ribbon(因此使用feign也需要引入ribbon的依赖)。openfeignspring cloudfeign的基础上支持了spring mvc的注解,如@RequesMapping@GetMapping@PostMapping等。

使用openfeign声明接口的例子:

@FeignClient(name = 'sck-demo-provider',
        path = "/v1",
        url = "http://sck-demo-provider",
        primary = false)
public interface DemoService {

    @GetMapping("/hello")
    GenericResponse<String> sayHello();

}

feign用于服务消费端,即接口调用端,因此需要将服务提供端暴露的接口提取出来创建一个Module。当然,服务提供端也会依赖这个Module,因为数据传输对象DTO需要共用,也将DTO类跟接口放在一起,但不推荐服务提供者强制使用implements去实现接口。

源码分析

使用openfegin我们可以不用在yaml文件添加任何关于openfegin的配置,而只需要在一个被@Configuration注释的配置类上或者Application启动类上添加@EnableFeignClients注解。例如:

@EnableFeignClients(basePackages = {"com.wujiuye.sck.consumer"})
public class SckDemoConsumerApplication {
}

basePackages属性用于指定被@FeignClient注解注释的接口所在的包的包名,或者也可以直接指定clients属性,clients属性可以直接指定一个或多个被@FeignClient注释的类。basePackages是一个数组,如果被@FeignClient注解注释的接口比较分散,可以指定多个包名,而不使用一个大的包名,这样可以减少包扫描耗费的时间,不拖慢应用的启动速度。

@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

@EnableFeignClients注解使用@Import导入FeignClientsRegistrar类,这是一个ImportBeanDefinitionRegistrar,因此我们重点关注它的registerBeanDefinitions方法。(关于Spring的知识点,默认大家都懂了)。

class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
		registerDefaultConfiguration(metadata, registry);
		registerFeignClients(metadata, registry);
	}
}

重点关注registerFeignClients方法,该方法负责读取@EnableFeignClients的属性,获取需要扫描的包名,然后扫描指定的所有包名下的被@FeignClient注解注释的接口,将扫描出来的接口调用registerFeignClient方法注册到spring容器。

class FeignClientsRegistrar
		implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
    
    private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		definition.addPropertyValue("contextId", contextId);
		// ......
		definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);

		String alias = contextId + "FeignClient";
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
	
		boolean primary = (Boolean) attributes.get("primary");
		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}
		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}
}

registerFeignClient源码所示,该方法根据读取@FeignClient注解的属性配置,以及该接口的类名信息,向Spring bean工厂注册一个FeignClientFactoryBean,从名称可以看出这是一个FactoryBean,因此接下来我们主要看这个FactoryBeangetObject方法、getObjectType方法。getObjectType方法不用说,肯定是返回当前的被@FeignClient注解注释的那个接口的类名。

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    @Override
    public Object getObject() throws Exception {
        return getTarget();
    }
}

getObject方法调用getTarget方法,但由于getTarget方法太长,只截取部分。

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    
    <T> T getTarget() {
		FeignContext context = this.applicationContext.getBean(FeignContext.class);
		Feign.Builder builder = feign(context);
		// .......
		String url = this.url + cleanPath();
		Client client = getOptional(context, Client.class);
		if (client != null) {
			if (client instanceof LoadBalancerFeignClient) {
				client = ((LoadBalancerFeignClient) client).getDelegate();
			}
			if (client instanceof FeignBlockingLoadBalancerClient) {
				client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
			}
			builder.client(client);
		}
		Targeter targeter = get(context, Targeter.class);
		return (T) targeter.target(this, builder, context,
				new HardCodedTarget<>(this.type, this.name, url));
	}
}

Clienthttp协议接口调用的实现,其定义如下:

public interface Client {

  Response execute(Request request, Options options) throws IOException;

}

正常情况下,getTarget方法中调用getOptional方法获取到的ClientNULL。不正常情况就是添加了ribbonstarter包,这时拿到的ClientLoadBalancerFeignClient,我们后面分析。

不管怎样,FeignClientFactoryBeangetTarget方法最后都是调用Targettarget方法来获取实现该接口的实例。Target的实现类有两个:DefaultTargeterHystrixTargeter。在不使用Hystrix的情况下,我们只分析DefaultTargeter的实现。

class DefaultTargeter implements Targeter {

	@Override
	public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
			FeignContext context, Target.HardCodedTarget<T> target) {
		return feign.target(target);
	}

}

如上源码所示,DefaultTargeter调用Feign.Builder实例的target方法生成接口的实例,我们继续跟踪target方法的调用链,直到找到创建接口实例的方法。

如图所示,Feign.BuildernewInstance方法正是创建接口实例的方法。有两种实现,一种是支持接口方法异步调用的,一种是普通的同步调用实现。不过这里用的还是ReflectiveFeign

public class ReflectiveFeign extends Feign {

  @Override
  public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    // ......
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    // ......
    InvocationHandler handler = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
        new Class<?>[] {target.type()}, handler);
    // .......
    return proxy;
  }
}

很熟悉的JDK动态代理。因此,Feign并不会为接口生成实现类,而是生成一个动态代理对象。factory.create这句创建的InvocationHandler正是FeignInvocationHandler,此InvocationHandlerJDK实现动态代理的InvocationHandler,而MethodHandlerfeign定义的MethodHandlermethodToHandler存储的是接口中定义的方法与feign生成的MethodHandler映射关系。

static class FeignInvocationHandler implements InvocationHandler {

    private final Target target;
    private final Map<Method, MethodHandler> dispatch;

    FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
      this.target = checkNotNull(target, "target");
      this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      // ....
      return dispatch.get(method).invoke(args);
    }

}

当我们调用接口的方法时,都会走到FeignInvocationHandlerinvoke方法,invoker方法根据method获取对应的MethodHandler,并调用MethodHandlerinvoke方法。往后MethodHandler要做的事情我们也基本能猜测得出来了。

想要找出这些MethodHandler在哪创建的,就需要回头从FeignClientFactoryBeangetTarget调用DefaultTargetertarget方法开始,DefaultTargetertarget方法直接调用Feign.Buildertarget方法,Feign.Buildertarget方法在调用newInstance方法之前调用了自身的build方法。

public class Feign{
 public static class Builder{
    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }
    public Feign build() {
      //......
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }
  }
}

build生成的Feign实例是ReflectiveFeign,因此,ReflectiveFeignnewInstance方式的这句:

Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);

targetToHandlersByName就是ParseHandlersByName的实例,我们来看下ParseHandlersByNameapply方法。

static final class ParseHandlersByName{
    public Map<String, MethodHandler> apply(Target target) {
      // 解析接口的方法,生成MethodMetadata实例
      List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type());
      Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
      // 遍历接口方法
      for (MethodMetadata md : metadata) {
        // ......
        if (md.isIgnored()) {
          result.put(md.configKey(), args -> {
            throw new IllegalStateException(md.configKey() + " is not a method handled by feign");
          });
        } else {
           // 调用Factory实例的create方法来创建MethodHandler
          result.put(md.configKey(),
              factory.create(target, md, buildTemplate, options, decoder, errorDecoder));
        }
      }
      return result;
    }
}

apply方法负责解析接口的方法,并为每个接口方法调用Factory实例的create方法创建MethodHandler

static class Factory {

    public MethodHandler create(Target<?> target,
                                MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options,
                                Decoder decoder,
                                ErrorDecoder errorDecoder) {
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
          logLevel, md, buildTemplateFromArgs, options, decoder,
          errorDecoder, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
    }
}

创建的MethodHandler的类型是SynchronousMethodHandler。因此,当我们调用接口的方法时,最终调用的是SynchronousMethodHandlerinvoke方法。

public class SynchronousMethodHandler implements MethodHandler{
  @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        return executeAndDecode(template, options);
      } catch (RetryableException e) {
        try {
          retryer.continueOrPropagate(e);
        } catch (RetryableException th) {
          Throwable cause = th.getCause();
          if (propagationPolicy == UNWRAP && cause != null) {
            throw cause;
          } else {
            throw th;
          }
        }
        if (logLevel != Logger.Level.NONE) {
          logger.logRetry(metadata.configKey(), logLevel);
        }
        continue;
      }
    }
  }
}

invoke方法做的事情就是包装请求参数调用接口,如果配置了重试次数,失败会重试。还记得FeignClientFactoryBeangetTarget方法调用Targettarget方法时传递的一个HardCodedTarget实例吗?这个就是用来生成请求参数Request的。

executeAndDecode方法:

public class SynchronousMethodHandler implements MethodHandler{

  Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);

    Response response;
    long start = System.nanoTime();
    try {
      response = client.execute(request, options);
      response = response.toBuilder()
          .request(request)
          .requestTemplate(template)
          .build();
    } catch (IOException e) {
      throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    if (decoder != null)
      return decoder.decode(response, metadata.returnType());
    // .......
  }
}

Client我们前面提到过,就是实现发送http协议请求的,那SynchronousMethodHandler实例的这个client是哪个Client的实现类呢?答案在Fegin的内部类Builder

public class Fegin{
    public static class Build{
        private Client client = new Client.Default(null, null);
    }
}

其实上面分析的源码过程,到FeginBuilder开始,就是Fegin的代码,而不是openfeign。文章开头说openfeign支持spring mvc的注解,但是我们好像跳过了,这里提供一个调用链,大家可以根据这个调用链去找源码。

feign.ReflectiveFeign.ParseHandlersByName.apply
    > feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>)
        > feign.Contract.BaseContract.parseAndValidateMetadata(java.lang.Class<?>, java.lang.reflect.Method)
            > org.springframework.cloud.openfeign.support.SpringMvcContract.processAnnotationOnMethod

SpringMvcContractprocessAnnotationOnMethod方法源码如下:

public class SpringMvcContract extends Contract.BaseContract
		implements ResourceLoaderAware {
    @Override
	protected void processAnnotationOnMethod(MethodMetadata data,
			Annotation methodAnnotation, Method method) {
		if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
				.annotationType().isAnnotationPresent(RequestMapping.class)) {
			return;
		}
		RequestMapping methodMapping = findMergedAnnotation(method, RequestMapping.class);
		// HTTP Method
		RequestMethod[] methods = methodMapping.method();
		if (methods.length == 0) {
			methods = new RequestMethod[] { RequestMethod.GET };
		}
		checkOne(method, methods, "method");
		data.template().method(Request.HttpMethod.valueOf(methods[0].name()));
		// path
		checkAtMostOne(method, methodMapping.value(), "value");
		if (methodMapping.value().length > 0) {
			String pathValue = emptyToNull(methodMapping.value()[0]);
			if (pathValue != null) {
				pathValue = resolve(pathValue);
				if (!pathValue.equals("/")) {
					data.template().uri(pathValue, true);
				}
			}
		}
		// produces
		parseProduces(data, method, methodMapping);
		// consumes
		parseConsumes(data, method, methodMapping);
		// headers
		parseHeaders(data, method, methodMapping);
		data.indexToExpander(new LinkedHashMap<Integer, Param.Expander>());
	}
}

通过分析openfeign的源码,我们已经了解了openfeign是怎样与Spring整合的,以及feign到底做了什么,在源码分析的过程中,我们忽略了一些细节,而这些留到我们遇到问题时再去深挖。

疑问一:openfeign是怎么拿到url的?

你不好奇openfeign是怎么拿到注册中心的服务url的吗?

@FeignClient(name = YcpayConstant.SERVICE_NAME,
        path = "/v1",
        primary = false)

当我们未配置@FeignClienturl属性时,name就起作用了。FeignClientFactoryBeangetTarget方法被我们忽略的代码:

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    <T> T getTarget() {
		// .....
		if (!StringUtils.hasText(this.url)) {
			if (!this.name.startsWith("http")) {
			    // 就是这句
				this.url = "http://" + this.name;
			}
			else {
				this.url = this.name;
			}
			this.url += cleanPath();
			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;
		}
		// .......
    }
}

假设我们配置的namesck-demo-provider,那么生成的url就是:

http://sck-demo-provider

疑问二:

你不好奇吗?为什么使用openfeign时,不配置url,且不导入ribbon的依赖会报错?

异常信息如下:

No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?

在分析FeignClientFactoryBeangetTarget方法源码时,我们漏掉了一些代码:

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    <T> T getTarget() {
		// ......
		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);
		}
        //......
    }
}

两种情况: * 1、如果指定了URL,那么getOptional方法不会返回null,且返回的ClientLoadBalancerFeignClient,但不会抛出异常。 * 2、如果不指定URL,则走负载均衡逻辑,走的是loadBalance方法,且抛出异常。

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
    protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
			HardCodedTarget<T> target) {
            // getOptional的最终调用:        
            // public <T> T getInstance(String name, Class<T> type) {
        	//	AnnotationConfigApplicationContext context = getContext(name);
        	//	if (BeanFactoryUtils.beanNamesForTypeIncludingAncestors(context,
        	//			type).length > 0) {
        	//		return context.getBean(type);
        	//	}
        	//	return null;
        	// }   
		Client client = getOptional(context, Client.class);
		if (client != null) {
			builder.client(client);
			Targeter targeter = get(context, Targeter.class);
			return targeter.target(this, builder, context, target);
		}
		throw new IllegalStateException(
				"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
	}
}

根据前面的分析,正常情况下getOptional方法返回的Client绝对是NULL,所以就执行到了loadBalance方法的最后一行代码,抛出IllegalStateException异常。

现在我们可以猜测,难道添加ribbon之后,getOptional就不返回NULL了吗?自动创建了一个Client实例并交由Spring管理?这个Client又是什么?

我们看下spring-cloud-starter-openfeign依赖导入的spring-cloud-openfeign-core,查看 自动配置文件spring.factories

spring.factories文件的内容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration
// 其它省略

其中FeignRibbonClientAutoConfiguration应该就是我们要找的。

@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
		matchIfMissing = true)
@Configuration(proxyBeanMethods = false)
@AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
		OkHttpFeignLoadBalancedConfiguration.class,
		DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
    // ......
}

spring.cloud.loadbalancer.ribbon.enabled配置为true或者未配置时,@ConditionalOnProperty自动配置条件都会成立。但是,当不导入ribbonstarter时,ILoadBalancer是不存在的,@ConditionalOnClass不满足条件,只有导入ribbonstarter包时,才会导入HttpClientFeignLoadBalancedConfigurationOkHttpFeignLoadBalancedConfigurationDefaultFeignLoadBalancedConfiguration这几个配置类。

@Configuration(proxyBeanMethods = false)
class DefaultFeignLoadBalancedConfiguration {

	@Bean
	@ConditionalOnMissingBean
	public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
			SpringClientFactory clientFactory) {
		return new LoadBalancerFeignClient(new Client.Default(null, null),cachingFactory,clientFactory);
	}

}

所以,当不导入ribbonstarter时,ILoadBalancer不存在,FeignRibbonClientAutoConfiguration自动配置不会起作用,没有注入Client,但是因为没有配置url,所以走了loadBalanceloadBalance方法中拿不到Client,最终抛出异常。

那么怎么解决这个问题? 两种方法: * 1、 添加ribbonstarter依赖 如sck-demo项目种添加ribbonstarter

 <dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-kubernetes-ribbon</artifactId>
 </dependency>

END

笔者主要通过阅读源码解决自己的一些疑问,也希望通过本篇的分析能够帮助到大家。

#后端

声明:公众号、CSDN、掘金的曾用名:“Java艺术”,因此您可能看到一些早期的文章的图片有“Java艺术”的水印。

文章推荐

Ribbon重试策略RetryHandler的配置与源码分析

本篇我们再对Ribbon的重试机制地实现做详细分析,从源码分析找出我们想要地答案,即如何配置Ribbon实现调用每个服务的接口使用不一样的重试策略,如配置失败重试多少次,以及自定义重试策略RetryHandler。

OpenFeign与Ribbon源码分析总结与面试题

本篇介绍OpenFeign与Feign的关系、Feign底层实现原理、Ribbon是什么、Ribbon底层实现原理、Ribbon是如何实现失败重试的?

Spring Cloud Ribbon源码分析

本篇继续分析OpenFeign是如何与Ribbon整合、Ribbon是如何实现负载均衡的、Ribbon是如何从注册中心获取服务的。

将分布式项目sck-demo部署到本地kubernetes

本篇介绍如何搭建本地Kubernetes集群,以及将分布式项目sck-demo部署到本地kubernetes,以及实现版本升级和回滚。

Spring Cloud kubernetes入门项目sck-demo

本篇我们将从一个简单的demo上手Spring Cloud kubernetes,当然,我们只用到Spring Cloud kubernetes的服务注册与发现、配置中心模块。

为什么要选择Spring Cloud Kubernetes?

选择Spring Cloud Kubernetes意味着我们想要将服务部署到Kubernetes集群,Spring Cloud Kubernetes为我们实现了Spring Cloud的一些接口,让我们可以快速搭建Spring Cloud微服务项目框架,并能使用Kubernetes云原生服务。